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
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "digest"
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Port
|
|
7
|
-
# Persistence adapter for sentinel files. Owns the on-disk JSON shape, the
|
|
8
|
-
# path layout (<store_root>/.run/sentinels/<target-rel-to-repo>.textus-managed.json
|
|
9
|
-
# — runtime, git-ignored, ADR 0070), and all File/FileUtils I/O.
|
|
10
|
-
# Core::Sentinel is a pure value object that depends on this port for
|
|
11
|
-
# reads and writes.
|
|
12
|
-
class SentinelStore
|
|
13
|
-
SUFFIX = ".textus-managed.json".freeze
|
|
14
|
-
|
|
15
|
-
def write!(target:, source:, store_root:)
|
|
16
|
-
path = sentinel_path(target, store_root)
|
|
17
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
18
|
-
repo_root = File.dirname(store_root)
|
|
19
|
-
File.write(path, JSON.generate(
|
|
20
|
-
"source" => rel_or_abs(source, repo_root),
|
|
21
|
-
"target" => rel_or_abs(target, repo_root),
|
|
22
|
-
"sha256" => Digest::SHA256.hexdigest(File.binread(target)),
|
|
23
|
-
"mode" => "copy",
|
|
24
|
-
))
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def load(path, repo_root)
|
|
28
|
-
raw = JSON.parse(File.read(path))
|
|
29
|
-
Textus::Port::Sentinel.new(
|
|
30
|
-
target: absolutize(raw["target"], repo_root),
|
|
31
|
-
source: absolutize(raw["source"], repo_root),
|
|
32
|
-
sha256: raw["sha256"],
|
|
33
|
-
mode: raw["mode"],
|
|
34
|
-
)
|
|
35
|
-
rescue JSON::ParserError, Errno::ENOENT
|
|
36
|
-
nil
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def sentinel_path(target, store_root)
|
|
40
|
-
repo_root = File.dirname(store_root)
|
|
41
|
-
rel = relative_to(target, repo_root) || File.basename(target)
|
|
42
|
-
File.join(Textus::Store::Layout.new(store_root).sentinels_root, rel + SUFFIX)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Absolute target paths of every sentinel recorded under `target_dir`.
|
|
46
|
-
def targets_under(target_dir, store_root)
|
|
47
|
-
repo_root = File.dirname(store_root)
|
|
48
|
-
rel = relative_to(target_dir, repo_root) or return []
|
|
49
|
-
root = Textus::Store::Layout.new(store_root).sentinels_root
|
|
50
|
-
sdir = File.join(root, rel)
|
|
51
|
-
return [] unless File.directory?(sdir)
|
|
52
|
-
|
|
53
|
-
prefix = root + "/"
|
|
54
|
-
Dir.glob(File.join(sdir, "**", "*#{SUFFIX}")).map do |spath|
|
|
55
|
-
# strip the sentinel-store prefix and the .textus-managed.json suffix to recover the repo-relative target path
|
|
56
|
-
trel = spath.delete_prefix(prefix).delete_suffix(SUFFIX)
|
|
57
|
-
File.join(repo_root, trel)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def rel_or_abs(path, repo_root)
|
|
64
|
-
relative_to(path, repo_root) || File.expand_path(path)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def relative_to(path, repo_root)
|
|
68
|
-
path = File.expand_path(path)
|
|
69
|
-
base = File.expand_path(repo_root)
|
|
70
|
-
return nil unless path.start_with?(base + File::SEPARATOR)
|
|
71
|
-
|
|
72
|
-
path[(base.length + 1)..]
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def absolutize(path, repo_root)
|
|
76
|
-
return path if path.nil?
|
|
77
|
-
return path if File.absolute_path?(path)
|
|
78
|
-
|
|
79
|
-
File.expand_path(path, repo_root)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Port
|
|
3
|
-
module Storage
|
|
4
|
-
# Read-only filesystem query port. The narrow interface that pure
|
|
5
|
-
# domain logic (staleness checks, sentinel value) depends on, so the
|
|
6
|
-
# domain never touches File/Dir directly. FileStore owns the write side.
|
|
7
|
-
class FileStat
|
|
8
|
-
def exists?(path) = File.exist?(path)
|
|
9
|
-
def directory?(path) = File.directory?(path)
|
|
10
|
-
def read(path) = File.binread(path)
|
|
11
|
-
def mtime(path) = File.mtime(path)
|
|
12
|
-
|
|
13
|
-
# Ruby 3.3+ guarantees Dir.glob returns a sorted Array; no explicit sort
|
|
14
|
-
# needed, but callers can rely on ordered results for stable behaviour.
|
|
15
|
-
def glob(pattern) = Dir.glob(pattern)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Port
|
|
5
|
-
module Storage
|
|
6
|
-
# Pure filesystem I/O port. Wraps File/FileUtils/Etag with no knowledge
|
|
7
|
-
# of envelopes, entries, schemas, or audit.
|
|
8
|
-
class FileStore
|
|
9
|
-
include Interface
|
|
10
|
-
|
|
11
|
-
def read(path) = File.binread(path)
|
|
12
|
-
|
|
13
|
-
def write(path, bytes)
|
|
14
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
15
|
-
File.binwrite(path, bytes)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Raises Errno::ENOENT if absent — mirrors File.delete and matches the
|
|
19
|
-
# semantics used by Store::Writer (which guards with File.exist? first).
|
|
20
|
-
def delete(path) = File.delete(path)
|
|
21
|
-
|
|
22
|
-
def exists?(path) = File.exist?(path)
|
|
23
|
-
|
|
24
|
-
def etag(path) = Value::Etag.for_file(path)
|
|
25
|
-
|
|
26
|
-
# Convenience filesystem ops so callers can go through the port
|
|
27
|
-
# instead of calling FileUtils/Dir directly. Keeps filesystem
|
|
28
|
-
# semantics in one place for easier testing and replacement.
|
|
29
|
-
def mkdir_p(path)
|
|
30
|
-
FileUtils.mkdir_p(path)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def mv(from_path, to_path)
|
|
34
|
-
FileUtils.mkdir_p(File.dirname(to_path))
|
|
35
|
-
FileUtils.mv(from_path, to_path)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def rmdir(path)
|
|
39
|
-
Dir.rmdir(path)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def dir_empty?(dir)
|
|
43
|
-
# Dir.empty? exists on modern Rubies; wrap for clarity
|
|
44
|
-
Dir.empty?(dir)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Port
|
|
3
|
-
module Storage
|
|
4
|
-
module Interface
|
|
5
|
-
def read(path) = raise NotImplementedError
|
|
6
|
-
def write(path, bytes) = raise NotImplementedError
|
|
7
|
-
def delete(path) = raise NotImplementedError
|
|
8
|
-
def exists?(path) = raise NotImplementedError
|
|
9
|
-
def etag(path) = raise NotImplementedError
|
|
10
|
-
def mkdir_p(path) = raise NotImplementedError
|
|
11
|
-
def mv(from_path, to_path) = raise NotImplementedError
|
|
12
|
-
def rmdir(path) = raise NotImplementedError
|
|
13
|
-
def dir_empty?(dir) = raise NotImplementedError
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
data/lib/textus/port/store.rb
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "sqlite3"
|
|
5
|
-
|
|
6
|
-
module Textus
|
|
7
|
-
module Port
|
|
8
|
-
# SQLite-backed runtime store for textus state. Owns the connection,
|
|
9
|
-
# schema setup, WAL mode, and transaction boundary for the index and queue.
|
|
10
|
-
class Store
|
|
11
|
-
attr_reader :path, :connection
|
|
12
|
-
|
|
13
|
-
def initialize(root:)
|
|
14
|
-
@path = Textus::Store::Layout.new(root).store_db_path
|
|
15
|
-
FileUtils.mkdir_p(File.dirname(@path))
|
|
16
|
-
@connection = SQLite3::Database.new(@path)
|
|
17
|
-
@connection.results_as_hash = true
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def execute(sql, params = [])
|
|
21
|
-
@connection.execute(sql, params)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def query_value(sql, params = [])
|
|
25
|
-
@connection.get_first_value(sql, params)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def setup!
|
|
29
|
-
execute("PRAGMA journal_mode=WAL")
|
|
30
|
-
execute("PRAGMA foreign_keys=ON")
|
|
31
|
-
connection.execute_batch(<<~SQL)
|
|
32
|
-
CREATE TABLE IF NOT EXISTS entries (
|
|
33
|
-
key TEXT PRIMARY KEY,
|
|
34
|
-
lane TEXT NOT NULL,
|
|
35
|
-
format TEXT NOT NULL,
|
|
36
|
-
etag TEXT,
|
|
37
|
-
content TEXT,
|
|
38
|
-
extra TEXT,
|
|
39
|
-
indexed_at TEXT NOT NULL
|
|
40
|
-
) STRICT;
|
|
41
|
-
|
|
42
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
|
|
43
|
-
key, lane, content,
|
|
44
|
-
content=entries, content_rowid=rowid
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
CREATE TABLE IF NOT EXISTS jobs (
|
|
48
|
-
id TEXT PRIMARY KEY,
|
|
49
|
-
type TEXT NOT NULL,
|
|
50
|
-
args TEXT NOT NULL,
|
|
51
|
-
state TEXT NOT NULL DEFAULT 'ready',
|
|
52
|
-
role TEXT NOT NULL,
|
|
53
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
54
|
-
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
55
|
-
errors TEXT,
|
|
56
|
-
lease TEXT,
|
|
57
|
-
created_at TEXT NOT NULL,
|
|
58
|
-
updated_at TEXT NOT NULL
|
|
59
|
-
) STRICT;
|
|
60
|
-
|
|
61
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_state ON jobs(state);
|
|
62
|
-
CREATE INDEX IF NOT EXISTS idx_entries_lane ON entries(lane);
|
|
63
|
-
|
|
64
|
-
CREATE TABLE IF NOT EXISTS audit_events (
|
|
65
|
-
seq INTEGER PRIMARY KEY,
|
|
66
|
-
ts TEXT NOT NULL,
|
|
67
|
-
role TEXT NOT NULL,
|
|
68
|
-
verb TEXT NOT NULL,
|
|
69
|
-
key TEXT NOT NULL,
|
|
70
|
-
etag_before TEXT,
|
|
71
|
-
etag_after TEXT
|
|
72
|
-
) STRICT;
|
|
73
|
-
|
|
74
|
-
CREATE INDEX IF NOT EXISTS idx_audit_events_seq ON audit_events(seq);
|
|
75
|
-
SQL
|
|
76
|
-
# Idempotent migration: add schema_ref column if missing (existing stores).
|
|
77
|
-
execute("ALTER TABLE entries ADD COLUMN schema_ref TEXT") rescue nil # rubocop:disable Style/RescueModifier
|
|
78
|
-
self
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def search_entries(q: nil, schema: nil, lane: nil, prefix: nil) # rubocop:disable Naming/MethodParameterName
|
|
82
|
-
return nil if q.nil? && schema.nil?
|
|
83
|
-
|
|
84
|
-
if q
|
|
85
|
-
fts_search(q: q, schema: schema, lane: lane, prefix: prefix)
|
|
86
|
-
else
|
|
87
|
-
schema_search(schema: schema, lane: lane, prefix: prefix)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def insert_audit_event(seq:, ts:, role:, verb:, key:, etag_before:, etag_after:) # rubocop:disable Naming/MethodParameterName
|
|
92
|
-
execute(
|
|
93
|
-
"INSERT OR IGNORE INTO audit_events (seq, ts, role, verb, key, etag_before, etag_after) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
94
|
-
[seq, ts, role, verb, key, etag_before, etag_after],
|
|
95
|
-
)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def audit_events_since(seq:)
|
|
99
|
-
execute(
|
|
100
|
-
"SELECT seq, ts, role, verb, key, etag_before, etag_after FROM audit_events WHERE seq > ? ORDER BY seq",
|
|
101
|
-
[seq],
|
|
102
|
-
)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def transaction
|
|
106
|
-
connection.transaction
|
|
107
|
-
yield
|
|
108
|
-
connection.commit
|
|
109
|
-
rescue StandardError
|
|
110
|
-
connection.rollback if connection.transaction_active?
|
|
111
|
-
raise
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def close
|
|
115
|
-
connection.close unless connection.closed?
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def self.open(root)
|
|
119
|
-
store = new(root: root)
|
|
120
|
-
store.setup!
|
|
121
|
-
return store unless block_given?
|
|
122
|
-
|
|
123
|
-
yield store
|
|
124
|
-
ensure
|
|
125
|
-
store&.close
|
|
126
|
-
end
|
|
127
|
-
private :connection
|
|
128
|
-
|
|
129
|
-
def fts_search(q:, schema:, lane:, prefix:) # rubocop:disable Naming/MethodParameterName
|
|
130
|
-
sql = "SELECT e.key, e.lane, e.schema_ref FROM entries e JOIN entries_fts fts ON e.rowid = fts.rowid WHERE entries_fts MATCH ?"
|
|
131
|
-
params = [q]
|
|
132
|
-
sql += " AND e.lane = ?" and params << lane if lane
|
|
133
|
-
sql += " AND e.schema_ref = ?" and params << schema if schema
|
|
134
|
-
sql += " AND (e.key = ? OR e.key LIKE ?)" and params.push(prefix, "#{prefix}.%") if prefix
|
|
135
|
-
execute(sql, params)
|
|
136
|
-
end
|
|
137
|
-
private :fts_search
|
|
138
|
-
|
|
139
|
-
def schema_search(schema:, lane:, prefix:)
|
|
140
|
-
sql = "SELECT key, lane, schema_ref FROM entries WHERE schema_ref = ?"
|
|
141
|
-
params = [schema]
|
|
142
|
-
sql += " AND lane = ?" and params << lane if lane
|
|
143
|
-
sql += " AND (key = ? OR key LIKE ?)" and params.push(prefix, "#{prefix}.%") if prefix
|
|
144
|
-
execute(sql, params)
|
|
145
|
-
end
|
|
146
|
-
private :schema_search
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Port
|
|
7
|
-
# Flock-based watcher presence lock. Held for the watcher's lifetime.
|
|
8
|
-
# Process death releases the flock automatically.
|
|
9
|
-
class WatcherLock
|
|
10
|
-
def initialize(root)
|
|
11
|
-
@path = Textus::Store::Layout.new(root).lock_path("watcher")
|
|
12
|
-
@file = nil
|
|
13
|
-
FileUtils.mkdir_p(File.dirname(@path))
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.running?(root)
|
|
17
|
-
path = Textus::Store::Layout.new(root).lock_path("watcher")
|
|
18
|
-
return false unless File.exist?(path)
|
|
19
|
-
|
|
20
|
-
File.open(path, "r+") do |file|
|
|
21
|
-
got = file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
22
|
-
file.flock(File::LOCK_UN) if got
|
|
23
|
-
return !got
|
|
24
|
-
end
|
|
25
|
-
rescue Errno::ENOENT
|
|
26
|
-
false
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def acquire
|
|
30
|
-
@file = File.open(@path, File::RDWR | File::CREAT, 0o644)
|
|
31
|
-
raise "watcher already running" unless @file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
32
|
-
|
|
33
|
-
@file.write("pid=#{Process.pid} host=#{Socket.gethostname}\n")
|
|
34
|
-
@file.flush
|
|
35
|
-
self
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def release
|
|
39
|
-
return unless @file
|
|
40
|
-
|
|
41
|
-
@file.flock(File::LOCK_UN)
|
|
42
|
-
@file.close
|
|
43
|
-
FileUtils.rm_f(@path)
|
|
44
|
-
@file = nil
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Produce
|
|
3
|
-
class Engine
|
|
4
|
-
def self.converge(container:, call:, keys:)
|
|
5
|
-
new(container:, call:).run(keys)
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def initialize(container:, call:)
|
|
9
|
-
@container = container
|
|
10
|
-
@call = call
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def run(keys)
|
|
14
|
-
results = { completed: [], failed: [] }
|
|
15
|
-
Array(keys).each { |key| produce_one(key, results) }
|
|
16
|
-
results
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
alias call run
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def produce_one(key, results)
|
|
24
|
-
workflow = @container.workflows.for(key)
|
|
25
|
-
if workflow
|
|
26
|
-
Workflow::Runner.new(workflow, container: @container, call: @call).run(key)
|
|
27
|
-
else
|
|
28
|
-
publish_only(key)
|
|
29
|
-
end
|
|
30
|
-
results[:completed] << key
|
|
31
|
-
rescue StandardError => e
|
|
32
|
-
results[:failed] << { key: key, error: e.message }
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def publish_only(key)
|
|
36
|
-
Textus::Produce::Publisher.call(container: @container, call: @call, key: key)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Produce
|
|
3
|
-
module Publisher
|
|
4
|
-
def self.call(container:, call:, key:)
|
|
5
|
-
entry = container.manifest.resolver.resolve(key).entry
|
|
6
|
-
return unless entry.publish_tree || !Array(entry.publish_to).empty?
|
|
7
|
-
|
|
8
|
-
entry_path = container.manifest.resolver.resolve(key).path
|
|
9
|
-
return unless entry.publish_tree || container.file_store.exists?(entry_path)
|
|
10
|
-
|
|
11
|
-
reader = Textus::Store::Entry::Reader.from(container: container)
|
|
12
|
-
pctx = Textus::Manifest::Entry::Base::PublishContext.new(
|
|
13
|
-
container: container,
|
|
14
|
-
call: call,
|
|
15
|
-
reader: reader.method(:read),
|
|
16
|
-
)
|
|
17
|
-
entry.publish_via(pctx)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "erb"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Produce
|
|
7
|
-
class Render
|
|
8
|
-
def initialize(template_loader:)
|
|
9
|
-
@template_loader = template_loader
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def bytes_for(target:, data:, boot:)
|
|
13
|
-
raise ArgumentError.new("Produce::Render called for a verbatim target #{target.to.inspect}") unless target.renders?
|
|
14
|
-
|
|
15
|
-
ctx = target.inject_boot ? data.merge("boot" => boot) : data
|
|
16
|
-
ERB.new(@template_loader.call(target.template), trim_mode: "-").result_with_hash(ctx)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Schema
|
|
3
|
-
class Registry
|
|
4
|
-
def initialize(dir)
|
|
5
|
-
@dir = dir
|
|
6
|
-
@schemas = {}
|
|
7
|
-
load_all
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def fetch(name)
|
|
11
|
-
@schemas[name] || raise(IoError.new("schema not found: #{File.join(@dir, "#{name}.yaml")}"))
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def fetch_or_nil(name)
|
|
15
|
-
return nil if name.nil?
|
|
16
|
-
|
|
17
|
-
fetch(name)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def all
|
|
21
|
-
@schemas.values
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def by_name
|
|
25
|
-
@schemas.dup
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def load_all
|
|
31
|
-
return unless File.directory?(@dir)
|
|
32
|
-
|
|
33
|
-
Dir.glob(File.join(@dir, "*.yaml")).each do |path|
|
|
34
|
-
name = File.basename(path, ".yaml")
|
|
35
|
-
@schemas[name] = Schema.load(path)
|
|
36
|
-
rescue StandardError => e
|
|
37
|
-
warn "textus: failed to load schema '#{name}' at #{path}: #{e.message}"
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
data/lib/textus/schema/tools.rb
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
require "fileutils"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
class Schema
|
|
6
|
-
module Tools
|
|
7
|
-
# textus schema init NAME --from=KEY → infer YAML schema from an entry's frontmatter
|
|
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::Store::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
|
-
# textus schema diff NAME → list keys whose frontmatter violates the schema
|
|
25
|
-
def self.diff(store, name:)
|
|
26
|
-
schema = load_schema(store, name)
|
|
27
|
-
drift = []
|
|
28
|
-
store.manifest.resolver.enumerate.each do |row|
|
|
29
|
-
env = pure_get(store, Textus::Value::Role::DEFAULT, row[:key])
|
|
30
|
-
begin
|
|
31
|
-
schema.validate!(env.meta)
|
|
32
|
-
rescue SchemaViolation => e
|
|
33
|
-
drift << { "key" => row[:key], "details" => e.details }
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
{ "protocol" => PROTOCOL, "schema_name" => name, "drift" => drift }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# textus schema migrate NAME --rename=OLD:NEW → rewrites frontmatter across affected entries
|
|
40
|
-
# If --rename is omitted, falls back to schema.evolution.migrate_from.
|
|
41
|
-
def self.migrate(store, name:, rename: nil)
|
|
42
|
-
renames =
|
|
43
|
-
if rename
|
|
44
|
-
old_field, new_field = rename.split(":", 2)
|
|
45
|
-
raise UsageError.new("--rename=OLD:NEW") unless old_field && new_field && !new_field.empty?
|
|
46
|
-
|
|
47
|
-
{ old_field => new_field }
|
|
48
|
-
else
|
|
49
|
-
load_schema(store, name).evolution["migrate_from"] || {}
|
|
50
|
-
end
|
|
51
|
-
raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
|
|
52
|
-
|
|
53
|
-
authority = accept_role_for(store)
|
|
54
|
-
ops = store.with_role(authority)
|
|
55
|
-
touched = []
|
|
56
|
-
store.manifest.resolver.enumerate.each do |row|
|
|
57
|
-
env = pure_get(store, authority, row[:key])
|
|
58
|
-
meta = env.meta.dup
|
|
59
|
-
changed = false
|
|
60
|
-
renames.each do |old, new|
|
|
61
|
-
if meta.key?(old)
|
|
62
|
-
meta[new] = meta.delete(old)
|
|
63
|
-
changed = true
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
next unless changed
|
|
67
|
-
|
|
68
|
-
ops.put(row[:key], meta: meta, body: env.body)
|
|
69
|
-
touched << row[:key]
|
|
70
|
-
end
|
|
71
|
-
{ "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def self.infer_type(value)
|
|
75
|
-
case value
|
|
76
|
-
when String then "string"
|
|
77
|
-
when Numeric then "number"
|
|
78
|
-
when true, false then "boolean"
|
|
79
|
-
when Array then "array"
|
|
80
|
-
when Hash then "object"
|
|
81
|
-
else "string"
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Orchestrator-free read: schema tooling must never trigger a fetch
|
|
86
|
-
# while inspecting/migrating entries (ADR 0062).
|
|
87
|
-
def self.pure_get(store, role, key)
|
|
88
|
-
store.with_role(role).get(key)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def self.load_schema(store, name)
|
|
92
|
-
store.schemas.fetch(name)
|
|
93
|
-
rescue IoError
|
|
94
|
-
raise UsageError.new("schema not found: #{name}")
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def self.accept_role_for(store)
|
|
98
|
-
authority = store.manifest.policy.roles_with_capability("author").first
|
|
99
|
-
return authority if authority
|
|
100
|
-
|
|
101
|
-
raise UsageError.new(
|
|
102
|
-
"schema migrate requires a role holding the 'author' capability; " \
|
|
103
|
-
"none declared (add e.g. `- { name: owner, can: [author] }` to roles:)",
|
|
104
|
-
)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|