textus 0.55.1 → 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 +17 -18
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +2 -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/{gate → protocol}/binder.rb +2 -10
- 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/protocol/freshness/ttl_evaluator.rb +61 -0
- 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/geometry.rb → protocol/layout.rb} +32 -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 +25 -25
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +3 -6
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +8 -6
- 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 +26 -62
- 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/errors.rb +0 -10
- data/lib/textus/surface/mcp/server.rb +46 -46
- 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/{core → value}/duration.rb +1 -4
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/value/payload.rb +7 -0
- 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 -61
- 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 -80
- metadata +167 -183
- data/SPEC.md +0 -908
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -138
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/boot.rb +0 -202
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness/verdict.rb +0 -34
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/core/sentinel.rb +0 -30
- 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/notebook_sources.rb +0 -50
- 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/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 -41
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -49
- data/lib/textus/format/json.rb +0 -102
- data/lib/textus/format/markdown.rb +0 -80
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -125
- data/lib/textus/format.rb +0 -46
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- 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/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/meta.rb +0 -54
- 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_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -26
- data/lib/textus/port/store.rb +0 -93
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -50
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/tools.rb +0 -115
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/container.rb +0 -43
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/envelope/reader.rb +0 -43
- data/lib/textus/store/envelope/writer.rb +0 -195
- data/lib/textus/store/index/builder.rb +0 -74
- 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 -104
- data/lib/textus/store/jobs/queue.rb +0 -154
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention.rb +0 -50
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/store.rb +0 -99
- data/lib/textus/surface/cli/verb/doctor.rb +0 -19
- 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.rb +0 -8
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -89
- data/lib/textus/value/result.rb +0 -26
- 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
data/lib/textus/manifest.rb
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
# Manifest is the composition record for a parsed manifest. It bundles
|
|
5
|
-
# four collaborators:
|
|
6
|
-
#
|
|
7
|
-
# * data — frozen value: raw, root, zones, entries, audit_config, role_caps
|
|
8
|
-
# * resolver — resolves keys → entry + path
|
|
9
|
-
# * policy — zone/role authority (lane_writers, declared_kind, derived_entry?,
|
|
10
|
-
# queue_lane?, permission_for, …)
|
|
11
|
-
# * rules — match-block rule engine (lifecycle, handler allowlist, materialize, …)
|
|
12
|
-
#
|
|
13
|
-
# Use `manifest.data.entries`, `manifest.policy.declared_kind(z)`, etc.
|
|
14
|
-
Manifest = Data.define(:data, :resolver, :policy, :rules)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
require_relative "manifest/schema"
|
|
18
|
-
require_relative "manifest/data"
|
|
19
|
-
require_relative "manifest/policy"
|
|
20
|
-
require_relative "manifest/resolver"
|
|
21
|
-
require_relative "manifest/capabilities"
|
|
22
|
-
|
|
23
|
-
# Reopen Textus::Manifest (defined above as a Data.define) to attach
|
|
24
|
-
# class-level loaders and helpers.
|
|
25
|
-
module Textus # rubocop:disable Style/OneClassPerFile
|
|
26
|
-
class Manifest
|
|
27
|
-
class << self
|
|
28
|
-
def parse(yaml_text, root: ".")
|
|
29
|
-
raw = YAML.safe_load(yaml_text, aliases: false)
|
|
30
|
-
check_version!(raw, "<string>")
|
|
31
|
-
build(raw, root)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def load(root)
|
|
35
|
-
manifest_path = File.join(root, "manifest.yaml")
|
|
36
|
-
raise IoError.new("manifest not found: #{manifest_path}") unless File.exist?(manifest_path)
|
|
37
|
-
|
|
38
|
-
raw = YAML.safe_load_file(manifest_path, aliases: false)
|
|
39
|
-
check_version!(raw, manifest_path)
|
|
40
|
-
build(raw, root)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def build(raw, root)
|
|
46
|
-
data = Manifest::Data.parse(raw, root: root)
|
|
47
|
-
new(
|
|
48
|
-
data: data,
|
|
49
|
-
resolver: Manifest::Resolver.new(data),
|
|
50
|
-
policy: data.policy,
|
|
51
|
-
rules: Manifest::Rules.parse(raw["rules"] || []),
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def check_version!(raw, source)
|
|
56
|
-
return if raw["version"] == PROTOCOL
|
|
57
|
-
|
|
58
|
-
raise BadFrontmatter.new(
|
|
59
|
-
source,
|
|
60
|
-
"unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
|
|
61
|
-
)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
data/lib/textus/meta.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Meta
|
|
7
|
-
NO_META_FORMATS = %w[text].freeze
|
|
8
|
-
|
|
9
|
-
FIELDS = {
|
|
10
|
-
"uid" => {
|
|
11
|
-
inject: lambda { |meta, content, existing_meta|
|
|
12
|
-
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
13
|
-
existing = existing_meta.is_a?(Hash) ? existing_meta["uid"] : nil
|
|
14
|
-
m["uid"] = existing || Textus::Value::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
15
|
-
[m, content]
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
"sources" => {
|
|
19
|
-
inject: lambda { |meta, content, existing_meta|
|
|
20
|
-
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
21
|
-
existing = existing_meta.is_a?(Hash) ? existing_meta["sources"] : nil
|
|
22
|
-
|
|
23
|
-
if m.key?("sources")
|
|
24
|
-
raise Textus::BadContent.new(nil, "_meta.sources must be an array") unless m["sources"].is_a?(Array)
|
|
25
|
-
|
|
26
|
-
m["sources"] = m["sources"].map { |s| validate_source_shape!(s) }
|
|
27
|
-
elsif existing.is_a?(Array) && !existing.empty?
|
|
28
|
-
m["sources"] = existing
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
[m, content]
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
}.freeze
|
|
35
|
-
|
|
36
|
-
def self.inject_all(meta, content, existing_meta = {}, format: nil)
|
|
37
|
-
return [meta, content] if NO_META_FORMATS.include?(format)
|
|
38
|
-
|
|
39
|
-
FIELDS.each_value do |field|
|
|
40
|
-
meta, content = field[:inject].call(meta, content, existing_meta)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
[meta, content]
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def self.validate_source_shape!(src)
|
|
47
|
-
raise Textus::BadContent.new(nil, "each source must be a string") unless src.is_a?(String)
|
|
48
|
-
|
|
49
|
-
raise Textus::BadContent.new(nil, "each source must start with 'raw.', got #{src.inspect}") unless src.match?(/\Araw\./)
|
|
50
|
-
|
|
51
|
-
src
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "json"
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Port
|
|
7
|
-
# Append-only audit log adapter: writes and rotates the on-disk audit JSONL
|
|
8
|
-
# under the store root. An instantiable class — it holds collaborators (the
|
|
9
|
-
# root path + size/keep config), so each store binds its own instance. It
|
|
10
|
-
# already satisfied ADR 0109's single-shape rule (every port is an
|
|
11
|
-
# instantiable class) before that ADR's Clock/Publisher conversions, so it
|
|
12
|
-
# was unchanged by them.
|
|
13
|
-
class AuditLog
|
|
14
|
-
DEFAULT_MAX_SIZE = 10_485_760
|
|
15
|
-
DEFAULT_KEEP = 5
|
|
16
|
-
|
|
17
|
-
def initialize(root, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP)
|
|
18
|
-
@root = root
|
|
19
|
-
@path = Textus::Store::Geometry.new(root).audit_log_path
|
|
20
|
-
@max_size = max_size
|
|
21
|
-
@keep = keep
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def last_writer_for(key)
|
|
25
|
-
return nil unless File.exist?(@path)
|
|
26
|
-
|
|
27
|
-
last_role = nil
|
|
28
|
-
File.foreach(@path) do |line|
|
|
29
|
-
parsed = parse_row(line.chomp)
|
|
30
|
-
next unless parsed
|
|
31
|
-
next unless parsed["key"] == key
|
|
32
|
-
next unless %w[put delete key_delete].include?(parsed["verb"])
|
|
33
|
-
|
|
34
|
-
last_role = parsed["role"]
|
|
35
|
-
end
|
|
36
|
-
last_role
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def latest_seq
|
|
40
|
-
return scan_max_seq(@path) if File.exist?(@path) && File.size(@path).positive?
|
|
41
|
-
|
|
42
|
-
# Active log is empty/missing — consult the most recent rotated file's sidecar.
|
|
43
|
-
meta = read_meta(1)
|
|
44
|
-
return meta["max_seq"] if meta
|
|
45
|
-
|
|
46
|
-
0
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def min_available_seq
|
|
50
|
-
rotated_metas = (1..@keep).map { |n| read_meta(n) }.compact
|
|
51
|
-
if rotated_metas.any?
|
|
52
|
-
rotated_metas.map { |m| m["min_seq"] }.min
|
|
53
|
-
elsif File.exist?(@path)
|
|
54
|
-
File.foreach(@path) do |line|
|
|
55
|
-
parsed = parse_row(line.chomp)
|
|
56
|
-
return parsed["seq"] if parsed && parsed["seq"]
|
|
57
|
-
end
|
|
58
|
-
nil
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def append(role:, verb:, key:, etag_before:, etag_after:, extras: nil)
|
|
63
|
-
FileUtils.mkdir_p(File.dirname(@path))
|
|
64
|
-
File.open(@path, File::WRONLY | File::APPEND | File::CREAT, 0o644) do |f|
|
|
65
|
-
f.flock(File::LOCK_EX)
|
|
66
|
-
next_seq = current_max_seq_unlocked + 1
|
|
67
|
-
row = assemble_row(next_seq, { role: role, verb: verb, key: key,
|
|
68
|
-
etag_before: etag_before, etag_after: etag_after }, extras)
|
|
69
|
-
f.write(JSON.generate(row) + "\n")
|
|
70
|
-
f.flush
|
|
71
|
-
rotate!(f) if f.size > @max_size
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Scan log files with optional filters. Returns parsed row hashes.
|
|
76
|
-
# Lane and timestamp filters are left to the caller (they need manifest
|
|
77
|
-
# resolution and Time parsing the port shouldn't know about).
|
|
78
|
-
def scan(seq_since: nil, key: nil, role: nil, verb: nil,
|
|
79
|
-
correlation_id: nil, limit: nil)
|
|
80
|
-
files = all_log_files
|
|
81
|
-
return [] if files.empty?
|
|
82
|
-
|
|
83
|
-
rows = []
|
|
84
|
-
files.each do |file|
|
|
85
|
-
File.foreach(file) do |line|
|
|
86
|
-
parsed = parse_row(line.chomp)
|
|
87
|
-
next unless parsed && matches?(parsed, seq_since:, key:, role:, verb:, correlation_id:)
|
|
88
|
-
|
|
89
|
-
rows << parsed
|
|
90
|
-
break if limit && rows.length >= limit
|
|
91
|
-
end
|
|
92
|
-
break if limit && rows.length >= limit
|
|
93
|
-
end
|
|
94
|
-
rows
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Returns an array of integrity-violation descriptors for the on-disk log.
|
|
98
|
-
# Each entry is { "lineno" => Integer, "reason" => String, "detail" => String }.
|
|
99
|
-
# Empty array means the log is well-formed (or doesn't exist yet).
|
|
100
|
-
def verify_integrity
|
|
101
|
-
return [] unless File.exist?(@path)
|
|
102
|
-
|
|
103
|
-
[].tap do |out|
|
|
104
|
-
iterate_with_prev_seq do |line, lineno, prev_seq|
|
|
105
|
-
check_line_integrity(line, lineno, prev_seq, out)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
def iterate_with_prev_seq
|
|
113
|
-
prev_seq = nil
|
|
114
|
-
File.foreach(@path).with_index(1) do |line, lineno|
|
|
115
|
-
yield line.chomp, lineno, prev_seq
|
|
116
|
-
parsed = parse_row(line.chomp)
|
|
117
|
-
prev_seq = parsed["seq"] if parsed&.dig("seq").is_a?(Integer)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def check_line_integrity(line, lineno, prev_seq, out)
|
|
122
|
-
violation = check_line(line, lineno)
|
|
123
|
-
if violation
|
|
124
|
-
out << violation
|
|
125
|
-
return
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
parsed = parse_row(line)
|
|
129
|
-
return unless parsed && (seq = parsed["seq"]).is_a?(Integer)
|
|
130
|
-
return unless prev_seq && seq != prev_seq + 1
|
|
131
|
-
|
|
132
|
-
out << {
|
|
133
|
-
"lineno" => lineno,
|
|
134
|
-
"reason" => "seq_gap",
|
|
135
|
-
"detail" => "expected #{prev_seq + 1}, got #{seq}",
|
|
136
|
-
}
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def rotated(n)
|
|
140
|
-
File.join(Textus::Store::Geometry.new(@root).audit_dir_path, "audit.log.#{n}")
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def rotated_meta(n)
|
|
144
|
-
File.join(Textus::Store::Geometry.new(@root).audit_dir_path, "audit.log.#{n}.meta.json")
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Caller holds the flock. Returns the highest seq across the active log,
|
|
148
|
-
# OR the most-recent rotated file's max_seq if the active log is empty.
|
|
149
|
-
def current_max_seq_unlocked
|
|
150
|
-
return scan_max_seq(@path) if File.exist?(@path) && File.size(@path).positive?
|
|
151
|
-
|
|
152
|
-
meta = read_meta(1)
|
|
153
|
-
meta ? meta["max_seq"] : 0
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def scan_max_seq(file)
|
|
157
|
-
last = 0
|
|
158
|
-
File.foreach(file) do |line|
|
|
159
|
-
parsed = parse_row(line.chomp)
|
|
160
|
-
last = parsed["seq"] if parsed && parsed["seq"].is_a?(Integer)
|
|
161
|
-
end
|
|
162
|
-
last
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def scan_seq_range(file)
|
|
166
|
-
min = nil
|
|
167
|
-
max = 0
|
|
168
|
-
File.foreach(file) do |line|
|
|
169
|
-
parsed = parse_row(line.chomp)
|
|
170
|
-
next unless parsed && parsed["seq"]
|
|
171
|
-
|
|
172
|
-
min ||= parsed["seq"]
|
|
173
|
-
max = parsed["seq"]
|
|
174
|
-
end
|
|
175
|
-
[min, max]
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def read_meta(n)
|
|
179
|
-
path = rotated_meta(n)
|
|
180
|
-
return nil unless File.exist?(path)
|
|
181
|
-
|
|
182
|
-
JSON.parse(File.read(path))
|
|
183
|
-
rescue JSON::ParserError
|
|
184
|
-
nil
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def assemble_row(seq, fields, extras = nil)
|
|
188
|
-
row = {
|
|
189
|
-
"seq" => seq,
|
|
190
|
-
"ts" => Time.now.utc.iso8601,
|
|
191
|
-
"role" => fields[:role],
|
|
192
|
-
"verb" => fields[:verb],
|
|
193
|
-
"key" => fields[:key],
|
|
194
|
-
"etag_before" => fields[:etag_before],
|
|
195
|
-
"etag_after" => fields[:etag_after],
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if extras.is_a?(Hash) && !extras.empty?
|
|
199
|
-
extras = extras.dup
|
|
200
|
-
%w[from_key to_key uid].each do |k|
|
|
201
|
-
row[k] = extras.delete(k) if extras.key?(k)
|
|
202
|
-
end
|
|
203
|
-
row["extras"] = extras unless extras.empty?
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
row
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Called inside the flock, after a successful write that pushed size over max.
|
|
210
|
-
# Renames audit.log → audit.log.1 (shifting older files), writes sidecar meta.
|
|
211
|
-
def rotate!(open_file)
|
|
212
|
-
open_file.flush
|
|
213
|
-
min_seq, max_seq = scan_seq_range(@path)
|
|
214
|
-
meta = { "min_seq" => min_seq, "max_seq" => max_seq, "rotated_at" => Time.now.utc.iso8601 }
|
|
215
|
-
|
|
216
|
-
# Drop the file that would be shifted past @keep.
|
|
217
|
-
FileUtils.rm_f(rotated(@keep))
|
|
218
|
-
FileUtils.rm_f(rotated_meta(@keep))
|
|
219
|
-
|
|
220
|
-
# Shift .N → .(N+1) for N = keep-1 down to 1.
|
|
221
|
-
(@keep - 1).downto(1) do |n|
|
|
222
|
-
File.rename(rotated(n), rotated(n + 1)) if File.exist?(rotated(n))
|
|
223
|
-
File.rename(rotated_meta(n), rotated_meta(n + 1)) if File.exist?(rotated_meta(n))
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Active log → .1
|
|
227
|
-
File.rename(@path, rotated(1))
|
|
228
|
-
File.write(rotated_meta(1), JSON.generate(meta) + "\n")
|
|
229
|
-
# Next append will create a fresh audit.log via File::CREAT.
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def parse_row(line)
|
|
233
|
-
return nil if line.empty?
|
|
234
|
-
|
|
235
|
-
JSON.parse(line)
|
|
236
|
-
rescue JSON::ParserError
|
|
237
|
-
nil
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def matches?(row, seq_since: nil, key: nil, role: nil, verb: nil, correlation_id: nil)
|
|
241
|
-
return false if seq_since && row["seq"] <= seq_since
|
|
242
|
-
return false if key && row["key"] != key
|
|
243
|
-
return false if role && row["role"] != role
|
|
244
|
-
return false if verb && row["verb"] != verb
|
|
245
|
-
return false if correlation_id && row.dig("extras", "correlation_id") != correlation_id
|
|
246
|
-
|
|
247
|
-
true
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def all_log_files
|
|
251
|
-
rotated = Dir.glob(File.join(Textus::Store::Geometry.new(@root).audit_dir_path, "audit.log.*"))
|
|
252
|
-
.reject { |path| path.end_with?(".meta.json") }
|
|
253
|
-
.sort_by { |path| -path.scan(/\d+$/).first.to_i }
|
|
254
|
-
active_log = File.exist?(@path) ? [@path] : []
|
|
255
|
-
rotated + active_log
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def check_line(stripped, lineno)
|
|
259
|
-
return nil if stripped.empty?
|
|
260
|
-
|
|
261
|
-
JSON.parse(stripped)
|
|
262
|
-
nil
|
|
263
|
-
rescue JSON::ParserError => e
|
|
264
|
-
{ "lineno" => lineno, "reason" => "invalid_json", "detail" => e.message }
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "socket"
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Port
|
|
7
|
-
# Cross-process build lock: a pid/host-stamped lockfile under the store root
|
|
8
|
-
# that serializes converge's produce/sweep. An instantiable class — it holds
|
|
9
|
-
# the root and lock state; `self.with(root:)` is a convenience that constructs
|
|
10
|
-
# one and runs the block under the held lock. It already satisfied ADR 0109's
|
|
11
|
-
# single-shape rule (every port is an instantiable class) before that ADR's
|
|
12
|
-
# Clock/Publisher conversions, so it was unchanged by them.
|
|
13
|
-
class BuildLock
|
|
14
|
-
MAX_HOLDER_BYTES = 512
|
|
15
|
-
|
|
16
|
-
def self.with(root:, &)
|
|
17
|
-
new(root: root).acquire_or_raise(&)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def initialize(root:)
|
|
21
|
-
@path = Textus::Store::Geometry.new(root).lock_path("build")
|
|
22
|
-
@file = nil
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def acquire_or_raise
|
|
26
|
-
FileUtils.mkdir_p(File.dirname(@path))
|
|
27
|
-
@file = File.open(@path, File::RDWR | File::CREAT, 0o644)
|
|
28
|
-
@file.close_on_exec = true
|
|
29
|
-
|
|
30
|
-
unless @file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
31
|
-
holder = read_holder_safely
|
|
32
|
-
@file.close
|
|
33
|
-
@file = nil
|
|
34
|
-
raise Textus::BuildInProgress.new(holder)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
@file.truncate(0)
|
|
38
|
-
@file.write("pid=#{Process.pid} started=#{Time.now.utc.iso8601} host=#{Socket.gethostname}\n")
|
|
39
|
-
@file.flush
|
|
40
|
-
|
|
41
|
-
yield
|
|
42
|
-
ensure
|
|
43
|
-
release
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def release
|
|
49
|
-
return unless @file
|
|
50
|
-
|
|
51
|
-
@file.flock(File::LOCK_UN)
|
|
52
|
-
@file.close
|
|
53
|
-
@file = nil
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def read_holder_safely
|
|
57
|
-
content = File.read(@path, MAX_HOLDER_BYTES)
|
|
58
|
-
content.gsub(/[^[:print:]\t ]/, "").strip
|
|
59
|
-
rescue StandardError
|
|
60
|
-
"unknown"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
data/lib/textus/port/clock.rb
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Port
|
|
3
|
-
# The wall clock. An instantiable class (ADR 0109) — uniform with the other
|
|
4
|
-
# ports; `now` reads the system time. Callers that need a fixed time still
|
|
5
|
-
# pass it as data via `Call#now`.
|
|
6
|
-
class Clock
|
|
7
|
-
def now = Time.now
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Port
|
|
5
|
-
# Publishes built artifacts from the store to repo-relative consumer paths.
|
|
6
|
-
# Publish = copy + sentinel. The in-store file is already the consumer-shaped
|
|
7
|
-
# artifact; no parsing or stripping.
|
|
8
|
-
#
|
|
9
|
-
# Sentinel I/O is delegated to Textus::Port::SentinelStore. Sentinels live
|
|
10
|
-
# under `<store_root>/.run/sentinels/` (runtime, git-ignored — ADR 0070) and
|
|
11
|
-
# mirror the target's repo-relative layout so consumer directories aren't
|
|
12
|
-
# polluted with `.textus-managed.json` siblings.
|
|
13
|
-
#
|
|
14
|
-
# An instantiable class (ADR 0109).
|
|
15
|
-
class Publisher
|
|
16
|
-
def publish(source:, target:, store_root:, provenance_source: source)
|
|
17
|
-
FileUtils.mkdir_p(File.dirname(target))
|
|
18
|
-
guard_clobber(source, target, store_root)
|
|
19
|
-
File.delete(target) if File.symlink?(target)
|
|
20
|
-
FileUtils.cp(source, target)
|
|
21
|
-
Textus::Port::SentinelStore.new.write!(target: target, source: provenance_source, store_root: store_root)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Removes a previously-published file and its sentinel. No-op unless the
|
|
25
|
-
# target is textus-managed — never deletes an unmanaged file.
|
|
26
|
-
def unpublish(target:, store_root:)
|
|
27
|
-
return unless managed?(target, store_root)
|
|
28
|
-
|
|
29
|
-
FileUtils.rm_f(target)
|
|
30
|
-
sentinel = Textus::Port::SentinelStore.new.sentinel_path(target, store_root)
|
|
31
|
-
FileUtils.rm_f(sentinel)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
# Refuse to clobber an unmanaged target — EXCEPT adopt one whose bytes
|
|
37
|
-
# already equal the source (ADR 0050: a migration copies files into the
|
|
38
|
-
# store and publishes them back to where they already live, so the target
|
|
39
|
-
# is byte-identical — nothing is at risk). Adoption writes no sentinel
|
|
40
|
-
# here; the normal publish path below does, and the cp is a content no-op.
|
|
41
|
-
# An unmanaged target whose content DIFFERS, or any unmanaged symlink, is
|
|
42
|
-
# still refused — that is the guard's real job.
|
|
43
|
-
def guard_clobber(source, target, store_root)
|
|
44
|
-
return unless File.exist?(target) || File.symlink?(target)
|
|
45
|
-
return if managed?(target, store_root)
|
|
46
|
-
return if adoptable?(source, target)
|
|
47
|
-
|
|
48
|
-
raise PublishError.new("refusing to clobber unmanaged file at #{target}", target: target)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def adoptable?(source, target)
|
|
52
|
-
!File.symlink?(target) && File.file?(target) && FileUtils.identical?(source, target)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def managed?(target, store_root)
|
|
56
|
-
File.exist?(Textus::Port::SentinelStore.new.sentinel_path(target, store_root))
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
@@ -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::Core::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::Geometry.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::Geometry.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,26 +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
|
-
def read(path) = File.binread(path)
|
|
10
|
-
|
|
11
|
-
def write(path, bytes)
|
|
12
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
13
|
-
File.binwrite(path, bytes)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Raises Errno::ENOENT if absent — mirrors File.delete and matches the
|
|
17
|
-
# semantics used by Store::Writer (which guards with File.exist? first).
|
|
18
|
-
def delete(path) = File.delete(path)
|
|
19
|
-
|
|
20
|
-
def exists?(path) = File.exist?(path)
|
|
21
|
-
|
|
22
|
-
def etag(path) = Value::Etag.for_file(path)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|