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,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Manifest
|
|
5
|
-
module Schema
|
|
6
|
-
# Orchestrates structural validation (dry-schema Contract) then cross-field
|
|
7
|
-
# semantic checks (Semantics). Public interface unchanged: Validator.validate!(raw).
|
|
8
|
-
module Validator
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
def validate!(raw)
|
|
12
|
-
raise BadManifest.new("manifest must be a hash") unless raw.is_a?(Hash)
|
|
13
|
-
|
|
14
|
-
# Root unknown-key check before Contract so it fires even when lanes: is empty.
|
|
15
|
-
Semantics.walk(raw, ROOT_KEYS, "$")
|
|
16
|
-
|
|
17
|
-
result = Contract.call(raw)
|
|
18
|
-
raise BadManifest.new(format_first_error(result.errors.messages)) unless result.success?
|
|
19
|
-
|
|
20
|
-
raise BadManifest.new("manifest must declare lanes:") if Array(raw["lanes"]).empty?
|
|
21
|
-
|
|
22
|
-
Semantics.check!(raw)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Format the first dry-schema error to match the legacy path-prefixed style:
|
|
26
|
-
# "unknown key 'x' at '$.lanes[0]'" for extra-key errors;
|
|
27
|
-
# "manifest structure error at <path>: <msg>" for type/value errors.
|
|
28
|
-
def format_first_error(messages)
|
|
29
|
-
msg = messages.first
|
|
30
|
-
return "manifest structure error: unknown" unless msg
|
|
31
|
-
|
|
32
|
-
parent = format_path(msg.path[0..-2])
|
|
33
|
-
key = msg.path.last
|
|
34
|
-
|
|
35
|
-
if msg.text == "is not allowed"
|
|
36
|
-
"unknown key '#{key}' at '#{parent}'"
|
|
37
|
-
else
|
|
38
|
-
"manifest structure error at #{format_path(msg.path)}: #{msg.text}"
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def format_path(parts)
|
|
43
|
-
"$" + Array(parts).map { |p| p.is_a?(Integer) ? "[#{p}]" : ".#{p}" }.join
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
module Schema
|
|
4
|
-
# The closed coordination vocabulary (ADR 0028; five in 0033; unified in
|
|
5
|
-
# 0034; the quarantine + derived ZONE-KINDS folded into one `machine` kind
|
|
6
|
-
# in ADR 0091). Each kind pairs with the capability that authorizes
|
|
7
|
-
# originating bytes in it. ONE source of truth; the derived constants below
|
|
8
|
-
# cannot drift. A BIJECTION again (0090 had two kinds → the converge capability; 0091
|
|
9
|
-
# collapses them, so kind ↔ capability is 1:1).
|
|
10
|
-
module Vocabulary
|
|
11
|
-
LANES = {
|
|
12
|
-
"canon" => "author",
|
|
13
|
-
"workspace" => "keep",
|
|
14
|
-
"machine" => "converge",
|
|
15
|
-
"queue" => "propose",
|
|
16
|
-
"raw" => "ingest",
|
|
17
|
-
}.freeze
|
|
18
|
-
|
|
19
|
-
LANE_KINDS = LANES.keys.freeze
|
|
20
|
-
CAPABILITIES = LANES.values.uniq.freeze
|
|
21
|
-
KIND_REQUIRES_VERB = LANES
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
# The manifest schema. Its data is split across Schema::Vocabulary (the
|
|
4
|
-
# coordination vocabulary) and Schema::Keys (key whitelists + FIELD_REGISTRY)
|
|
5
|
-
# as of ADR 0109; the validation walk lives in Schema::Validator (ADR 0107).
|
|
6
|
-
# The constants are re-exported here so callers keep saying `Schema::LANES`.
|
|
7
|
-
module Schema
|
|
8
|
-
# Re-export the vocabulary.
|
|
9
|
-
LANES = Vocabulary::LANES
|
|
10
|
-
LANE_KINDS = Vocabulary::LANE_KINDS
|
|
11
|
-
CAPABILITIES = Vocabulary::CAPABILITIES
|
|
12
|
-
KIND_REQUIRES_VERB = Vocabulary::KIND_REQUIRES_VERB
|
|
13
|
-
# Re-export the keys + registry.
|
|
14
|
-
ROOT_KEYS = Keys::ROOT_KEYS
|
|
15
|
-
ROLE_KEYS = Keys::ROLE_KEYS
|
|
16
|
-
LANE_KEYS = Keys::LANE_KEYS
|
|
17
|
-
ENTRY_KEYS = Keys::ENTRY_KEYS
|
|
18
|
-
PUBLISH_KEYS = Keys::PUBLISH_KEYS
|
|
19
|
-
SOURCE_KEYS = Keys::SOURCE_KEYS
|
|
20
|
-
RETENTION_KEYS = Keys::RETENTION_KEYS
|
|
21
|
-
AUDIT_KEYS = Keys::AUDIT_KEYS
|
|
22
|
-
FIELD_REGISTRY = Keys::FIELD_REGISTRY
|
|
23
|
-
RULE_KEYS = Keys::RULE_KEYS
|
|
24
|
-
OWNER_SUBJECT_PATTERN = Keys::OWNER_SUBJECT_PATTERN
|
|
25
|
-
|
|
26
|
-
# Public entry point — the validation walk lives in Schema::Validator (ADR 0107).
|
|
27
|
-
def self.validate!(raw) = Validator.validate!(raw)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
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/orchestration.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Orchestration
|
|
3
|
-
ListKeysQuery = Data.define(:prefix, :lane)
|
|
4
|
-
MoveKeyCommand = Data.define(:old_key, :new_key, :if_etag, :dry_run)
|
|
5
|
-
DeleteKeyCommand = Data.define(:key, :if_etag)
|
|
6
|
-
AuditQuery = Data.define(:seq_since, :key, :lane, :role, :verb, :since, :correlation_id, :limit)
|
|
7
|
-
|
|
8
|
-
def initialize(list_keys:, move_key:, delete_key:, audit_entries:)
|
|
9
|
-
@list_keys = list_keys
|
|
10
|
-
@move_key = move_key
|
|
11
|
-
@delete_key = delete_key
|
|
12
|
-
@audit_entries = audit_entries
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def list_keys(prefix:, lane:, call:)
|
|
16
|
-
query = ListKeysQuery.new(prefix: prefix, lane: lane)
|
|
17
|
-
normalize(@list_keys.call(query, call), key: "rows")
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def move_key(old_key:, new_key:, call:, if_etag: nil, dry_run: false)
|
|
21
|
-
command = MoveKeyCommand.new(old_key: old_key, new_key: new_key, if_etag: if_etag, dry_run: dry_run)
|
|
22
|
-
normalize(@move_key.call(command, call), key: "move")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def delete_key(key:, call:, if_etag: nil)
|
|
26
|
-
command = DeleteKeyCommand.new(key: key, if_etag: if_etag)
|
|
27
|
-
normalize(@delete_key.call(command, call), key: "delete")
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# rubocop:disable Metrics/ParameterLists
|
|
31
|
-
def audit_entries(call:, seq_since: nil, key: nil, lane: nil, role: nil, verb: nil, since: nil, correlation_id: nil, limit: nil)
|
|
32
|
-
query = AuditQuery.new(
|
|
33
|
-
seq_since: seq_since,
|
|
34
|
-
key: key,
|
|
35
|
-
lane: lane,
|
|
36
|
-
role: role,
|
|
37
|
-
verb: verb,
|
|
38
|
-
since: since,
|
|
39
|
-
correlation_id: correlation_id,
|
|
40
|
-
limit: limit,
|
|
41
|
-
)
|
|
42
|
-
normalize(@audit_entries.call(query, call), key: "rows")
|
|
43
|
-
end
|
|
44
|
-
# rubocop:enable Metrics/ParameterLists
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def normalize(result, key:)
|
|
49
|
-
return result unless result.is_a?(Value::Result)
|
|
50
|
-
return result if result.failure?
|
|
51
|
-
|
|
52
|
-
Value::Result.success({ key => result.value })
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
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 = nil, layout: nil, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP)
|
|
18
|
-
@geometry = layout || Textus::Store::Layout.new(root)
|
|
19
|
-
@path = @geometry.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
|
-
@geometry.audit_rotated_log_path(n)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def rotated_meta(n)
|
|
144
|
-
@geometry.audit_rotated_meta_path(n)
|
|
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(@geometry.audit_log_glob)
|
|
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::Layout.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
|
data/lib/textus/port/sentinel.rb
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
require "digest"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Port
|
|
5
|
-
class Sentinel
|
|
6
|
-
attr_reader :target, :source, :sha256, :mode
|
|
7
|
-
|
|
8
|
-
def initialize(target:, source:, sha256:, mode:)
|
|
9
|
-
@target = target
|
|
10
|
-
@source = source
|
|
11
|
-
@sha256 = sha256
|
|
12
|
-
@mode = mode
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def orphan?(file_stat) = @target.nil? || !file_stat.exists?(@target)
|
|
16
|
-
|
|
17
|
-
def drift?(file_stat)
|
|
18
|
-
return false if orphan?(file_stat)
|
|
19
|
-
return false if @sha256.nil?
|
|
20
|
-
|
|
21
|
-
Digest::SHA256.hexdigest(file_stat.read(@target)) != @sha256
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|