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,156 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
class Resolver
|
|
5
|
+
Resolution = ::Data.define(:entry, :path, :remaining)
|
|
6
|
+
|
|
7
|
+
MAX_DISTANCE_LEN = 200
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@data = data
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(key)
|
|
14
|
+
@data.validate_key!(key)
|
|
15
|
+
dr = Protocol::Key.resolve(key, @data.entries)
|
|
16
|
+
raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless dr
|
|
17
|
+
raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless dr.entry
|
|
18
|
+
|
|
19
|
+
Resolution.new(entry: dr.entry, path: build_path(dr.entry, dr.remaining), remaining: dr.remaining)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def suggestions_for(key)
|
|
23
|
+
candidates = enumerate.map { |r| r[:key] }
|
|
24
|
+
candidates.concat(@data.entries.reject { |e| nested_entry?(e) }.map(&:key))
|
|
25
|
+
candidates.uniq!
|
|
26
|
+
suggest(key, candidates, limit: 5)
|
|
27
|
+
rescue StandardError
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def enumerate(prefix: nil, include_keyless: false)
|
|
32
|
+
out = @data.entries.flat_map do |entry|
|
|
33
|
+
nested_entry?(entry) ? enumerate_nested(entry, include_keyless: include_keyless) : enumerate_leaf(entry)
|
|
34
|
+
end
|
|
35
|
+
out.select! { |row| row[:key] == prefix || row[:key].start_with?("#{prefix}.") } if prefix
|
|
36
|
+
out.sort_by { |row| row[:key] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def suggest(requested, candidates, limit: 5)
|
|
42
|
+
return [] if requested.nil? || requested.empty?
|
|
43
|
+
|
|
44
|
+
scored = candidates.first(200).map do |k|
|
|
45
|
+
prefix = self.class.shared_prefix_segments(requested, k)
|
|
46
|
+
dist = self.class.levenshtein(requested, k) || Float::INFINITY
|
|
47
|
+
[k, prefix, dist]
|
|
48
|
+
end
|
|
49
|
+
scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def nested_entry?(entry)
|
|
53
|
+
entry.nested?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_path(entry, remaining)
|
|
57
|
+
if remaining.empty?
|
|
58
|
+
resolve_leaf_path(entry)
|
|
59
|
+
else
|
|
60
|
+
fmt = Textus::Protocol::Format.for(entry.format)
|
|
61
|
+
base = Textus::Protocol::Key::Path.normalize_relative_path(entry.path)
|
|
62
|
+
path = File.join(@data.root, base, *remaining)
|
|
63
|
+
path + fmt.nested_ext(remaining.last)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def enumerate_leaf(entry)
|
|
68
|
+
fp = resolve_leaf_path(entry)
|
|
69
|
+
File.exist?(fp) ? [{ key: entry.key, path: fp, manifest_entry: entry }] : []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def enumerate_nested(entry, include_keyless: false)
|
|
73
|
+
# publish_tree mirrors opaque payload by path — its files are never
|
|
74
|
+
# enumerated as keys (ADR 0047). Ask the resolved mode, not the path.
|
|
75
|
+
# The `include_keyless:` override is used only by the projection lister
|
|
76
|
+
# so that `from: project` selects can read source data from keyless
|
|
77
|
+
# nested entries (e.g. knowledge.evidence) without exposing them as
|
|
78
|
+
# addressable store keys in the public `list` surface.
|
|
79
|
+
return [] if entry.publish_mode.keyless? && !include_keyless
|
|
80
|
+
|
|
81
|
+
base = File.join(@data.root, Textus::Protocol::Key::Path.normalize_relative_path(entry.path))
|
|
82
|
+
return [] unless File.directory?(base)
|
|
83
|
+
|
|
84
|
+
Dir.glob(File.join(base, nested_glob(entry.format)))
|
|
85
|
+
.filter_map { |path| nested_row_for(entry, base, path) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def nested_row_for(entry, base, path)
|
|
89
|
+
rel = path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
|
|
90
|
+
|
|
91
|
+
stripped = rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "")
|
|
92
|
+
segs = stripped.split("/").reject { |s| s.empty? || s == "." }
|
|
93
|
+
return nil if segs.empty?
|
|
94
|
+
|
|
95
|
+
illegal = segs.find { |s| !valid_segment?(s) }
|
|
96
|
+
if illegal
|
|
97
|
+
warn("textus: skipping illegal key segment '#{illegal}' at #{path} — " \
|
|
98
|
+
"rename to match [a-z0-9][a-z0-9-]* (run 'textus doctor' for the full list)")
|
|
99
|
+
return nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
{ key: (entry.key.split(".") + segs).join("."), path: path, manifest_entry: entry }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def valid_segment?(seg)
|
|
106
|
+
return false if seg.nil? || seg.empty?
|
|
107
|
+
return false if seg.length > Protocol::Key::Grammar::MAX_SEGMENT_LEN
|
|
108
|
+
|
|
109
|
+
seg.match?(Protocol::Key::Grammar::SEGMENT)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def resolve_leaf_path(entry)
|
|
113
|
+
Textus::Protocol::Key::Path.resolve(@data, entry)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def nested_glob(format)
|
|
117
|
+
Textus::Protocol::Format.for(format).nested_glob
|
|
118
|
+
end
|
|
119
|
+
class << self
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def shared_prefix_segments(left, right)
|
|
123
|
+
asegs = left.split(".")
|
|
124
|
+
bsegs = right.split(".")
|
|
125
|
+
n = [asegs.length, bsegs.length].min
|
|
126
|
+
i = 0
|
|
127
|
+
i += 1 while i < n && asegs[i] == bsegs[i]
|
|
128
|
+
i
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def levenshtein(left, right)
|
|
132
|
+
return nil if left.length > MAX_DISTANCE_LEN || right.length > MAX_DISTANCE_LEN
|
|
133
|
+
return right.length if left.empty?
|
|
134
|
+
return left.length if right.empty?
|
|
135
|
+
|
|
136
|
+
prev = (0..right.length).to_a
|
|
137
|
+
curr = Array.new(right.length + 1, 0)
|
|
138
|
+
(1..left.length).each do |i|
|
|
139
|
+
curr[0] = i
|
|
140
|
+
(1..right.length).each do |j|
|
|
141
|
+
cost = left[i - 1] == right[j - 1] ? 0 : 1
|
|
142
|
+
curr[j] = [
|
|
143
|
+
curr[j - 1] + 1,
|
|
144
|
+
prev[j] + 1,
|
|
145
|
+
prev[j - 1] + cost,
|
|
146
|
+
].min
|
|
147
|
+
end
|
|
148
|
+
prev, curr = curr, prev
|
|
149
|
+
end
|
|
150
|
+
prev[right.length]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "dry/schema"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Manifest
|
|
6
|
+
module Schema
|
|
7
|
+
# rubocop:disable Metrics/BlockLength
|
|
8
|
+
Contract = Dry::Schema.JSON do
|
|
9
|
+
optional(:lanes).value(:array).each do
|
|
10
|
+
hash do
|
|
11
|
+
required(:name).value(:string)
|
|
12
|
+
required(:kind).value(included_in?: LANE_KINDS)
|
|
13
|
+
optional(:owner).value(:string)
|
|
14
|
+
optional(:desc).value(:string)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
optional(:roles).value(:array).each do
|
|
19
|
+
hash do
|
|
20
|
+
required(:name).value(:string)
|
|
21
|
+
optional(:can).value(:array).each(:string)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
optional(:entries).value(:array).each do
|
|
26
|
+
hash do
|
|
27
|
+
required(:key).value(:string)
|
|
28
|
+
optional(:format).value(:string)
|
|
29
|
+
optional(:schema).maybe(:string)
|
|
30
|
+
optional(:tracked).value(:bool)
|
|
31
|
+
optional(:naming).value(:string)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
optional(:data).value(:hash)
|
|
36
|
+
|
|
37
|
+
optional(:audit).hash do
|
|
38
|
+
optional(:max_size).value(:integer)
|
|
39
|
+
optional(:keep).value(:integer)
|
|
40
|
+
end
|
|
41
|
+
optional(:worker).hash do
|
|
42
|
+
optional(:pool).value(:integer)
|
|
43
|
+
optional(:poll).value(:integer)
|
|
44
|
+
optional(:lease_ttl).value(:integer)
|
|
45
|
+
optional(:max_attempts).value(:integer)
|
|
46
|
+
end
|
|
47
|
+
optional(:version).value(:string)
|
|
48
|
+
end
|
|
49
|
+
# rubocop:enable Metrics/BlockLength
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Manifest
|
|
6
|
+
module Schema
|
|
7
|
+
# Orchestrates structural validation (dry-schema Contract) then cross-field
|
|
8
|
+
# semantic checks (Semantics). Public interface unchanged: Validator.validate!(raw).
|
|
9
|
+
module Validator
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def validate!(raw)
|
|
13
|
+
raise BadManifest.new("manifest must be a hash") unless raw.is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
# Root unknown-key check before Contract so it fires even when lanes: is empty.
|
|
16
|
+
Schema.walk(raw, ROOT_KEYS, "$")
|
|
17
|
+
|
|
18
|
+
result = Contract.call(raw)
|
|
19
|
+
raise BadManifest.new(format_first_error(result.errors.messages)) unless result.success?
|
|
20
|
+
|
|
21
|
+
if raw.key?("data")
|
|
22
|
+
raise BadManifest.new("manifest must declare data:") if raw["data"].nil?
|
|
23
|
+
elsif Array(raw["lanes"]).empty?
|
|
24
|
+
raise BadManifest.new("manifest must declare lanes:")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Format the first dry-schema error to match the legacy path-prefixed style:
|
|
29
|
+
# "unknown key 'x' at '$.lanes[0]'" for extra-key errors;
|
|
30
|
+
# "manifest structure error at <path>: <msg>" for type/value errors.
|
|
31
|
+
def format_first_error(messages)
|
|
32
|
+
msg = messages.first
|
|
33
|
+
return "manifest structure error: unknown" unless msg
|
|
34
|
+
|
|
35
|
+
parent = format_path(msg.path[0..-2])
|
|
36
|
+
key = msg.path.last
|
|
37
|
+
|
|
38
|
+
if msg.text == "is not allowed"
|
|
39
|
+
"unknown key '#{key}' at '#{parent}'"
|
|
40
|
+
else
|
|
41
|
+
"manifest structure error at #{format_path(msg.path)}: #{msg.text}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_path(parts)
|
|
46
|
+
"$" + Array(parts).map { |p| p.is_a?(Integer) ? "[#{p}]" : ".#{p}" }.join
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Manifest
|
|
4
|
+
module Schema
|
|
5
|
+
# The closed coordination vocabulary (ADR 0028; five in 0033; unified in
|
|
6
|
+
# 0034; the quarantine + derived ZONE-KINDS folded into one `machine` kind
|
|
7
|
+
# in ADR 0091). Each kind pairs with the capability that authorizes
|
|
8
|
+
# originating bytes in it.
|
|
9
|
+
LANES = {
|
|
10
|
+
"canon" => "author",
|
|
11
|
+
"workspace" => "keep",
|
|
12
|
+
"machine" => "converge",
|
|
13
|
+
"queue" => "propose",
|
|
14
|
+
"raw" => "ingest",
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
LANE_KINDS = LANES.keys.freeze
|
|
18
|
+
CAPABILITIES = LANES.values.uniq.freeze
|
|
19
|
+
KIND_REQUIRES_VERB = LANES
|
|
20
|
+
|
|
21
|
+
# Key whitelists and rule-field registry (ADR 0109).
|
|
22
|
+
ROOT_KEYS = %w[version roles owner lanes entries data audit worker].freeze
|
|
23
|
+
LANE_KEYS = %w[name kind owner desc].freeze
|
|
24
|
+
ENTRY_KEYS = %w[key schema format naming tracked].freeze
|
|
25
|
+
AUDIT_KEYS = %w[max_size keep].freeze
|
|
26
|
+
|
|
27
|
+
OWNER_SUBJECT_PATTERN = /\A[a-z][a-z0-9_-]*\z/
|
|
28
|
+
|
|
29
|
+
# Public entry point — structural validation then semantic checks.
|
|
30
|
+
def self.validate!(raw) = Validator.validate!(raw)
|
|
31
|
+
|
|
32
|
+
def self.walk(raw, allowed, path)
|
|
33
|
+
raw.each_key do |k|
|
|
34
|
+
raise BadManifest.new("unknown key '#{k}' at '#{path}'") unless allowed.include?(k)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
Manifest = Data.define(:data, :resolver, :policy)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "manifest/schema"
|
|
10
|
+
require_relative "manifest/data"
|
|
11
|
+
require_relative "manifest/policy"
|
|
12
|
+
require_relative "manifest/resolver"
|
|
13
|
+
|
|
14
|
+
module Textus
|
|
15
|
+
module Protocol
|
|
16
|
+
class Manifest
|
|
17
|
+
class << self
|
|
18
|
+
def parse(yaml_text, root: ".")
|
|
19
|
+
raw = YAML.safe_load(yaml_text, aliases: true)
|
|
20
|
+
check_version!(raw, "<string>")
|
|
21
|
+
build(raw, root)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load(root)
|
|
25
|
+
manifest_path = File.join(root, "manifest.yaml")
|
|
26
|
+
raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
|
|
27
|
+
|
|
28
|
+
raw = YAML.safe_load_file(manifest_path, aliases: true)
|
|
29
|
+
check_version!(raw, manifest_path)
|
|
30
|
+
build(raw, root)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build(raw, root)
|
|
36
|
+
# Phase 1: structural data + authority policy (no entry validation)
|
|
37
|
+
data = Manifest::Data.parse(raw, root: root)
|
|
38
|
+
policy = Manifest::Policy.new(data)
|
|
39
|
+
|
|
40
|
+
policy.entries = data.entries
|
|
41
|
+
|
|
42
|
+
new(
|
|
43
|
+
data:,
|
|
44
|
+
resolver: Manifest::Resolver.new(data),
|
|
45
|
+
policy:,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def check_version!(raw, source)
|
|
50
|
+
return if raw["version"] == PROTOCOL
|
|
51
|
+
|
|
52
|
+
raise BadFrontmatter.new(
|
|
53
|
+
source,
|
|
54
|
+
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Textus::Manifest = Textus::Protocol::Manifest
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Produce
|
|
4
|
+
class Render
|
|
5
|
+
class Context
|
|
6
|
+
def self.for(locals:, resolver: nil, from_path: nil, from_key: nil, edge_store: nil)
|
|
7
|
+
new(locals, resolver, from_path, from_key, edge_store)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(locals, resolver, from_path, from_key, edge_store)
|
|
11
|
+
@locals = locals
|
|
12
|
+
@resolver = resolver
|
|
13
|
+
@from_path = from_path
|
|
14
|
+
@from_key = from_key
|
|
15
|
+
@edge_store = edge_store
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_erb_binding
|
|
19
|
+
mod = Module.new
|
|
20
|
+
@locals.each { |k, v| mod.define_method(k.to_sym) { v } }
|
|
21
|
+
obj = Object.new.extend(mod)
|
|
22
|
+
|
|
23
|
+
if @resolver && @from_path
|
|
24
|
+
resolver = @resolver
|
|
25
|
+
from_path = @from_path
|
|
26
|
+
from_key = @from_key
|
|
27
|
+
edge_store = @edge_store
|
|
28
|
+
obj.define_singleton_method(:textus_link) do |key|
|
|
29
|
+
resolved = resolver.resolve(key: key, from_path: from_path)
|
|
30
|
+
edge_store&.record(from_key: from_key, to_key: key)
|
|
31
|
+
resolved
|
|
32
|
+
rescue Textus::Protocol::Links::Resolver::UnknownKeyError
|
|
33
|
+
"`textus get #{key}`"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
obj.instance_eval { binding }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Produce
|
|
4
|
+
class Render
|
|
5
|
+
def initialize(template_loader:, manifest: nil, source_publish_path: nil, entry_key: nil, edge_store: nil)
|
|
6
|
+
@template_loader = template_loader
|
|
7
|
+
@manifest = manifest
|
|
8
|
+
@source_publish_path = source_publish_path
|
|
9
|
+
@entry_key = entry_key
|
|
10
|
+
@edge_store = edge_store
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def bytes_for(target:, data:, boot:)
|
|
14
|
+
raise ArgumentError.new("Produce::Render called for a verbatim target #{target.to.inspect}") unless target.renders?
|
|
15
|
+
|
|
16
|
+
locals = target.inject_boot ? data.merge("boot" => boot) : data
|
|
17
|
+
resolver = @manifest ? Textus::Protocol::Links::Resolver.new(manifest: @manifest) : nil
|
|
18
|
+
ctx = Render::Context.for(
|
|
19
|
+
locals: locals,
|
|
20
|
+
resolver: resolver,
|
|
21
|
+
from_path: target.to,
|
|
22
|
+
from_key: @entry_key,
|
|
23
|
+
edge_store: @edge_store,
|
|
24
|
+
)
|
|
25
|
+
raw = ERB.new(@template_loader.call(target.template), trim_mode: "-").result(ctx.to_erb_binding)
|
|
26
|
+
rewrite(raw, target.to, resolver)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def rewrite(bytes, from_path, resolver)
|
|
32
|
+
return bytes unless resolver && bytes.include?("textus:")
|
|
33
|
+
|
|
34
|
+
Textus::Protocol::Links::UriRewriter.new(
|
|
35
|
+
resolver: resolver,
|
|
36
|
+
from_path: from_path,
|
|
37
|
+
from_key: @entry_key,
|
|
38
|
+
edge_store: @edge_store,
|
|
39
|
+
).rewrite(bytes)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Schema
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize(dir)
|
|
6
|
+
@dir = dir
|
|
7
|
+
@schemas = {}
|
|
8
|
+
load_all
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def fetch(name)
|
|
12
|
+
@schemas[name] || raise(Textus::IoError.new("schema not found: #{File.join(@dir, "#{name}.yaml")}"))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch_or_nil(name)
|
|
16
|
+
return nil if name.nil?
|
|
17
|
+
|
|
18
|
+
fetch(name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def all
|
|
22
|
+
@schemas.values
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def by_name
|
|
26
|
+
@schemas.dup
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def load_all
|
|
32
|
+
return unless File.directory?(@dir)
|
|
33
|
+
|
|
34
|
+
Dir.glob(File.join(@dir, "**", "*.yaml")).each do |path|
|
|
35
|
+
name = path.delete_prefix("#{@dir}/").sub(/\.yaml\z/, "")
|
|
36
|
+
@schemas[name] = Textus::Protocol::Schema.load(path)
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
warn "textus: failed to load schema '#{name}' at #{path}: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Textus
|
|
5
|
+
module Protocol
|
|
6
|
+
class Schema
|
|
7
|
+
module Tools
|
|
8
|
+
def self.init(store, name:, from:)
|
|
9
|
+
env = pure_get(store, Textus::Value::Role::DEFAULT, from)
|
|
10
|
+
meta = env["_meta"]
|
|
11
|
+
schema = {
|
|
12
|
+
"name" => name,
|
|
13
|
+
"required" => meta.keys,
|
|
14
|
+
"optional" => [],
|
|
15
|
+
"fields" => meta.each_with_object({}) { |(k, v), h| h[k] = { "type" => infer_type(v) } },
|
|
16
|
+
}
|
|
17
|
+
geom = Textus::Protocol::Layout.new(store.root)
|
|
18
|
+
FileUtils.mkdir_p(geom.schemas_dir)
|
|
19
|
+
target = geom.schema_path(name)
|
|
20
|
+
File.write(target, YAML.dump(schema))
|
|
21
|
+
{ "protocol" => PROTOCOL, "schema_name" => name, "path" => target }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.diff(store, name:)
|
|
25
|
+
schema = load_schema(store, name)
|
|
26
|
+
drift = []
|
|
27
|
+
store.manifest.resolver.enumerate.each do |row|
|
|
28
|
+
env = pure_get(store, Textus::Value::Role::DEFAULT, row[:key])
|
|
29
|
+
begin
|
|
30
|
+
schema.validate!(env["_meta"])
|
|
31
|
+
rescue SchemaViolation => e
|
|
32
|
+
drift << { "key" => row[:key], "details" => e.details }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
{ "protocol" => PROTOCOL, "schema_name" => name, "drift" => drift }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.migrate(store, name:, rename: nil)
|
|
39
|
+
renames =
|
|
40
|
+
if rename
|
|
41
|
+
old_field, new_field = rename.split(":", 2)
|
|
42
|
+
raise UsageError.new("--rename=OLD:NEW") unless old_field && new_field && !new_field.empty?
|
|
43
|
+
|
|
44
|
+
{ old_field => new_field }
|
|
45
|
+
else
|
|
46
|
+
load_schema(store, name).evolution["migrate_from"] || {}
|
|
47
|
+
end
|
|
48
|
+
raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
|
|
49
|
+
|
|
50
|
+
authority = accept_role_for(store)
|
|
51
|
+
ops = store.with_role(authority)
|
|
52
|
+
touched = []
|
|
53
|
+
store.manifest.resolver.enumerate.each do |row|
|
|
54
|
+
env = pure_get(store, authority, row[:key])
|
|
55
|
+
meta = (env["_meta"] || {}).dup
|
|
56
|
+
changed = false
|
|
57
|
+
renames.each do |old, new|
|
|
58
|
+
if meta.key?(old)
|
|
59
|
+
meta[new] = meta.delete(old)
|
|
60
|
+
changed = true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
next unless changed
|
|
64
|
+
|
|
65
|
+
ops.put(key: row[:key], meta: meta, body: env["body"])
|
|
66
|
+
touched << row[:key]
|
|
67
|
+
end
|
|
68
|
+
{ "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.infer_type(value)
|
|
72
|
+
case value
|
|
73
|
+
when String then "string"
|
|
74
|
+
when Numeric then "number"
|
|
75
|
+
when true, false then "boolean"
|
|
76
|
+
when Array then "array"
|
|
77
|
+
when Hash then "object"
|
|
78
|
+
else "string"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.pure_get(store, role, key)
|
|
83
|
+
store.with_role(role).get(key: key)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.load_schema(store, name)
|
|
87
|
+
store.schemas.fetch(name)
|
|
88
|
+
rescue IoError
|
|
89
|
+
raise UsageError.new("schema not found: #{name}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.accept_role_for(store)
|
|
93
|
+
authority = store.manifest.policy.writers_for("knowledge").first
|
|
94
|
+
return authority if authority
|
|
95
|
+
|
|
96
|
+
raise UsageError.new(
|
|
97
|
+
"schema migrate requires a role that can write knowledge; " \
|
|
98
|
+
"none found in the fixed lane table",
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|