textus 0.55.2 → 0.56.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +175 -2768
- data/README.md +8 -9
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +0 -39
- data/lib/textus/human.rb +7 -0
- data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
- data/lib/textus/infra/clock.rb +7 -0
- data/lib/textus/infra/database.rb +127 -0
- data/lib/textus/infra/file_system.rb +54 -0
- data/lib/textus/infra/locks.rb +50 -0
- data/lib/textus/infra/port/clock.rb +18 -0
- data/lib/textus/infra/port/publisher.rb +71 -0
- data/lib/textus/infra/port/sentinel.rb +25 -0
- data/lib/textus/infra/port/sentinel_store.rb +93 -0
- data/lib/textus/infra/store/audit_log.rb +42 -0
- data/lib/textus/infra/store/base.rb +25 -0
- data/lib/textus/infra/store/cursor_store.rb +27 -0
- data/lib/textus/infra/store/entry_index.rb +31 -0
- data/lib/textus/infra/store/envelope/meta.rb +63 -0
- data/lib/textus/infra/store/file_store.rb +23 -0
- data/lib/textus/infra/store/index/builder.rb +28 -0
- data/lib/textus/infra/store/index/lookup.rb +23 -0
- data/lib/textus/infra/store/link_store.rb +43 -0
- data/lib/textus/infra/store/workflow_queue.rb +54 -0
- data/lib/textus/infra/store.rb +29 -0
- data/lib/textus/ingest.rb +8 -0
- data/lib/textus/lanes/artifact/cache.rb +35 -0
- data/lib/textus/lanes/artifact/handlers.rb +29 -0
- data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
- data/lib/textus/lanes/artifact/verbs.rb +18 -0
- data/lib/textus/lanes/ingest/dedup.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
- data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
- data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types.rb +25 -0
- data/lib/textus/lanes/ingest/handlers.rb +54 -0
- data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
- data/lib/textus/lanes/ingest/key_builder.rb +33 -0
- data/lib/textus/lanes/ingest/resolver.rb +21 -0
- data/lib/textus/lanes/knowledge/freeform.rb +15 -0
- data/lib/textus/lanes/knowledge/handlers.rb +53 -0
- data/lib/textus/lanes/knowledge/runner.rb +45 -0
- data/lib/textus/lanes/knowledge/verbs.rb +16 -0
- data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
- data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
- data/lib/textus/{dispatch → protocol}/binder.rb +1 -7
- data/lib/textus/protocol/boot/catalog.rb +34 -0
- data/lib/textus/protocol/boot/context.rb +124 -0
- data/lib/textus/protocol/boot/protocol.rb +33 -0
- data/lib/textus/protocol/boot.rb +42 -0
- data/lib/textus/protocol/builder/domain.rb +17 -0
- data/lib/textus/protocol/builder/infrastructure.rb +17 -0
- data/lib/textus/protocol/builder/runtime.rb +46 -0
- data/lib/textus/protocol/builder.rb +11 -0
- data/lib/textus/protocol/container.rb +22 -0
- data/lib/textus/protocol/diff.rb +170 -0
- data/lib/textus/protocol/entry_constraint.rb +54 -0
- data/lib/textus/protocol/envelope.rb +70 -0
- data/lib/textus/protocol/format/base.rb +80 -0
- data/lib/textus/protocol/format/bash.rb +10 -0
- data/lib/textus/protocol/format/javascript.rb +10 -0
- data/lib/textus/protocol/format/json.rb +82 -0
- data/lib/textus/protocol/format/markdown.rb +53 -0
- data/lib/textus/protocol/format/python.rb +10 -0
- data/lib/textus/protocol/format/ruby.rb +10 -0
- data/lib/textus/protocol/format/script.rb +55 -0
- data/lib/textus/protocol/format/text.rb +48 -0
- data/lib/textus/protocol/format/yaml.rb +105 -0
- data/lib/textus/protocol/format.rb +73 -0
- data/lib/textus/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
- data/lib/textus/protocol/gate.rb +61 -0
- data/lib/textus/protocol/handlers/command_context.rb +19 -0
- data/lib/textus/protocol/handlers/concern.rb +15 -0
- data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
- data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
- data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
- data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
- data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
- data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
- data/lib/textus/protocol/handlers/query_context.rb +41 -0
- data/lib/textus/protocol/handlers/read.rb +67 -0
- data/lib/textus/protocol/handlers/system.rb +93 -0
- data/lib/textus/protocol/handlers/write.rb +57 -0
- data/lib/textus/protocol/init.rb +143 -0
- data/lib/textus/protocol/key/grammar.rb +35 -0
- data/lib/textus/protocol/key/matching.rb +23 -0
- data/lib/textus/protocol/key/path.rb +23 -0
- data/lib/textus/protocol/key.rb +25 -0
- data/lib/textus/protocol/lane.rb +12 -0
- data/lib/textus/{store → protocol}/layout.rb +16 -22
- data/lib/textus/protocol/links/resolver.rb +33 -0
- data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
- data/lib/textus/protocol/links.rb +10 -0
- data/lib/textus/protocol/manifest/data.rb +90 -0
- data/lib/textus/protocol/manifest/entry/base.rb +95 -0
- data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
- data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
- data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
- data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
- data/lib/textus/protocol/manifest/entry.rb +10 -0
- data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
- data/lib/textus/protocol/manifest/policy.rb +69 -0
- data/lib/textus/protocol/manifest/resolver.rb +156 -0
- data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
- data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
- data/lib/textus/protocol/manifest/schema.rb +40 -0
- data/lib/textus/protocol/manifest.rb +62 -0
- data/lib/textus/protocol/produce/render/context.rb +43 -0
- data/lib/textus/protocol/produce/render.rb +44 -0
- data/lib/textus/protocol/schema/registry.rb +44 -0
- data/lib/textus/protocol/schema/tools.rb +104 -0
- data/lib/textus/protocol/schema.rb +117 -0
- data/lib/textus/protocol/session/bridge.rb +11 -0
- data/lib/textus/protocol/session/context.rb +5 -0
- data/lib/textus/protocol/session.rb +87 -0
- data/lib/textus/protocol/store_engine/delete.rb +37 -0
- data/lib/textus/protocol/store_engine/move.rb +109 -0
- data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
- data/lib/textus/protocol/store_engine/put.rb +104 -0
- data/lib/textus/protocol/store_engine.rb +86 -0
- data/lib/textus/protocol/v4.rb +25 -0
- data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
- data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
- data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
- data/lib/textus/protocol/verb_registry.rb +42 -0
- data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
- data/lib/textus/surface/cli/runner.rb +22 -21
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +2 -7
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +7 -7
- data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
- data/lib/textus/surface/cli.rb +25 -6
- data/lib/textus/surface/dispatch.rb +17 -0
- data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
- data/lib/textus/surface/mcp/catalog.rb +12 -12
- data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
- data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
- data/lib/textus/surface/mcp/server.rb +35 -24
- data/lib/textus/surface/watcher.rb +18 -12
- data/lib/textus/surface/web.rb +50 -0
- data/lib/textus/value/call.rb +1 -1
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/consumer.rb +39 -0
- data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
- data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
- data/lib/textus/workflow/dsl/step_builder.rb +56 -0
- data/lib/textus/workflow/dsl.rb +111 -21
- data/lib/textus/workflow/event_emitter.rb +21 -0
- data/lib/textus/workflow/publisher.rb +59 -0
- data/lib/textus/workflow/registry.rb +31 -8
- data/lib/textus/workflow/runner.rb +25 -53
- data/lib/textus/workflow/scheduler.rb +45 -0
- data/lib/textus/workflow/step_executor.rb +78 -0
- data/lib/textus/workflow/step_helpers.rb +102 -0
- data/lib/textus/workflow.rb +2 -5
- data/lib/textus.rb +16 -16
- metadata +169 -185
- data/SPEC.md +0 -909
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -141
- data/lib/textus/boot.rb +0 -181
- data/lib/textus/dispatch/contracts.rb +0 -63
- data/lib/textus/dispatch/handler_registry.rb +0 -21
- data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
- data/lib/textus/dispatch/middleware/auth.rb +0 -40
- data/lib/textus/dispatch/middleware/base.rb +0 -26
- data/lib/textus/dispatch/middleware/binder.rb +0 -20
- data/lib/textus/dispatch/middleware/cascade.rb +0 -53
- data/lib/textus/dispatch/pipeline.rb +0 -35
- data/lib/textus/doctor/check/audit_log.rb +0 -34
- data/lib/textus/doctor/check/generator_drift.rb +0 -29
- data/lib/textus/doctor/check/illegal_keys.rb +0 -60
- data/lib/textus/doctor/check/manifest_files.rb +0 -25
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
- data/lib/textus/doctor/check/proposal_targets.rb +0 -45
- data/lib/textus/doctor/check/protocol_version.rb +0 -47
- data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
- data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
- data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
- data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
- data/lib/textus/doctor/check/schema_violations.rb +0 -28
- data/lib/textus/doctor/check/schemas.rb +0 -26
- data/lib/textus/doctor/check/scratchpad_sources.rb +0 -55
- data/lib/textus/doctor/check/sentinels.rb +0 -60
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
- data/lib/textus/doctor/check/templates.rb +0 -29
- data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
- data/lib/textus/doctor/check.rb +0 -43
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -77
- data/lib/textus/format/json.rb +0 -81
- data/lib/textus/format/markdown.rb +0 -59
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -104
- data/lib/textus/format.rb +0 -61
- data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
- data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
- data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
- data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
- data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
- data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
- data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
- data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
- data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
- data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
- data/lib/textus/handlers/read/audit_entries.rb +0 -48
- data/lib/textus/handlers/read/blame_entry.rb +0 -71
- data/lib/textus/handlers/read/deps_entry.rb +0 -17
- data/lib/textus/handlers/read/get_entry.rb +0 -68
- data/lib/textus/handlers/read/list_keys.rb +0 -36
- data/lib/textus/handlers/read/pulse_entries.rb +0 -66
- data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
- data/lib/textus/handlers/read/uid_entry.rb +0 -18
- data/lib/textus/handlers/read/where_entry.rb +0 -18
- data/lib/textus/handlers/write/accept_proposal.rb +0 -39
- data/lib/textus/handlers/write/data_mv.rb +0 -55
- data/lib/textus/handlers/write/delete_key.rb +0 -17
- data/lib/textus/handlers/write/enqueue_job.rb +0 -27
- data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
- data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
- data/lib/textus/handlers/write/move_key.rb +0 -80
- data/lib/textus/handlers/write/propose_entry.rb +0 -29
- data/lib/textus/handlers/write/put_entry.rb +0 -29
- data/lib/textus/handlers/write/reject_proposal.rb +0 -29
- data/lib/textus/init.rb +0 -157
- data/lib/textus/jobs.rb +0 -9
- data/lib/textus/key/distance.rb +0 -55
- data/lib/textus/key/grammar.rb +0 -33
- data/lib/textus/key/matching.rb +0 -24
- data/lib/textus/key/path.rb +0 -28
- data/lib/textus/manifest/capabilities.rb +0 -29
- data/lib/textus/manifest/data.rb +0 -102
- data/lib/textus/manifest/entry/base.rb +0 -105
- data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
- data/lib/textus/manifest/entry/leaf.rb +0 -17
- data/lib/textus/manifest/entry/nested.rb +0 -37
- data/lib/textus/manifest/entry/parser.rb +0 -99
- data/lib/textus/manifest/entry/produced.rb +0 -36
- data/lib/textus/manifest/entry/publish/mode.rb +0 -45
- data/lib/textus/manifest/entry/publish/none.rb +0 -14
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
- data/lib/textus/manifest/entry/publish/template.rb +0 -16
- data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
- data/lib/textus/manifest/entry/publish/tree.rb +0 -50
- data/lib/textus/manifest/entry/publish.rb +0 -45
- data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
- data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
- data/lib/textus/manifest/entry/validators/publish.rb +0 -30
- data/lib/textus/manifest/entry/validators.rb +0 -18
- data/lib/textus/manifest/entry.rb +0 -8
- data/lib/textus/manifest/policy/matcher.rb +0 -51
- data/lib/textus/manifest/policy/predicates/author_held.rb +0 -22
- data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
- data/lib/textus/manifest/policy/predicates.rb +0 -54
- data/lib/textus/manifest/policy/publish_target.rb +0 -34
- data/lib/textus/manifest/policy/react.rb +0 -30
- data/lib/textus/manifest/policy/retention.rb +0 -29
- data/lib/textus/manifest/policy/source.rb +0 -30
- data/lib/textus/manifest/policy.rb +0 -95
- data/lib/textus/manifest/resolver.rb +0 -118
- data/lib/textus/manifest/rules.rb +0 -86
- data/lib/textus/manifest/schema/contract.rb +0 -61
- data/lib/textus/manifest/schema/keys.rb +0 -90
- data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
- data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
- data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
- data/lib/textus/manifest/schema/semantics.rb +0 -27
- data/lib/textus/manifest/schema/validator.rb +0 -48
- data/lib/textus/manifest/schema/vocabulary.rb +0 -25
- data/lib/textus/manifest/schema.rb +0 -30
- data/lib/textus/manifest.rb +0 -65
- data/lib/textus/orchestration.rb +0 -55
- data/lib/textus/port/audit_log.rb +0 -268
- data/lib/textus/port/build_lock.rb +0 -64
- data/lib/textus/port/clock.rb +0 -10
- data/lib/textus/port/publisher.rb +0 -60
- data/lib/textus/port/sentinel.rb +0 -25
- data/lib/textus/port/sentinel_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -49
- data/lib/textus/port/storage/interface.rb +0 -17
- data/lib/textus/port/store.rb +0 -149
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -40
- data/lib/textus/produce/publisher.rb +0 -21
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/registry.rb +0 -42
- data/lib/textus/schema/tools.rb +0 -108
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/store/container.rb +0 -173
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/entry/reader.rb +0 -47
- data/lib/textus/store/entry/writer.rb +0 -219
- data/lib/textus/store/envelope/meta.rb +0 -61
- data/lib/textus/store/freshness/drift_detector.rb +0 -93
- data/lib/textus/store/freshness/evaluator.rb +0 -20
- data/lib/textus/store/freshness/verdict.rb +0 -24
- data/lib/textus/store/freshness.rb +0 -8
- data/lib/textus/store/index/builder.rb +0 -76
- data/lib/textus/store/index/lookup.rb +0 -60
- data/lib/textus/store/jobs/base.rb +0 -13
- data/lib/textus/store/jobs/index.rb +0 -15
- data/lib/textus/store/jobs/materialize.rb +0 -15
- data/lib/textus/store/jobs/plan.rb +0 -11
- data/lib/textus/store/jobs/planner.rb +0 -124
- data/lib/textus/store/jobs/queue.rb +0 -162
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention/base.rb +0 -52
- data/lib/textus/store/jobs/retention/sweep.rb +0 -55
- data/lib/textus/store/jobs/retention.rb +0 -8
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store.rb +0 -122
- data/lib/textus/surface/cli/verb/doctor.rb +0 -20
- data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
- data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
- data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
- data/lib/textus/surface/mcp/projector.rb +0 -20
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -90
- data/lib/textus/value/result.rb +0 -46
- data/lib/textus/verb_registry.rb +0 -417
- data/lib/textus/workflow/collector.rb +0 -27
- data/lib/textus/workflow/context.rb +0 -5
- data/lib/textus/workflow/loader.rb +0 -17
- data/lib/textus/workflow/pattern.rb +0 -18
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Schema
|
|
6
|
+
attr_reader :name, :required, :optional, :fields, :raw
|
|
7
|
+
|
|
8
|
+
def self.load(path)
|
|
9
|
+
raw = YAML.safe_load_file(path, permitted_classes: [Symbol, Date, Time], aliases: false)
|
|
10
|
+
new(raw)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(raw)
|
|
14
|
+
@raw = raw || {}
|
|
15
|
+
@name = @raw["name"]
|
|
16
|
+
@required = Array(@raw["required"])
|
|
17
|
+
@optional = Array(@raw["optional"])
|
|
18
|
+
@fields = @raw["fields"] || {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
@raw
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def maintained_by(field)
|
|
26
|
+
meta = @fields[field] or return nil
|
|
27
|
+
meta["maintained_by"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def unowned_fields
|
|
31
|
+
@fields.each_with_object([]) do |(name, spec), acc|
|
|
32
|
+
next unless spec.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
acc << name if spec["maintained_by"].nil?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def evolution
|
|
39
|
+
raw = @raw["evolution"] || {}
|
|
40
|
+
raw.each_with_object({}) do |(k, v), h|
|
|
41
|
+
h[k] = v.is_a?(Date) || v.is_a?(Time) ? v.to_s : v
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate!(frontmatter)
|
|
46
|
+
missing = @required - frontmatter.keys
|
|
47
|
+
raise Textus::SchemaViolation.new("missing" => missing) unless missing.empty?
|
|
48
|
+
|
|
49
|
+
known = (@required + @optional).uniq
|
|
50
|
+
frontmatter.each do |k, v|
|
|
51
|
+
next unless @fields.key?(k)
|
|
52
|
+
|
|
53
|
+
check_type!(k, v, @fields[k])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
warnings = frontmatter.keys - known
|
|
57
|
+
warnings.map { |w| "unknown field: #{w}" }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
TYPE_VALIDATORS = {
|
|
61
|
+
"string" => :validate_string,
|
|
62
|
+
"number" => :validate_number,
|
|
63
|
+
"boolean" => :validate_boolean,
|
|
64
|
+
"enum" => :validate_enum,
|
|
65
|
+
"array" => :validate_array,
|
|
66
|
+
"object" => :validate_object,
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def check_type!(field, value, spec)
|
|
72
|
+
type = spec["type"]
|
|
73
|
+
return unless type
|
|
74
|
+
|
|
75
|
+
send(TYPE_VALIDATORS[type], field, value, spec)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_string(field, value, spec)
|
|
79
|
+
bad!(field, "expected string") unless value.is_a?(String)
|
|
80
|
+
return unless (max = spec["max"]) && value.bytesize > max
|
|
81
|
+
|
|
82
|
+
bad!(field, "exceeds max #{max}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_number(field, value, _spec)
|
|
86
|
+
bad!(field, "expected number") unless value.is_a?(Numeric)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_boolean(field, value, _spec)
|
|
90
|
+
bad!(field, "expected boolean") unless [true, false].include?(value)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_enum(field, value, spec)
|
|
94
|
+
values = Array(spec["values"])
|
|
95
|
+
bad!(field, "not in enum #{values.inspect}") unless values.include?(value)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def validate_array(field, value, spec)
|
|
99
|
+
bad!(field, "expected array") unless value.is_a?(Array)
|
|
100
|
+
return unless (items = spec["items"])
|
|
101
|
+
|
|
102
|
+
value.each_with_index { |v, i| check_type!("#{field}[#{i}]", v, items) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate_object(field, value, spec)
|
|
106
|
+
bad!(field, "expected object") unless value.is_a?(Hash)
|
|
107
|
+
return unless (sub = spec["fields"])
|
|
108
|
+
|
|
109
|
+
sub.each { |fk, fspec| check_type!("#{field}.#{fk}", value[fk], fspec) if value.key?(fk) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def bad!(field, reason)
|
|
113
|
+
raise Textus::SchemaViolation.new("field" => field, "reason" => reason)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Textus::Protocol::Session::Bridge = Data.define(:container, :gate) do
|
|
2
|
+
def store = container.store
|
|
3
|
+
def manifest = container.manifest
|
|
4
|
+
def schemas = container.schemas
|
|
5
|
+
def layout = container.layout
|
|
6
|
+
def workflows = container.workflows
|
|
7
|
+
def root = container.root
|
|
8
|
+
def ttl_evaluator = container.ttl_evaluator
|
|
9
|
+
|
|
10
|
+
def dispatch(...) = gate.dispatch(...)
|
|
11
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "forwardable"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Session
|
|
6
|
+
extend Forwardable
|
|
7
|
+
|
|
8
|
+
attr_reader :context, :bridge
|
|
9
|
+
|
|
10
|
+
DOMAIN_VERBS = (VerbRegistry::ENTRY_VERBS + VerbRegistry::OPS_VERBS).to_set.freeze
|
|
11
|
+
|
|
12
|
+
def_delegators :context, :role, :cursor, :correlation_id, :contract_etag, :propose_lane
|
|
13
|
+
def_delegator :context, :dry_run, :dry_run?
|
|
14
|
+
def_delegators :bridge, :root, :container, :store, :manifest, :schemas, :layout, :workflows, :gate, :ttl_evaluator
|
|
15
|
+
|
|
16
|
+
def self.open(root, role: Value::Role::DEFAULT, correlation_id: nil, dry_run: false)
|
|
17
|
+
root = File.expand_path(root)
|
|
18
|
+
container = Protocol::Builder.new.call(root)
|
|
19
|
+
gate = container.gate
|
|
20
|
+
bridge = Session::Bridge.new(container:, gate:)
|
|
21
|
+
context = Session::Context.new(
|
|
22
|
+
role: role.to_s,
|
|
23
|
+
correlation_id: correlation_id || SecureRandom.uuid,
|
|
24
|
+
dry_run: dry_run,
|
|
25
|
+
cursor: container.store.audit_log.latest[:seq],
|
|
26
|
+
contract_etag: Value::Etag.for_contract(bridge.layout),
|
|
27
|
+
propose_lane: container.manifest.policy.propose_lane_for(role.to_s),
|
|
28
|
+
)
|
|
29
|
+
new(bridge:, context:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(bridge:, context:)
|
|
33
|
+
@bridge = bridge
|
|
34
|
+
@context = context
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def method_missing(name, *args, **kwargs)
|
|
38
|
+
if DOMAIN_VERBS.include?(name)
|
|
39
|
+
raise ArgumentError.new("#{name} accepts keyword arguments only") unless args.empty?
|
|
40
|
+
|
|
41
|
+
return bridge.dispatch(role.to_sym, name, kwargs, call: context.call)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return bridge.public_send(name, *args, **kwargs) if bridge.respond_to?(name)
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def respond_to_missing?(name, include_private = false)
|
|
50
|
+
DOMAIN_VERBS.include?(name) || bridge.respond_to?(name) || super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def with_correlation_id(cid)
|
|
54
|
+
self.class.new(bridge:, context: context.with(correlation_id: cid))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def with_role(new_role)
|
|
58
|
+
self.class.new(
|
|
59
|
+
bridge:,
|
|
60
|
+
context: context.with(
|
|
61
|
+
role: new_role.to_s,
|
|
62
|
+
cursor: container.store.audit_log.latest[:seq],
|
|
63
|
+
contract_etag: Value::Etag.for_contract(container.layout),
|
|
64
|
+
propose_lane: manifest.policy.propose_lane_for(new_role.to_s),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def advance_cursor(new_cursor)
|
|
70
|
+
@context = context.with(cursor: new_cursor)
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def check_etag!(observed_etag)
|
|
75
|
+
return if observed_etag == contract_etag
|
|
76
|
+
|
|
77
|
+
raise Textus::ContractDrift.new(
|
|
78
|
+
"contract changed (was #{short_etag(contract_etag)}, now #{short_etag(observed_etag)})",
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def short_etag(etag) = etag.to_s.delete_prefix("sha256:")[0, 8]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class StoreEngine
|
|
4
|
+
module Delete
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def call(key, deps:, if_etag: nil)
|
|
8
|
+
path = Pipeline.resolve_path(key, deps)
|
|
9
|
+
assert_exists!(key, path, deps)
|
|
10
|
+
etag_before = Pipeline.check_etag(key, path, if_etag, deps.file_system)
|
|
11
|
+
deps.file_system.delete(path)
|
|
12
|
+
deps.entry_index.remove(key:)
|
|
13
|
+
Pipeline.prune_parents(path, deps)
|
|
14
|
+
append_audit(key, etag_before, deps)
|
|
15
|
+
Pipeline.emit_event(deps, event_type: "entry.deleted",
|
|
16
|
+
pending_key: key, emit_key: key)
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def assert_exists!(key, path, deps)
|
|
21
|
+
return if deps.file_system.exists?(path)
|
|
22
|
+
|
|
23
|
+
raise Textus::UnknownKey.new(key,
|
|
24
|
+
suggestions: deps.manifest.resolver.suggestions_for(key))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def append_audit(key, etag_before, deps)
|
|
28
|
+
deps.audit_log.append(
|
|
29
|
+
role: deps.call.role, verb: "key_delete", key:,
|
|
30
|
+
etag_before:, etag_after: nil,
|
|
31
|
+
correlation_id: deps.call.correlation_id
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class StoreEngine
|
|
4
|
+
module Move
|
|
5
|
+
MoveResult = Data.define(:from_key, :to_key, :from_path, :to_path, :dry_run, :envelope)
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def call(from_key:, to_key:, deps:, if_etag: nil, dry_run: false)
|
|
10
|
+
validate!(from_key, to_key)
|
|
11
|
+
from_path, to_path, from_entry, to_entry = resolve_paths(from_key, to_key, deps)
|
|
12
|
+
assert_source_exists!(from_key, from_path, deps)
|
|
13
|
+
assert_target_not_exists!(to_path)
|
|
14
|
+
check_cross_zone!(from_entry, to_entry)
|
|
15
|
+
ensure_uid!(from_key, from_entry, from_path, deps)
|
|
16
|
+
etag_before = Pipeline.check_etag(from_key, from_path, if_etag, deps.file_system)
|
|
17
|
+
|
|
18
|
+
if dry_run
|
|
19
|
+
return MoveResult.new(from_key:, to_key:, from_path:, to_path:,
|
|
20
|
+
dry_run: true, envelope: nil)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
deps.file_system.mv(from_path, to_path)
|
|
24
|
+
Pipeline.prune_parents(from_path, deps)
|
|
25
|
+
rewrite_basename!(to_key, to_path, to_entry)
|
|
26
|
+
etag_after = Value::Etag.for_file(to_path)
|
|
27
|
+
envelope = deps.reader.read(key: to_key)
|
|
28
|
+
append_audit(to_key, etag_before, etag_after, deps)
|
|
29
|
+
reindex_entry(from_key, to_key, to_entry, envelope, deps)
|
|
30
|
+
Pipeline.emit_event(deps, event_type: "entry.moved",
|
|
31
|
+
pending_key: from_key, emit_key: to_key,
|
|
32
|
+
emit_lane: to_entry.lane,
|
|
33
|
+
pending_keys: [from_key, to_key])
|
|
34
|
+
MoveResult.new(from_key:, to_key:, from_path:, to_path:,
|
|
35
|
+
dry_run: false, envelope:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate!(from_key, to_key)
|
|
39
|
+
Manifest::Data.validate_key!(from_key)
|
|
40
|
+
Manifest::Data.validate_key!(to_key)
|
|
41
|
+
raise UsageError.new("old and new keys are identical") if from_key == to_key
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_paths(from_key, to_key, deps)
|
|
45
|
+
from = deps.manifest.resolver.resolve(from_key)
|
|
46
|
+
to = deps.manifest.resolver.resolve(to_key)
|
|
47
|
+
[from.path, to.path, from.entry, to.entry]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def assert_source_exists!(key, path, deps)
|
|
51
|
+
return if deps.file_system.exists?(path)
|
|
52
|
+
|
|
53
|
+
raise UnknownKey.new(key, suggestions: deps.manifest.resolver.suggestions_for(key))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def assert_target_not_exists!(path)
|
|
57
|
+
return unless path && File.exist?(path)
|
|
58
|
+
|
|
59
|
+
raise UsageError.new("target already exists")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_cross_zone!(from_entry, to_entry)
|
|
63
|
+
return if from_entry.lane == to_entry.lane
|
|
64
|
+
|
|
65
|
+
raise UsageError.new("cross-zone moves forbidden (#{from_entry.lane} → #{to_entry.lane})")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ensure_uid!(_key, entry, path, deps)
|
|
69
|
+
return if Infra::Store::Envelope::Meta::NO_META_FORMATS.include?(entry.format)
|
|
70
|
+
return unless path && deps.file_system.exists?(path)
|
|
71
|
+
|
|
72
|
+
raw = deps.file_system.read(path)
|
|
73
|
+
parsed = Format.for(entry.format).parse(raw, path:)
|
|
74
|
+
meta = parsed["_meta"] || {}
|
|
75
|
+
return if meta["uid"].is_a?(String) && !meta["uid"].empty?
|
|
76
|
+
|
|
77
|
+
new_meta, = Infra::Store::Envelope::Meta.inject_all(meta, parsed["content"], {},
|
|
78
|
+
format: entry.format)
|
|
79
|
+
bytes, = Format.for(entry.format).serialize_for_put(
|
|
80
|
+
meta: new_meta, body: parsed["body"] || "", content: parsed["content"], path:,
|
|
81
|
+
)
|
|
82
|
+
deps.file_system.write(path, bytes)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def rewrite_basename!(key, path, entry)
|
|
86
|
+
basename = key.split(".").last
|
|
87
|
+
Format.for(entry.format).rewrite_name(path, basename)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def append_audit(key, etag_before, etag_after, deps)
|
|
91
|
+
deps.audit_log.append(
|
|
92
|
+
role: deps.call.role, verb: "key_mv", key:,
|
|
93
|
+
etag_before:, etag_after:,
|
|
94
|
+
correlation_id: deps.call.correlation_id
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def reindex_entry(from_key, to_key, to_entry, envelope, deps)
|
|
99
|
+
deps.entry_index.remove(key: from_key)
|
|
100
|
+
content = envelope.content ? envelope.content.to_json : envelope.body
|
|
101
|
+
deps.entry_index.record(
|
|
102
|
+
key: to_key, lane: to_entry.lane, format: to_entry.format,
|
|
103
|
+
schema: to_entry.schema, content:
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class StoreEngine
|
|
4
|
+
module Pipeline
|
|
5
|
+
WriteDeps = Data.define(:file_system, :manifest, :store, :call, :reader, :layout, :event_emitter, :schemas) do
|
|
6
|
+
def audit_log = store.audit_log
|
|
7
|
+
def entry_index = store.entry_index
|
|
8
|
+
def file_store = store.file_store
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
DeleteDeps = Data.define(:file_system, :manifest, :store, :call, :layout, :event_emitter) do
|
|
12
|
+
def audit_log = store.audit_log
|
|
13
|
+
def entry_index = store.entry_index
|
|
14
|
+
def workflow_queue = store.workflow_queue
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
MoveDeps = Data.define(:file_system, :manifest, :store, :call, :reader, :layout, :event_emitter) do
|
|
18
|
+
def audit_log = store.audit_log
|
|
19
|
+
def entry_index = store.entry_index
|
|
20
|
+
def workflow_queue = store.workflow_queue
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def resolve_path(key, deps)
|
|
26
|
+
deps.manifest.resolver.resolve(key).path
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check_etag(key, path, if_etag, file_system)
|
|
30
|
+
etag_before = file_system.exists?(path) ? file_system.etag(path) : nil
|
|
31
|
+
raise Textus::EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
|
|
32
|
+
|
|
33
|
+
etag_before
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prune_parents(path, deps)
|
|
37
|
+
floor = deps.layout.lane_floor(path)
|
|
38
|
+
return unless floor
|
|
39
|
+
|
|
40
|
+
dir = File.dirname(path)
|
|
41
|
+
while dir.start_with?("#{floor}/") && deps.file_system.dir_empty?(dir)
|
|
42
|
+
deps.file_system.rmdir(dir)
|
|
43
|
+
dir = File.dirname(dir)
|
|
44
|
+
end
|
|
45
|
+
rescue SystemCallError
|
|
46
|
+
# race: dir may have been emptied by another writer
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def emit_event(deps, event_type:, pending_key:, emit_key:, emit_lane: nil, pending_keys: [])
|
|
50
|
+
return unless deps.event_emitter
|
|
51
|
+
return if deps.store.workflow_queue.pending?(event_type:, key: pending_key)
|
|
52
|
+
return if pending_keys.any? { |k| deps.store.workflow_queue.pending?(event_type:, key: k) }
|
|
53
|
+
|
|
54
|
+
deps.event_emitter.emit(event_type:, key: emit_key, lane: emit_lane || "artifacts")
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
warn "textus: emit_workflow_event(#{event_type}, #{emit_key}) failed: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class StoreEngine
|
|
4
|
+
module Put
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def call(key, mentry:, path:, payload:, deps:, if_etag: nil, fmt: nil)
|
|
8
|
+
format = fmt || mentry.format
|
|
9
|
+
|
|
10
|
+
validate_input(key, payload, format, mentry, path)
|
|
11
|
+
validate_schema(payload, mentry, deps)
|
|
12
|
+
|
|
13
|
+
existing_env = read_existing(key, mentry, path, format, deps)
|
|
14
|
+
meta, content = inject_meta(payload, existing_env, format, deps)
|
|
15
|
+
bytes, eff_meta, eff_body, eff_content = serialize(payload, meta, content, path, format)
|
|
16
|
+
|
|
17
|
+
etag_before = Pipeline.check_etag(key, path, if_etag, deps.file_system)
|
|
18
|
+
deps.file_system.write(path, bytes)
|
|
19
|
+
envelope = Envelope.build(
|
|
20
|
+
key:, mentry:, path:,
|
|
21
|
+
meta: eff_meta, body: eff_body,
|
|
22
|
+
etag: Value::Etag.for_bytes(bytes),
|
|
23
|
+
content: eff_content
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
append_audit(key, etag_before, envelope, deps)
|
|
27
|
+
index_entry(key, mentry, format, eff_body, eff_content, deps)
|
|
28
|
+
Pipeline.emit_event(deps, event_type: "entry.written",
|
|
29
|
+
pending_key: key, emit_key: key,
|
|
30
|
+
emit_lane: mentry.lane)
|
|
31
|
+
envelope
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_input(key, payload, format, mentry, path)
|
|
35
|
+
EntryConstraint.validate_naming!(key, mentry)
|
|
36
|
+
fmt = Format.for(format)
|
|
37
|
+
fmt.enforce_name_match!(path, payload.meta)
|
|
38
|
+
fmt.validate_raw_entry!(
|
|
39
|
+
{ "_meta" => payload.meta, "content" => payload.content },
|
|
40
|
+
mentry.lane,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_schema(payload, mentry, deps)
|
|
45
|
+
EntryConstraint.validate_schema!(payload, mentry, schemas: deps.schemas)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read_existing(key, mentry, path, format, deps)
|
|
49
|
+
return nil unless path && deps.file_system.exists?(path)
|
|
50
|
+
|
|
51
|
+
raw = deps.file_system.read(path)
|
|
52
|
+
parsed = Format.for(format).parse(raw, path:)
|
|
53
|
+
Envelope.build(
|
|
54
|
+
key:, mentry:, path:,
|
|
55
|
+
meta: parsed["_meta"] || {},
|
|
56
|
+
body: parsed["body"] || "",
|
|
57
|
+
content: parsed["content"],
|
|
58
|
+
etag: Value::Etag.for_file(path)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inject_meta(payload, existing_env, format, deps)
|
|
63
|
+
existing_meta = existing_env ? existing_env.meta : {}
|
|
64
|
+
Infra::Store::Envelope::Meta.inject_all(
|
|
65
|
+
payload.meta || {}, payload.content, existing_meta,
|
|
66
|
+
format:,
|
|
67
|
+
etag_for: method(:etag_for_key).curry.call(deps)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def etag_for_key(deps, key)
|
|
72
|
+
path = deps.manifest.resolver.resolve(key).path
|
|
73
|
+
return nil unless deps.file_system.exists?(path)
|
|
74
|
+
|
|
75
|
+
Value::Etag.for_file(path)
|
|
76
|
+
rescue Textus::Error
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def serialize(payload, meta, content, path, format)
|
|
81
|
+
Format.for(format).serialize_for_put(
|
|
82
|
+
meta:, body: payload.body, content:, path:,
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def append_audit(key, etag_before, envelope, deps)
|
|
87
|
+
deps.audit_log.append(
|
|
88
|
+
role: deps.call.role, verb: "put", key:,
|
|
89
|
+
etag_before:, etag_after: envelope.etag,
|
|
90
|
+
correlation_id: deps.call.correlation_id
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def index_entry(key, mentry, format, eff_body, eff_content, deps)
|
|
95
|
+
content = eff_content ? eff_content.to_json : eff_body
|
|
96
|
+
deps.entry_index.record(
|
|
97
|
+
key:, lane: mentry.lane, format:,
|
|
98
|
+
schema: mentry.schema, content:
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class StoreEngine
|
|
4
|
+
def initialize(file_system:, manifest:, layout:, store:, event_emitter: nil, schemas: nil)
|
|
5
|
+
@file_system = file_system
|
|
6
|
+
@manifest = manifest
|
|
7
|
+
@layout = layout
|
|
8
|
+
@store = store
|
|
9
|
+
@event_emitter = event_emitter
|
|
10
|
+
@schemas = schemas
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(key:)
|
|
14
|
+
result = resolve(key:)
|
|
15
|
+
path = result.path
|
|
16
|
+
return nil unless path && @file_system.exists?(path)
|
|
17
|
+
|
|
18
|
+
raw = @file_system.read(path)
|
|
19
|
+
parsed = Format.for(result.entry.format).parse(raw, path:)
|
|
20
|
+
Envelope.build(key:, mentry: result.entry, path:,
|
|
21
|
+
meta: parsed["_meta"] || {},
|
|
22
|
+
body: parsed["body"] || "",
|
|
23
|
+
content: parsed["content"],
|
|
24
|
+
etag: Value::Etag.for_file(path))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def exists?(key:)
|
|
28
|
+
path = resolve(key:).path
|
|
29
|
+
path && @file_system.exists?(path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def list(prefix: nil, lane: nil, q: nil, schema: nil)
|
|
33
|
+
if @store.entry_index && (q || schema)
|
|
34
|
+
rows = @store.entry_index.search(query: q, schema:, lane:, prefix:)
|
|
35
|
+
(rows || []).map { |r| { "key" => r["key"], "lane" => r["lane"] } }
|
|
36
|
+
else
|
|
37
|
+
rows = @manifest.resolver.enumerate(prefix:)
|
|
38
|
+
rows = rows.select { |r| r[:manifest_entry].lane == lane } if lane
|
|
39
|
+
rows.map { |r| { "key" => r[:key], "lane" => r[:manifest_entry].lane } }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolve(key:) = @manifest.resolver.resolve(key)
|
|
44
|
+
def mentry_for(key:)= resolve(key:).entry
|
|
45
|
+
|
|
46
|
+
def put(key:, meta:, body:, content:, call:, if_etag: nil, format: nil)
|
|
47
|
+
result = resolve(key:)
|
|
48
|
+
payload = Value::Payload.new(meta: meta || {}, body:, content:)
|
|
49
|
+
deps = build_deps(reader: nil, call:)
|
|
50
|
+
|
|
51
|
+
if format
|
|
52
|
+
fmt_mod = Textus::Protocol::Format.for(format)
|
|
53
|
+
path = result.path.sub(/\.[^.]+$/, "") + fmt_mod.nested_ext(result.remaining.last)
|
|
54
|
+
Put.call(key, mentry: result.entry, path:, payload:, if_etag:, deps:, fmt: format)
|
|
55
|
+
else
|
|
56
|
+
Put.call(key, mentry: result.entry, path: result.path, payload:, if_etag:, deps:)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def delete(key:, call:, if_etag: nil)
|
|
61
|
+
deps = Pipeline::DeleteDeps.new(
|
|
62
|
+
file_system: @file_system, manifest: @manifest, store: @store,
|
|
63
|
+
layout: @layout, event_emitter: @event_emitter, call:
|
|
64
|
+
)
|
|
65
|
+
Delete.call(key, if_etag:, deps:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def move(from_key:, to_key:, call:, if_etag: nil, dry_run: false)
|
|
69
|
+
deps = Pipeline::MoveDeps.new(
|
|
70
|
+
file_system: @file_system, manifest: @manifest, store: @store,
|
|
71
|
+
layout: @layout, event_emitter: @event_emitter, reader: self, call:
|
|
72
|
+
)
|
|
73
|
+
Move.call(from_key:, to_key:, if_etag:, dry_run:, deps:)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def build_deps(reader:, call:)
|
|
79
|
+
Pipeline::WriteDeps.new(
|
|
80
|
+
file_system: @file_system, manifest: @manifest, store: @store,
|
|
81
|
+
layout: @layout, event_emitter: @event_emitter, schemas: @schemas, reader:, call:
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module V4
|
|
4
|
+
LANES = {
|
|
5
|
+
"knowledge" => { writers: %w[human],
|
|
6
|
+
desc: "the maintained source of truth about the repo",
|
|
7
|
+
retention_ttl: nil },
|
|
8
|
+
"scratchpad" => { writers: %w[human agent],
|
|
9
|
+
desc: "the agent's own durable scratchpad (working memory)",
|
|
10
|
+
retention_ttl: nil },
|
|
11
|
+
"artifacts" => { writers: %w[automation],
|
|
12
|
+
desc: "machine-maintained computed outputs",
|
|
13
|
+
retention_ttl: 3600 * 24 * 30 },
|
|
14
|
+
"raw" => { writers: %w[human agent automation],
|
|
15
|
+
desc: "ingested external source material (write-once)",
|
|
16
|
+
retention_ttl: nil },
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
NAMING_PATTERNS = {
|
|
20
|
+
"sequential" => /\A\d{4}-[a-z0-9-]+\.\w+\z/,
|
|
21
|
+
"dated" => /\A\d{4}-\d{2}-\d{2}-[a-z0-9-]+\.\w+\z/,
|
|
22
|
+
}.freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module VerbRegistry
|
|
4
|
+
ArgSpec = Data.define(
|
|
5
|
+
:name, :type, :required, :positional,
|
|
6
|
+
:description, :wire_name, :default, :source, :coerce, :cli_default
|
|
7
|
+
) do
|
|
8
|
+
def wire = wire_name || name
|
|
9
|
+
|
|
10
|
+
def self.arg(name:, type: String, required: false, positional: false,
|
|
11
|
+
description: nil, wire_name: nil,
|
|
12
|
+
default: nil, source: nil, coerce: nil, cli_default: nil)
|
|
13
|
+
new(name:, type:, required:, positional:,
|
|
14
|
+
description:, wire_name:, default:, source:, coerce:, cli_default:)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|