textus 0.54.2 → 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 +38 -1
- data/README.md +17 -10
- data/SPEC.md +39 -11
- data/docs/architecture/README.md +23 -11
- data/docs/reference/conventions.md +5 -2
- data/exe/textus +1 -1
- data/lib/textus/boot.rb +64 -93
- data/lib/textus/dispatch/binder.rb +48 -0
- 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 +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +4 -4
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +9 -10
- data/lib/textus/doctor.rb +2 -2
- data/lib/textus/errors.rb +8 -0
- data/lib/textus/format/base.rb +34 -10
- data/lib/textus/format/json.rb +5 -27
- data/lib/textus/format/markdown.rb +5 -27
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -27
- data/lib/textus/format.rb +22 -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 -7
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +4 -4
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -2
- 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/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/{ports → port}/audit_log.rb +46 -6
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/{ports → port}/sentinel_store.rb +4 -4
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/port/storage/file_store.rb +49 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +149 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- 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 +7 -10
- data/lib/textus/store/container.rb +173 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/entry/reader.rb +47 -0
- data/lib/textus/store/entry/writer.rb +219 -0
- 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 +76 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +124 -0
- data/lib/textus/store/jobs/queue.rb +162 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- 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 +8 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/layout.rb +97 -0
- data/lib/textus/store.rb +63 -32
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +14 -68
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +5 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +7 -5
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/surface/cli/verb/put.rb +27 -0
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/surface/mcp/catalog.rb +58 -0
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -11
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/{surfaces → surface}/mcp/server.rb +24 -35
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +90 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +46 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +11 -33
- data/lib/textus.rb +4 -69
- metadata +142 -105
- data/lib/textus/action/accept.rb +0 -44
- data/lib/textus/action/audit.rb +0 -131
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -97
- data/lib/textus/action/boot.rb +0 -17
- data/lib/textus/action/data_mv.rb +0 -69
- data/lib/textus/action/deps.rb +0 -34
- data/lib/textus/action/doctor.rb +0 -24
- data/lib/textus/action/drain.rb +0 -39
- data/lib/textus/action/enqueue.rb +0 -53
- data/lib/textus/action/get.rb +0 -78
- data/lib/textus/action/ingest.rb +0 -203
- data/lib/textus/action/jobs.rb +0 -36
- data/lib/textus/action/key_delete.rb +0 -36
- data/lib/textus/action/key_delete_prefix.rb +0 -44
- data/lib/textus/action/key_mv.rb +0 -129
- data/lib/textus/action/key_mv_prefix.rb +0 -57
- data/lib/textus/action/list.rb +0 -42
- data/lib/textus/action/propose.rb +0 -52
- data/lib/textus/action/published.rb +0 -24
- data/lib/textus/action/pulse.rb +0 -56
- data/lib/textus/action/put.rb +0 -52
- data/lib/textus/action/rdeps.rb +0 -39
- data/lib/textus/action/reject.rb +0 -37
- data/lib/textus/action/rule_explain.rb +0 -91
- data/lib/textus/action/rule_lint.rb +0 -68
- data/lib/textus/action/rule_list.rb +0 -42
- data/lib/textus/action/schema_envelope.rb +0 -29
- data/lib/textus/action/uid.rb +0 -33
- data/lib/textus/action/where.rb +0 -36
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/contract.rb +0 -174
- 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/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/gate/auth.rb +0 -227
- data/lib/textus/gate.rb +0 -116
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/ports/storage/file_store.rb +0 -26
- data/lib/textus/role.rb +0 -36
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/session.rb +0 -35
- data/lib/textus/surfaces/cli/verb/put.rb +0 -30
- data/lib/textus/surfaces/mcp/catalog.rb +0 -111
- data/lib/textus/surfaces/role_scope.rb +0 -34
- data/lib/textus/types.rb +0 -15
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Store
|
|
3
|
+
class Container
|
|
4
|
+
Infrastructure = Data.define(:file_store, :schemas, :audit_log, :job_store, :layout)
|
|
5
|
+
Coordination = Data.define(:manifest, :workflows, :pipeline)
|
|
6
|
+
|
|
7
|
+
def self.attribute_names
|
|
8
|
+
@attribute_names ||= [:root] + Infrastructure.members + Coordination.members
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(infra, coord)
|
|
12
|
+
@infra = infra
|
|
13
|
+
@coord = coord
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :infra, :coord, :pipeline, :reader, :writer
|
|
17
|
+
|
|
18
|
+
def root
|
|
19
|
+
@infra.layout.root
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Infrastructure.members.each do |name|
|
|
23
|
+
define_method(name) { @infra.public_send(name) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Coordination.members.each do |name|
|
|
27
|
+
define_method(name) { @coord.public_send(name) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def wire!(pipeline:, reader:, writer:)
|
|
31
|
+
@pipeline = pipeline
|
|
32
|
+
@reader = reader
|
|
33
|
+
@writer = writer
|
|
34
|
+
@coord = Coordination.new(
|
|
35
|
+
manifest: @coord.manifest,
|
|
36
|
+
workflows: @coord.workflows,
|
|
37
|
+
pipeline: pipeline,
|
|
38
|
+
)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.build(infra, coord_seed)
|
|
43
|
+
coord = Coordination.new(
|
|
44
|
+
manifest: coord_seed.manifest,
|
|
45
|
+
workflows: coord_seed.workflows,
|
|
46
|
+
pipeline: nil,
|
|
47
|
+
)
|
|
48
|
+
container = new(infra, coord)
|
|
49
|
+
pipeline = build_pipeline(container)
|
|
50
|
+
reader = Textus::Store::Entry::Reader.from(container: container)
|
|
51
|
+
writer = create_writer_factory(container)
|
|
52
|
+
container.wire!(pipeline: pipeline, reader: reader, writer: writer)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.orchestration_for(container)
|
|
56
|
+
Orchestration.new(
|
|
57
|
+
list_keys: Handlers::Read::ListKeys.new(manifest: container.manifest, job_store: container.job_store),
|
|
58
|
+
move_key: Handlers::Write::MoveKey.new(container: container, manifest: container.manifest),
|
|
59
|
+
delete_key: Handlers::Write::DeleteKey.new(container: container),
|
|
60
|
+
audit_entries: Handlers::Read::AuditEntries.new(manifest: container.manifest, audit_log: container.audit_log),
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.build_pipeline(container) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
65
|
+
registry = Dispatch::HandlerRegistry.new
|
|
66
|
+
fe = freshness_evaluator(container)
|
|
67
|
+
orch = orchestration_for(container)
|
|
68
|
+
|
|
69
|
+
registry.register(Dispatch::Contracts::GetEntry,
|
|
70
|
+
Handlers::Read::GetEntry.new(container: container, freshness_evaluator: fe))
|
|
71
|
+
registry.register(Dispatch::Contracts::PutEntry,
|
|
72
|
+
Handlers::Write::PutEntry.new(container: container))
|
|
73
|
+
registry.register(Dispatch::Contracts::ListKeys,
|
|
74
|
+
Handlers::Read::ListKeys.new(manifest: container.manifest, job_store: container.job_store))
|
|
75
|
+
registry.register(Dispatch::Contracts::DeleteKey,
|
|
76
|
+
Handlers::Write::DeleteKey.new(container: container))
|
|
77
|
+
registry.register(Dispatch::Contracts::MoveKey,
|
|
78
|
+
Handlers::Write::MoveKey.new(container: container, manifest: container.manifest))
|
|
79
|
+
registry.register(Dispatch::Contracts::ProposeEntry,
|
|
80
|
+
Handlers::Write::ProposeEntry.new(container: container))
|
|
81
|
+
registry.register(Dispatch::Contracts::AcceptProposal,
|
|
82
|
+
Handlers::Write::AcceptProposal.new(container: container))
|
|
83
|
+
registry.register(Dispatch::Contracts::RejectProposal,
|
|
84
|
+
Handlers::Write::RejectProposal.new(container: container))
|
|
85
|
+
registry.register(Dispatch::Contracts::EnqueueJob,
|
|
86
|
+
Handlers::Write::EnqueueJob.new(job_store: container.job_store))
|
|
87
|
+
registry.register(Dispatch::Contracts::WhereEntry,
|
|
88
|
+
Handlers::Read::WhereEntry.new(manifest: container.manifest))
|
|
89
|
+
registry.register(Dispatch::Contracts::UidEntry,
|
|
90
|
+
Handlers::Read::UidEntry.new(container: container))
|
|
91
|
+
registry.register(Dispatch::Contracts::DepsEntry,
|
|
92
|
+
Handlers::Read::DepsEntry.new(manifest: container.manifest))
|
|
93
|
+
registry.register(Dispatch::Contracts::RdepsEntry,
|
|
94
|
+
Handlers::Read::RdepsEntry.new(manifest: container.manifest))
|
|
95
|
+
registry.register(Dispatch::Contracts::BootStore,
|
|
96
|
+
Handlers::Maintenance::BootStore.new(container: container))
|
|
97
|
+
registry.register(Dispatch::Contracts::DoctorStore,
|
|
98
|
+
Handlers::Maintenance::DoctorStore.new(container: container))
|
|
99
|
+
registry.register(Dispatch::Contracts::PublishedEntries,
|
|
100
|
+
Handlers::Maintenance::PublishedEntries.new(manifest: container.manifest))
|
|
101
|
+
registry.register(Dispatch::Contracts::RuleExplain,
|
|
102
|
+
Handlers::Maintenance::RuleExplain.new(manifest: container.manifest))
|
|
103
|
+
registry.register(Dispatch::Contracts::RuleList,
|
|
104
|
+
Handlers::Maintenance::RuleList.new(manifest: container.manifest))
|
|
105
|
+
registry.register(Dispatch::Contracts::SchemaEnvelope,
|
|
106
|
+
Handlers::Maintenance::SchemaEnvelope.new(manifest: container.manifest, schemas: container.schemas))
|
|
107
|
+
registry.register(Dispatch::Contracts::DrainStore,
|
|
108
|
+
Handlers::Maintenance::DrainStore.new(container: container, job_store: container.job_store))
|
|
109
|
+
registry.register(Dispatch::Contracts::IngestEntry,
|
|
110
|
+
Handlers::Maintenance::IngestEntry.new(container: container))
|
|
111
|
+
registry.register(Dispatch::Contracts::JobsAction,
|
|
112
|
+
Handlers::Maintenance::JobsAction.new(job_store: container.job_store))
|
|
113
|
+
registry.register(Dispatch::Contracts::RuleLint,
|
|
114
|
+
Handlers::Maintenance::RuleLint.new(manifest: container.manifest))
|
|
115
|
+
registry.register(Dispatch::Contracts::DataMv,
|
|
116
|
+
Handlers::Write::DataMv.new(container: container))
|
|
117
|
+
registry.register(Dispatch::Contracts::AuditEntries,
|
|
118
|
+
Handlers::Read::AuditEntries.new(manifest: container.manifest, audit_log: container.audit_log))
|
|
119
|
+
registry.register(Dispatch::Contracts::PulseEntries,
|
|
120
|
+
Handlers::Read::PulseEntries.new(
|
|
121
|
+
manifest: container.manifest,
|
|
122
|
+
audit_log: container.audit_log,
|
|
123
|
+
file_store: container.file_store,
|
|
124
|
+
job_store: container.job_store,
|
|
125
|
+
orchestration: orch,
|
|
126
|
+
))
|
|
127
|
+
registry.register(Dispatch::Contracts::BlameEntry,
|
|
128
|
+
Handlers::Read::BlameEntry.new(manifest: container.manifest, orchestration: orch))
|
|
129
|
+
registry.register(Dispatch::Contracts::KeyMvPrefix,
|
|
130
|
+
Handlers::Write::KeyMvPrefix.new(orchestration: orch))
|
|
131
|
+
registry.register(Dispatch::Contracts::KeyDeletePrefix,
|
|
132
|
+
Handlers::Write::KeyDeletePrefix.new(orchestration: orch))
|
|
133
|
+
|
|
134
|
+
Dispatch::Pipeline.new(
|
|
135
|
+
registry: registry,
|
|
136
|
+
container: container,
|
|
137
|
+
middleware: [
|
|
138
|
+
Dispatch::Middleware::Binder.new,
|
|
139
|
+
Dispatch::Middleware::Auth.new,
|
|
140
|
+
Dispatch::Middleware::AuditIndex.new(
|
|
141
|
+
job_store: container.job_store,
|
|
142
|
+
audit_log: container.audit_log,
|
|
143
|
+
),
|
|
144
|
+
Dispatch::Middleware::Cascade.new,
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
private_class_method :build_pipeline
|
|
149
|
+
|
|
150
|
+
def self.create_writer_factory(container)
|
|
151
|
+
lambda do |call|
|
|
152
|
+
Textus::Store::Entry::Writer.new(
|
|
153
|
+
file_store: container.file_store,
|
|
154
|
+
manifest: container.manifest,
|
|
155
|
+
schemas: container.schemas,
|
|
156
|
+
audit_log: container.audit_log,
|
|
157
|
+
call: call,
|
|
158
|
+
reader: container.reader,
|
|
159
|
+
layout: container.layout,
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.freshness_evaluator(container)
|
|
165
|
+
Store::Freshness::TtlEvaluator.new(
|
|
166
|
+
manifest: container.manifest,
|
|
167
|
+
file_stat: Textus::Port::Storage::FileStat.new,
|
|
168
|
+
clock: Textus::Port::Clock.new,
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Store
|
|
5
|
+
# Per-role cursor cache under <root>/.state/cursors/<role>. A convenience so
|
|
6
|
+
# `textus pulse` (no --since) means "since I last looked". Gitignored;
|
|
7
|
+
# losing it just re-emits recent deltas, never corrupts the store. ADR 0036/0038.
|
|
8
|
+
class Cursor
|
|
9
|
+
def initialize(root:, role:)
|
|
10
|
+
@path = Store::Layout.new(root).cursor_path(role)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read
|
|
14
|
+
Integer(File.read(@path).strip)
|
|
15
|
+
rescue Errno::ENOENT, ArgumentError
|
|
16
|
+
0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def write(seq)
|
|
20
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
21
|
+
File.write(@path, seq.to_s)
|
|
22
|
+
seq
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Store
|
|
3
|
+
module Entry
|
|
4
|
+
# Read-only counterpart to EnvelopeWriter. Resolves a key, reads the
|
|
5
|
+
# bytes, parses them via the format strategy, and hands back an
|
|
6
|
+
# Envelope. Used by Mv (pre-move inspection) and by EnvelopeWriter
|
|
7
|
+
# (existing-meta lookup for the uid/sources preservation step in #put).
|
|
8
|
+
#
|
|
9
|
+
# No audit, no events, no permission checks — those live one layer up.
|
|
10
|
+
class Reader
|
|
11
|
+
def self.from(container:)
|
|
12
|
+
# Prefer a cached reader on the container (injection point) for
|
|
13
|
+
# tests and alternative runtimes. Fall back to constructing one.
|
|
14
|
+
return container.reader if container.respond_to?(:reader) && container.reader
|
|
15
|
+
|
|
16
|
+
new(file_store: container.file_store, manifest: container.manifest,
|
|
17
|
+
layout: container.layout)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(file_store:, manifest:, layout:)
|
|
21
|
+
@file_store = file_store
|
|
22
|
+
@manifest = manifest
|
|
23
|
+
@layout = layout
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def read(key)
|
|
27
|
+
res = @manifest.resolver.resolve(key)
|
|
28
|
+
path = res.path
|
|
29
|
+
return nil unless @file_store.exists?(path)
|
|
30
|
+
|
|
31
|
+
mentry = res.entry
|
|
32
|
+
raw = @file_store.read(path)
|
|
33
|
+
parsed = Format.for(mentry.format).parse(raw, path: path)
|
|
34
|
+
Textus::Value::Envelope.build(
|
|
35
|
+
key: key, mentry: mentry, path: path,
|
|
36
|
+
meta: parsed["_meta"], body: parsed["body"],
|
|
37
|
+
etag: Value::Etag.for_bytes(raw), content: parsed["content"]
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def exists?(key)
|
|
42
|
+
@file_store.exists?(@manifest.resolver.resolve(key).path)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Store
|
|
5
|
+
module Entry
|
|
6
|
+
# Owns the write pipeline (validate, serialize, etag-check, write, audit).
|
|
7
|
+
# Talks to ports (FileStore, Schemas, AuditLog, Manifest) and an
|
|
8
|
+
# Reader for the existing-uid lookup.
|
|
9
|
+
#
|
|
10
|
+
# Invariant: every public method's final action is @audit_log.append(...).
|
|
11
|
+
#
|
|
12
|
+
# No permission check, no event firing — those belong to the caller
|
|
13
|
+
# (Write::Put / ::Delete / ::Mv).
|
|
14
|
+
class Writer
|
|
15
|
+
def self.from(container:, call:)
|
|
16
|
+
# If the container exposes a writer factory (in tests we set this),
|
|
17
|
+
# use it. Otherwise, construct a fresh Writer.
|
|
18
|
+
return container.writer.call(call) if container.respond_to?(:writer) && container.writer
|
|
19
|
+
|
|
20
|
+
new(
|
|
21
|
+
file_store: container.file_store, manifest: container.manifest,
|
|
22
|
+
schemas: container.schemas, audit_log: container.audit_log,
|
|
23
|
+
call: call, reader: Reader.from(container: container),
|
|
24
|
+
layout: container.layout
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(file_store:, manifest:, schemas:, audit_log:, call:, reader:, layout:)
|
|
29
|
+
@file_store = file_store
|
|
30
|
+
@manifest = manifest
|
|
31
|
+
@schemas = schemas
|
|
32
|
+
@audit_log = audit_log
|
|
33
|
+
@call = call
|
|
34
|
+
@reader = reader
|
|
35
|
+
@layout = layout
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def put(key, mentry:, payload:, if_etag: nil)
|
|
39
|
+
path = resolve_path(key)
|
|
40
|
+
meta = payload.meta || {}
|
|
41
|
+
content = payload.content
|
|
42
|
+
|
|
43
|
+
existing_env = read_existing(key)
|
|
44
|
+
existing_meta = existing_env ? existing_env.meta : {}
|
|
45
|
+
meta, content = inject_meta(meta, content, existing_meta, mentry.format)
|
|
46
|
+
|
|
47
|
+
bytes, eff_meta, eff_body, eff_content = serialize_entry(mentry, path, meta, payload, content)
|
|
48
|
+
|
|
49
|
+
enforce_name_match!(path, eff_meta, mentry.format)
|
|
50
|
+
validate_schema(mentry, eff_meta, eff_content)
|
|
51
|
+
validate_raw(eff_meta, eff_content, mentry.lane, mentry.format)
|
|
52
|
+
|
|
53
|
+
etag_before = check_etag!(path, key, if_etag)
|
|
54
|
+
write_bytes(path, bytes)
|
|
55
|
+
|
|
56
|
+
envelope = build_envelope(key, mentry, path, eff_meta, eff_body, eff_content, bytes)
|
|
57
|
+
audit_put(key, etag_before, envelope.etag)
|
|
58
|
+
envelope
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def delete(key, mentry: nil, if_etag: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
62
|
+
# `mentry:` is accepted for symmetry with `put` / `move` and to
|
|
63
|
+
# leave room for future format-specific delete hooks; no field
|
|
64
|
+
# on it is needed today.
|
|
65
|
+
path = @manifest.resolver.resolve(key).path
|
|
66
|
+
raise UnknownKey.new(key, suggestions: @manifest.resolver.suggestions_for(key)) unless @file_store.exists?(path)
|
|
67
|
+
|
|
68
|
+
etag_before = @file_store.etag(path)
|
|
69
|
+
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
|
|
70
|
+
|
|
71
|
+
@file_store.delete(path)
|
|
72
|
+
prune_empty_parents(path)
|
|
73
|
+
@audit_log.append(
|
|
74
|
+
role: @call.role, verb: "key_delete", key: key,
|
|
75
|
+
etag_before: etag_before, etag_after: nil,
|
|
76
|
+
extras: @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def move(from_key:, to_key:, new_mentry:, if_etag: nil)
|
|
81
|
+
from_path = @manifest.resolver.resolve(from_key).path
|
|
82
|
+
to_path = @manifest.resolver.resolve(to_key).path
|
|
83
|
+
raise UnknownKey.new(from_key, suggestions: @manifest.resolver.suggestions_for(from_key)) unless @file_store.exists?(from_path)
|
|
84
|
+
|
|
85
|
+
etag_before = @file_store.etag(from_path)
|
|
86
|
+
raise EtagMismatch.new(from_key, if_etag, etag_before) if if_etag && if_etag != etag_before
|
|
87
|
+
|
|
88
|
+
@file_store.mv(from_path, to_path)
|
|
89
|
+
prune_empty_parents(from_path)
|
|
90
|
+
basename = to_key.split(".").last
|
|
91
|
+
Format.for(new_mentry.format).rewrite_name(to_path, basename)
|
|
92
|
+
etag_after = Value::Etag.for_file(to_path)
|
|
93
|
+
|
|
94
|
+
envelope = @reader.read(to_key)
|
|
95
|
+
|
|
96
|
+
extras = {
|
|
97
|
+
"from_key" => from_key, "to_key" => to_key,
|
|
98
|
+
"from_path" => from_path, "to_path" => to_path,
|
|
99
|
+
"uid" => envelope.uid
|
|
100
|
+
}
|
|
101
|
+
extras["correlation_id"] = @call.correlation_id if @call.correlation_id
|
|
102
|
+
|
|
103
|
+
@audit_log.append(
|
|
104
|
+
role: @call.role, verb: "key_mv", key: to_key,
|
|
105
|
+
etag_before: etag_before, etag_after: etag_after,
|
|
106
|
+
extras: extras
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
envelope
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# After a file leaves a directory (delete or move-source), remove any
|
|
115
|
+
# now-empty parent dirs so bulk move/delete doesn't accrue orphan dirs
|
|
116
|
+
# (F3 of #161). Floored at the entry's *zone directory* — a zone is a
|
|
117
|
+
# declared, first-class container, so its own dir is preserved even when
|
|
118
|
+
# momentarily empty; only the sub-dirs the bulk op carved out are
|
|
119
|
+
# pruned. Stops at the first non-empty ancestor, so a dir holding a
|
|
120
|
+
# `.gitkeep` or sibling entries survives. Best-effort: a lost race or a
|
|
121
|
+
# non-empty dir is silently fine, never fatal to the write.
|
|
122
|
+
def prune_empty_parents(path)
|
|
123
|
+
floor = @layout.lane_floor(path)
|
|
124
|
+
return unless floor
|
|
125
|
+
|
|
126
|
+
dir = File.dirname(path)
|
|
127
|
+
while dir.start_with?("#{floor}/") && @file_store.dir_empty?(dir)
|
|
128
|
+
@file_store.rmdir(dir)
|
|
129
|
+
dir = File.dirname(dir)
|
|
130
|
+
end
|
|
131
|
+
rescue SystemCallError
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def read_existing(key)
|
|
136
|
+
@reader.read(key)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def inject_meta(meta, content, existing_meta, format)
|
|
140
|
+
Envelope::Meta.inject_all(
|
|
141
|
+
meta, content, existing_meta,
|
|
142
|
+
format: format,
|
|
143
|
+
etag_for: method(:resolve_source_etag)
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def resolve_source_etag(key)
|
|
148
|
+
path = @manifest.resolver.resolve(key).path
|
|
149
|
+
return nil unless @file_store.exists?(path)
|
|
150
|
+
|
|
151
|
+
Value::Etag.for_file(path)
|
|
152
|
+
rescue Textus::Error
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def resolve_path(key)
|
|
157
|
+
@manifest.resolver.resolve(key).path
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def serialize_entry(mentry, path, meta, payload, content)
|
|
161
|
+
Textus::Format.for(mentry.format).serialize_for_put(
|
|
162
|
+
meta: meta, body: payload.body, content: content, path: path,
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def enforce_name_match!(path, meta, format)
|
|
167
|
+
Textus::Format.for(format).enforce_name_match!(path, meta)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def validate_schema(mentry, eff_meta, eff_content)
|
|
171
|
+
schema = @schemas.fetch_or_nil(mentry.schema)
|
|
172
|
+
return unless schema
|
|
173
|
+
|
|
174
|
+
Format.for(mentry.format).validate_against(
|
|
175
|
+
schema,
|
|
176
|
+
{ "_meta" => eff_meta, "content" => eff_content },
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def validate_raw(eff_meta, eff_content, lane, format)
|
|
181
|
+
Textus::Format.for(format).validate_raw_entry!(
|
|
182
|
+
{ "_meta" => eff_meta, "content" => eff_content },
|
|
183
|
+
lane,
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def check_etag!(path, key, if_etag)
|
|
188
|
+
etag_before = @file_store.exists?(path) ? @file_store.etag(path) : nil
|
|
189
|
+
raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
|
|
190
|
+
|
|
191
|
+
etag_before
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def write_bytes(path, bytes)
|
|
195
|
+
@file_store.write(path, bytes)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def build_envelope(key, mentry, path, eff_meta, eff_body, eff_content, bytes = nil)
|
|
199
|
+
raw = bytes || @file_store.read(path)
|
|
200
|
+
Textus::Value::Envelope.build(
|
|
201
|
+
key: key, mentry: mentry, path: path,
|
|
202
|
+
meta: eff_meta, body: eff_body,
|
|
203
|
+
etag: Value::Etag.for_bytes(raw),
|
|
204
|
+
content: eff_content
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def audit_put(key, etag_before, etag_after)
|
|
209
|
+
extras = @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
|
|
210
|
+
@audit_log.append(
|
|
211
|
+
role: @call.role, verb: "put", key: key,
|
|
212
|
+
etag_before: etag_before, etag_after: etag_after,
|
|
213
|
+
extras: extras
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Store
|
|
5
|
+
module Envelope
|
|
6
|
+
module Meta
|
|
7
|
+
NO_META_FORMATS = %w[text].freeze
|
|
8
|
+
|
|
9
|
+
FIELDS = {
|
|
10
|
+
"uid" => {
|
|
11
|
+
inject: lambda { |meta, content, existing_meta, **_opts|
|
|
12
|
+
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
13
|
+
existing = existing_meta.is_a?(Hash) ? existing_meta["uid"] : nil
|
|
14
|
+
m["uid"] = existing || Textus::Value::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
15
|
+
[m, content]
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
"sources" => {
|
|
19
|
+
inject: lambda { |meta, content, existing_meta, etag_for: nil|
|
|
20
|
+
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
21
|
+
existing = existing_meta.is_a?(Hash) ? existing_meta["sources"] : nil
|
|
22
|
+
|
|
23
|
+
if m.key?("sources")
|
|
24
|
+
raise Textus::BadContent.new(nil, "_meta.sources must be an array") unless m["sources"].is_a?(Array)
|
|
25
|
+
|
|
26
|
+
m["sources"] = m["sources"].map { |s| Meta.normalize_source!(s, etag_for) }
|
|
27
|
+
elsif existing.is_a?(Array) && !existing.empty?
|
|
28
|
+
m["sources"] = existing
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
[m, content]
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def self.inject_all(meta, content, existing_meta = {}, format: nil, etag_for: nil)
|
|
37
|
+
return [meta, content] if NO_META_FORMATS.include?(format)
|
|
38
|
+
|
|
39
|
+
FIELDS.each_value do |field|
|
|
40
|
+
meta, content = field[:inject].call(meta, content, existing_meta, etag_for: etag_for)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[meta, content]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.normalize_source!(src, etag_for)
|
|
47
|
+
key = case src
|
|
48
|
+
when String then src
|
|
49
|
+
when Hash then src["key"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
raise Textus::BadContent.new(nil, "each source must be a string key or { key: } object") unless key.is_a?(String)
|
|
53
|
+
raise Textus::BadContent.new(nil, "each source key must be a non-empty string") if key.empty?
|
|
54
|
+
|
|
55
|
+
etag = etag_for&.call(key)
|
|
56
|
+
etag ? { "key" => key, "etag" => etag } : { "key" => key }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
class Store
|
|
7
|
+
module Freshness
|
|
8
|
+
class DriftDetector
|
|
9
|
+
def initialize(manifest:, file_stat:, clock:)
|
|
10
|
+
@manifest = manifest
|
|
11
|
+
@file_stat = file_stat
|
|
12
|
+
@clock = clock
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def drift_rows(mentry)
|
|
16
|
+
return [] unless mentry.external?
|
|
17
|
+
|
|
18
|
+
path = Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
19
|
+
reason = drift_reason(mentry, path)
|
|
20
|
+
reason ? [drift_row(mentry, path, reason)] : []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def drift_reason(mentry, path)
|
|
26
|
+
return "derived entry has never been generated" unless @file_stat.exists?(path)
|
|
27
|
+
|
|
28
|
+
generated_at = generated_at_of(mentry, path)
|
|
29
|
+
return "missing generated.at frontmatter" unless generated_at
|
|
30
|
+
|
|
31
|
+
gen_time = parse_time(generated_at)
|
|
32
|
+
return "unparseable generated.at: #{generated_at.inspect}" unless gen_time
|
|
33
|
+
|
|
34
|
+
offender = newest_source_after(mentry.source, gen_time)
|
|
35
|
+
"source '#{offender}' modified after generated.at" if offender
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generated_at_of(mentry, path)
|
|
39
|
+
Textus::Format.for(mentry.format).parse(@file_stat.read(path), path: path)["_meta"]
|
|
40
|
+
.dig("generated", "at")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_time(str)
|
|
44
|
+
Time.parse(str.to_s)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def newest_source_after(external_src, gen_time)
|
|
50
|
+
Array(external_src.sources).each do |src|
|
|
51
|
+
offender = check_source(src, gen_time)
|
|
52
|
+
return offender if offender
|
|
53
|
+
end
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def check_source(src, gen_time)
|
|
58
|
+
if src.match?(/\A[a-z0-9.][a-z0-9._-]*\z/) && !src.include?("/")
|
|
59
|
+
@manifest.resolver.enumerate(prefix: src).each do |row|
|
|
60
|
+
return src if @file_stat.mtime(row[:path]) > gen_time
|
|
61
|
+
end
|
|
62
|
+
nil
|
|
63
|
+
else
|
|
64
|
+
check_filesystem_source(src, gen_time)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check_filesystem_source(src, gen_time)
|
|
69
|
+
abs = absolutize_source(src)
|
|
70
|
+
if @file_stat.directory?(abs)
|
|
71
|
+
dir_has_newer_file?(abs, gen_time) ? src : nil
|
|
72
|
+
elsif @file_stat.exists?(abs) && @file_stat.mtime(abs) > gen_time
|
|
73
|
+
src
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def absolutize_source(src)
|
|
78
|
+
File.absolute_path?(src) ? src : File.join(File.dirname(@manifest.data.root), src)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dir_has_newer_file?(abs, gen_time)
|
|
82
|
+
@file_stat.glob(File.join(abs, "**", "*")).any? do |fpath|
|
|
83
|
+
!@file_stat.directory?(fpath) && @file_stat.exists?(fpath) && @file_stat.mtime(fpath) > gen_time
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def drift_row(mentry, path, reason)
|
|
88
|
+
{ "key" => mentry.key, "path" => path, "generator" => mentry.source.command, "reason" => reason }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Store
|
|
5
|
+
module Freshness
|
|
6
|
+
# Thin facade delegating to the focused TtlEvaluator and DriftDetector.
|
|
7
|
+
# Prefer using TtlEvaluator or DriftDetector directly.
|
|
8
|
+
class Evaluator
|
|
9
|
+
def initialize(manifest:, file_stat:, clock:)
|
|
10
|
+
@ttl = TtlEvaluator.new(manifest: manifest, file_stat: file_stat, clock: clock)
|
|
11
|
+
@drift = DriftDetector.new(manifest: manifest, file_stat: file_stat, clock: clock)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def verdict(mentry) = @ttl.verdict(mentry)
|
|
15
|
+
def stale_keys(**) = @ttl.stale_keys(**)
|
|
16
|
+
def drift_rows(mentry) = @drift.drift_rows(mentry)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|