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,105 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
class Base < Entry
|
|
5
|
-
attr_reader :raw, :key, :path, :lane, :schema, :owner, :format, :publish_targets
|
|
6
|
-
|
|
7
|
-
# rubocop:disable Lint/MissingSuper
|
|
8
|
-
def initialize(raw:, key:, path:, lane:, schema:, owner:, format:, publish_targets: [])
|
|
9
|
-
@raw = raw
|
|
10
|
-
@key = key
|
|
11
|
-
@path = path
|
|
12
|
-
@lane = lane
|
|
13
|
-
@schema = schema
|
|
14
|
-
@owner = owner
|
|
15
|
-
@format = format
|
|
16
|
-
@publish_targets = Array(publish_targets)
|
|
17
|
-
end
|
|
18
|
-
# rubocop:enable Lint/MissingSuper
|
|
19
|
-
|
|
20
|
-
def lane_writers(policy)
|
|
21
|
-
verb = policy.verb_for_lane(@lane)
|
|
22
|
-
policy.roles_with_capability(verb)
|
|
23
|
-
rescue UsageError => e
|
|
24
|
-
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def in_proposal_lane?(policy) = policy.queue_lane?(@lane)
|
|
28
|
-
|
|
29
|
-
def nested? = false
|
|
30
|
-
def derived? = false
|
|
31
|
-
def intake? = false
|
|
32
|
-
def leaf? = false
|
|
33
|
-
|
|
34
|
-
# Production traits. Default false on Base (a leaf/intake entry is neither
|
|
35
|
-
# an out-of-band command nor a projection); Produced overrides both from
|
|
36
|
-
# its source. Lets publish modes call these without a `respond_to?` guard.
|
|
37
|
-
def external? = false
|
|
38
|
-
def projection? = false
|
|
39
|
-
|
|
40
|
-
alias zone lane
|
|
41
|
-
alias in_proposal_zone? in_proposal_lane?
|
|
42
|
-
|
|
43
|
-
# Whether git should track this entry's file. Default true; an entry
|
|
44
|
-
# marked `tracked: false` in the manifest stays protocol-readable but is
|
|
45
|
-
# listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
|
|
46
|
-
# reads from raw here rather than threading through every constructor.
|
|
47
|
-
def tracked? = @raw["tracked"] != false
|
|
48
|
-
|
|
49
|
-
# Single source of truth is @publish_targets (ADR 0094). These
|
|
50
|
-
# derive the ADR-0049/0052 views the publish modes consume.
|
|
51
|
-
def publish_to = @publish_targets.select(&:to_target?).map(&:to)
|
|
52
|
-
def publish_tree = @publish_targets.find(&:tree_target?)&.tree
|
|
53
|
-
|
|
54
|
-
# Nil stubs for cross-cutting optional attrs. Subclasses override the
|
|
55
|
-
# ones they own. Validators and serializers can call these directly
|
|
56
|
-
# without `respond_to?` guards.
|
|
57
|
-
def events = {}
|
|
58
|
-
def ignore = []
|
|
59
|
-
|
|
60
|
-
# Per-entry ignore (ADR 0042). Base entries enumerate no tree, so
|
|
61
|
-
# nothing is ever ignored; Nested overrides with real patterns.
|
|
62
|
-
def ignored?(_rel_path) = false
|
|
63
|
-
|
|
64
|
-
# Minimal context object passed into entry `publish_via` hooks.
|
|
65
|
-
# Everything beyond the three primitives is derived.
|
|
66
|
-
PublishContext = ::Data.define(:container, :call, :reader) do
|
|
67
|
-
def manifest = container.manifest
|
|
68
|
-
def root = container.root
|
|
69
|
-
def repo_root = File.dirname(container.root)
|
|
70
|
-
|
|
71
|
-
# No-op: event bus removed in workflow redesign; callers that fire
|
|
72
|
-
# :entry_published / :entry_produced remain unchanged in the source.
|
|
73
|
-
def emit(_event, **_payload) = nil
|
|
74
|
-
|
|
75
|
-
# Read a named template from the store's templates/ directory.
|
|
76
|
-
# Raises TemplateError when the file doesn't exist.
|
|
77
|
-
def read_template(name)
|
|
78
|
-
path = container.layout.template_path(name)
|
|
79
|
-
unless File.exist?(path)
|
|
80
|
-
raise Textus::TemplateError.new(
|
|
81
|
-
"template '#{name}' not found",
|
|
82
|
-
template_name: name,
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
File.read(path)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# ADR 0049: an entry resolves, once, to one Publish::* mode that owns its
|
|
90
|
-
# publish algorithm. A plain entry publishes via ToPaths (publish_to) or
|
|
91
|
-
# None; Nested resolves among the key/path-driven modes. Derived
|
|
92
|
-
# overrides publish_via to materialize first.
|
|
93
|
-
def publish_mode
|
|
94
|
-
@publish_mode ||= Publish.resolve(self)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Returns: { kind: :built|:leaves, value: ... } to be accumulated by
|
|
98
|
-
# Write::Build, or nil to skip.
|
|
99
|
-
def publish_via(pctx, prefix: nil)
|
|
100
|
-
publish_mode.publish(pctx, prefix: prefix)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
# Pure glob matcher backing per-entry `ignore:` patterns (ADR 0042).
|
|
5
|
-
# `rel_path` is the slash-joined path of a candidate file relative to the
|
|
6
|
-
# entry's base directory.
|
|
7
|
-
#
|
|
8
|
-
# Matching is segment-wise so the `**` globstar means "zero or more path
|
|
9
|
-
# segments" — `File.fnmatch` alone cannot express this (under
|
|
10
|
-
# FNM_PATHNAME a trailing `**` will not cross a `/`; without it a leading
|
|
11
|
-
# `**/` will not match zero leading segments). So `**/node_modules/**`
|
|
12
|
-
# catches the `node_modules` subtree at any depth, including the store
|
|
13
|
-
# root, and the directory entry itself.
|
|
14
|
-
#
|
|
15
|
-
# Within a single segment, matching delegates to `File.fnmatch` with
|
|
16
|
-
# FNM_EXTGLOB, so a single `*` is anchored to that segment (it does not
|
|
17
|
-
# cross `/`) and `{a,b}` alternation works.
|
|
18
|
-
module IgnoreMatcher
|
|
19
|
-
SEGMENT_FLAGS = File::FNM_EXTGLOB
|
|
20
|
-
|
|
21
|
-
def self.match?(patterns, rel_path)
|
|
22
|
-
path_segs = rel_path.split("/").reject(&:empty?)
|
|
23
|
-
Array(patterns).any? do |pat|
|
|
24
|
-
match_segments(pat.split("/").reject(&:empty?), path_segs)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Classic globstar matcher. `**` matches zero or more whole segments;
|
|
29
|
-
# any other pattern segment matches exactly one path segment via fnmatch.
|
|
30
|
-
def self.match_segments(pat_segs, path_segs)
|
|
31
|
-
return path_segs.empty? if pat_segs.empty?
|
|
32
|
-
|
|
33
|
-
if pat_segs.first == "**"
|
|
34
|
-
match_segments(pat_segs[1..], path_segs) ||
|
|
35
|
-
(!path_segs.empty? && match_segments(pat_segs, path_segs[1..]))
|
|
36
|
-
else
|
|
37
|
-
!path_segs.empty? &&
|
|
38
|
-
File.fnmatch?(pat_segs.first, path_segs.first, SEGMENT_FLAGS) &&
|
|
39
|
-
match_segments(pat_segs[1..], path_segs[1..])
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
private_class_method :match_segments
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
# A directory entry: enumerates a tree of leaves and resolves to one
|
|
5
|
-
# publish mode (ADR 0049). The publish algorithms themselves live in
|
|
6
|
-
# Entry::Publish::* — Nested is just the value (attributes + ignore
|
|
7
|
-
# predicate) those modes read.
|
|
8
|
-
class Nested < Base
|
|
9
|
-
attr_reader :ignore
|
|
10
|
-
|
|
11
|
-
def initialize(ignore: nil, **rest)
|
|
12
|
-
super(**rest)
|
|
13
|
-
@ignore = Array(ignore)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def nested? = true
|
|
17
|
-
|
|
18
|
-
# True when `rel_path` (slash-joined, relative to the entry base) matches
|
|
19
|
-
# any configured ignore glob. Evaluated ABOVE key-legality (ADR 0042):
|
|
20
|
-
# an ignored path is excluded, never judged.
|
|
21
|
-
def ignored?(rel_path) = IgnoreMatcher.match?(@ignore, rel_path)
|
|
22
|
-
|
|
23
|
-
KIND = :nested
|
|
24
|
-
|
|
25
|
-
def self.from_raw(common, raw)
|
|
26
|
-
# publish_tree is derived from publish_targets (ADR 0094) via Base#publish_tree
|
|
27
|
-
new(
|
|
28
|
-
ignore: raw["ignore"],
|
|
29
|
-
**common,
|
|
30
|
-
)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
Entry::REGISTRY[KIND] = self
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Parser
|
|
5
|
-
def self.call(raw)
|
|
6
|
-
key = raw["key"] or raise UsageError.new("manifest entry missing key")
|
|
7
|
-
lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
|
|
8
|
-
|
|
9
|
-
raw_kind = raw["kind"] or raise BadManifest.new("entry '#{key}' missing required `kind:` (#{Entry::REGISTRY.keys.join("|")})")
|
|
10
|
-
kind = raw_kind.to_sym
|
|
11
|
-
if %i[derived intake].include?(kind)
|
|
12
|
-
raise BadManifest.new(
|
|
13
|
-
"entry '#{key}': kind: #{kind} was collapsed into `kind: produced` (ADR 0095) — " \
|
|
14
|
-
"the produce method is `source.from` (#{kind == :intake ? "handler" : "project|command"})",
|
|
15
|
-
)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
explicit_path = raw["path"]
|
|
19
|
-
format = resolve_format(raw, explicit_path)
|
|
20
|
-
path = explicit_path || derive_path(key, kind, format)
|
|
21
|
-
|
|
22
|
-
common = {
|
|
23
|
-
raw: raw,
|
|
24
|
-
key: key, path: path, lane: lane,
|
|
25
|
-
schema: raw["schema"], owner: raw["owner"],
|
|
26
|
-
format: format,
|
|
27
|
-
publish_targets: publish_targets(raw)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
klass = Entry::REGISTRY[kind] or
|
|
31
|
-
raise BadManifest.new("entry '#{key}': unknown kind: #{kind.inspect} (known: #{Entry::REGISTRY.keys.join(", ")})")
|
|
32
|
-
klass.from_raw(common, raw)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Parse the optional `source:` block. Returns nil when absent (workflow
|
|
36
|
-
# produced entries register their produce logic in .textus/workflows/).
|
|
37
|
-
def self.parse_source(raw, _key)
|
|
38
|
-
block = raw["source"]
|
|
39
|
-
return nil if block.nil?
|
|
40
|
-
|
|
41
|
-
Textus::Manifest::Policy::Source.new(block)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# ADR 0094: `publish:` is a LIST of target objects — to-targets
|
|
45
|
-
# [{to, template?, inject_boot?}] and/or a tree-target [{tree}]. The
|
|
46
|
-
# ADR-0052 map forms ({to: […]} / {tree: …}) are retired.
|
|
47
|
-
def self.publish_targets(raw)
|
|
48
|
-
block = raw["publish"]
|
|
49
|
-
return [] if block.nil?
|
|
50
|
-
|
|
51
|
-
unless block.is_a?(Array)
|
|
52
|
-
raise BadManifest.new(
|
|
53
|
-
"entry '#{raw["key"]}': `publish:` must be a list of targets " \
|
|
54
|
-
"[{to:, template:?} | {tree:}] (ADR 0094); the `publish: { … }` map form was retired",
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
block.map { |t| Textus::Manifest::Policy::PublishTarget.new(t) }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def self.resolve_format(raw, path)
|
|
61
|
-
declared = raw["format"]
|
|
62
|
-
|
|
63
|
-
return declared || "markdown" if path.nil? || path.empty?
|
|
64
|
-
|
|
65
|
-
ext = File.extname(path)
|
|
66
|
-
inferred = Textus::Format.infer_from_extension(ext)
|
|
67
|
-
|
|
68
|
-
if declared.nil?
|
|
69
|
-
return inferred if inferred
|
|
70
|
-
|
|
71
|
-
return "markdown"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
raise UsageError.new("entry '#{raw["key"]}': unknown format #{declared.inspect}") unless Textus::Format.formats.include?(declared)
|
|
75
|
-
|
|
76
|
-
if ext != "" && inferred && inferred != declared
|
|
77
|
-
raise UsageError.new(
|
|
78
|
-
"entry '#{raw["key"]}': path extension #{ext.inspect} does not match declared format #{declared.inspect}",
|
|
79
|
-
)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
declared
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Derives the manifest-relative path from key + kind + format.
|
|
86
|
-
# Key::Path.normalize_relative_path will prepend data/ at resolution time.
|
|
87
|
-
def self.derive_path(key, kind_sym, format)
|
|
88
|
-
dir_path = key.split(".").join("/")
|
|
89
|
-
return dir_path if kind_sym == :nested
|
|
90
|
-
|
|
91
|
-
ext = Textus::Format.for(format).extensions.first
|
|
92
|
-
"#{dir_path}#{ext}"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
private_class_method :derive_path
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
# A produced entry — `kind: produced` with an optional `source:` block.
|
|
5
|
-
# When `source:` is present it must be `from: external` (out-of-band
|
|
6
|
-
# generator; textus detects drift but never runs it). When absent the
|
|
7
|
-
# entry is produced by a workflow file in .textus/workflows/.
|
|
8
|
-
class Produced < Base
|
|
9
|
-
attr_reader :source
|
|
10
|
-
|
|
11
|
-
def initialize(source:, **rest)
|
|
12
|
-
super(**rest)
|
|
13
|
-
@source = source
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def external? = @source&.external? || false
|
|
17
|
-
def nested? = !!@raw["nested"]
|
|
18
|
-
|
|
19
|
-
KIND = :produced
|
|
20
|
-
|
|
21
|
-
# Publish existing store bytes via the shared publish mode (Publish::ToPaths
|
|
22
|
-
# or Publish::None). Workflow runners handle the produce step; this method
|
|
23
|
-
# only publishes whatever bytes are already on disk.
|
|
24
|
-
def publish_via(pctx, prefix: nil)
|
|
25
|
-
publish_mode.publish(pctx, prefix: prefix)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.from_raw(common, raw)
|
|
29
|
-
new(source: Parser.parse_source(raw, common[:key]), **common)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
Entry::REGISTRY[KIND] = self
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Publish
|
|
5
|
-
# Base for every publish mode: wraps the resolved entry and owns the one
|
|
6
|
-
# repo-root escape guard the writing modes share (ADR 0049). Subclasses
|
|
7
|
-
# implement `#publish(pctx, prefix:)` returning the existing
|
|
8
|
-
# `{ kind:, value:, pruned: }` shape (or nil), and `#validate!` for the
|
|
9
|
-
# per-mode shape rules reached *because* this mode resolved.
|
|
10
|
-
class Mode
|
|
11
|
-
def initialize(entry)
|
|
12
|
-
@entry = entry
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
attr_reader :entry
|
|
16
|
-
|
|
17
|
-
# No shape rules by default — ToPaths/None publish without templating.
|
|
18
|
-
def validate!; end
|
|
19
|
-
|
|
20
|
-
# Whether this entry's subtree files are opaque payload that must
|
|
21
|
-
# never be enumerated as keys. Only Tree (publish_tree, ADR 0047)
|
|
22
|
-
# overrides to true; doctor's IllegalKeys and the resolver consult
|
|
23
|
-
# this so they stop key-walking a keyless mirror's files.
|
|
24
|
-
def keyless? = false
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
# Expand `rel` under repo_root and confirm it stays inside it.
|
|
29
|
-
def repo_abs(pctx, rel)
|
|
30
|
-
File.expand_path(File.join(pctx.repo_root, rel))
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def inside_repo?(pctx, abs)
|
|
34
|
-
abs.start_with?(File.expand_path(pctx.repo_root) + File::SEPARATOR)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Store-side directory this entry's tree lives under.
|
|
38
|
-
def store_base(pctx)
|
|
39
|
-
File.join(pctx.root, Textus::Key::Path.normalize_relative_path(entry.path))
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Publish
|
|
5
|
-
# An entry with no publish_* key — nothing to publish.
|
|
6
|
-
class None < Mode
|
|
7
|
-
def publish(_pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
8
|
-
nil
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Publish
|
|
5
|
-
# ADR 0049: the one walk->publish->prune pipeline behind Tree (whole-entry
|
|
6
|
-
# mirror, ADR 0047). It was once shared with the per-leaf publish_each
|
|
7
|
-
# mode too; ADR 0051 removed publish_each, leaving Tree the only caller.
|
|
8
|
-
# The `walk_root`/`prune_honors_ignore:` parameters survive from that
|
|
9
|
-
# shared shape — Tree always walks at `base` and honors `ignore` in the
|
|
10
|
-
# prune (ADR 0047 D4, so a derived index in the mirrored dir survives).
|
|
11
|
-
class SubtreeMirror
|
|
12
|
-
def initialize(entry, pctx, publisher: Textus::Port::Publisher.new)
|
|
13
|
-
@entry = entry
|
|
14
|
-
@pctx = pctx
|
|
15
|
-
@publisher = publisher
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# base: store dir the entry owns — the root `ignored?` globs are
|
|
19
|
-
# relative to (ADR 0042).
|
|
20
|
-
# walk_root: dir the glob is rooted at (== base for Tree). dst paths
|
|
21
|
-
# mirror rel-to-walk_root.
|
|
22
|
-
# target_dir: repo-side destination root.
|
|
23
|
-
# key/envelope: emitted per file; envelope is nil for the keyless Tree.
|
|
24
|
-
# prune_honors_ignore: when true a managed file the entry `ignore`s
|
|
25
|
-
# survives the prune (ADR 0047 D4 — lets a derived index live in the
|
|
26
|
-
# mirrored dir); when false every unwritten managed file is pruned.
|
|
27
|
-
def mirror(base:, walk_root:, target_dir:, key:, envelope:, prune_honors_ignore:)
|
|
28
|
-
return { written: [], pruned: [] } unless File.directory?(walk_root)
|
|
29
|
-
|
|
30
|
-
written = publish_files(base: base, walk_root: walk_root, target_dir: target_dir, key: key, envelope: envelope)
|
|
31
|
-
{ written: written, pruned: prune(target_dir, written, prune_honors_ignore) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def publish_files(base:, walk_root:, target_dir:, key:, envelope:)
|
|
37
|
-
# FNM_DOTMATCH includes dotfiles; File.file? below skips dirs (and
|
|
38
|
-
# symlinks-to-dirs). Trees are authored content, not symlink graphs.
|
|
39
|
-
Dir.glob(File.join(walk_root, "**", "*"), File::FNM_DOTMATCH).sort.filter_map do |src|
|
|
40
|
-
next nil unless File.file?(src)
|
|
41
|
-
next nil if @entry.ignored?(relative(src, base))
|
|
42
|
-
|
|
43
|
-
dst = File.join(target_dir, relative(src, walk_root))
|
|
44
|
-
@publisher.publish(source: src, target: dst, store_root: @pctx.root)
|
|
45
|
-
@pctx.emit(:entry_published, key: key, envelope: envelope, source: src, target: dst)
|
|
46
|
-
{ "key" => key, "source" => src, "target" => dst }
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Scoped to target_dir. Safe across leaves because ADR 0046 D5
|
|
51
|
-
# (shallowest-index-wins) keeps leaf target dirs non-nesting, so
|
|
52
|
-
# targets_under can't reach another leaf's sentinels.
|
|
53
|
-
def prune(target_dir, written, honor_ignore)
|
|
54
|
-
kept = written.map { |w| File.expand_path(w["target"]) }
|
|
55
|
-
store = Textus::Port::SentinelStore.new
|
|
56
|
-
store.targets_under(target_dir, @pctx.root).filter_map do |managed|
|
|
57
|
-
abs = File.expand_path(managed)
|
|
58
|
-
next nil if kept.include?(abs)
|
|
59
|
-
next nil if honor_ignore && @entry.ignored?(relative(abs, target_dir))
|
|
60
|
-
|
|
61
|
-
@publisher.unpublish(target: managed, store_root: @pctx.root)
|
|
62
|
-
managed
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def relative(path, root)
|
|
67
|
-
path.sub(%r{\A#{Regexp.escape(root)}/}, "")
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Publish
|
|
5
|
-
# Template-variable detection for publish targets. The only surviving
|
|
6
|
-
# use after ADR 0051 (which removed publish_each and its `{leaf}`/
|
|
7
|
-
# `{basename}`/`{key}`/`{ext}` vocabulary) is Tree.validate!, which uses
|
|
8
|
-
# VAR_RE to reject any `{var}` in a publish_tree value — that key names a
|
|
9
|
-
# single directory by plain path and interprets no variables.
|
|
10
|
-
module Template
|
|
11
|
-
VAR_RE = /\{([a-z]+)\}/
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
require "tempfile"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Manifest
|
|
5
|
-
class Entry
|
|
6
|
-
module Publish
|
|
7
|
-
# publish.to: render or copy the entry's stored data to each fixed repo path.
|
|
8
|
-
# The behaviour of any entry that declares `publish: [{ to: ... }, ...]`.
|
|
9
|
-
# ADR 0094: iterates publish_targets (to-targets), rendering through a
|
|
10
|
-
# template when the target declares one, or copying verbatim otherwise.
|
|
11
|
-
class ToPaths < Mode
|
|
12
|
-
def initialize(entry, publisher: Textus::Port::Publisher.new)
|
|
13
|
-
super(entry)
|
|
14
|
-
@publisher = publisher
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
18
|
-
targets = entry.publish_targets.select(&:to_target?)
|
|
19
|
-
|
|
20
|
-
return nil if targets.empty?
|
|
21
|
-
|
|
22
|
-
data_path = pctx.manifest.resolver.resolve(entry.key).path
|
|
23
|
-
envelope = pctx.reader.call(entry.key)
|
|
24
|
-
renderer = Textus::Produce::Render.new(template_loader: ->(n) { pctx.read_template(n) })
|
|
25
|
-
content = nil # parsed lazily; the data's `content` (always _meta-free)
|
|
26
|
-
|
|
27
|
-
targets.each do |t|
|
|
28
|
-
if t.renders?
|
|
29
|
-
content ||= Textus::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
|
|
30
|
-
publish_bytes(render_bytes(t, content, renderer, pctx), entry.key, t, pctx, data_path, envelope)
|
|
31
|
-
elsif strip_meta?(entry)
|
|
32
|
-
content ||= Textus::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
|
|
33
|
-
bytes = Textus::Format.for(entry.format).serialize(meta: {}, body: "", content: content)
|
|
34
|
-
publish_bytes(bytes, entry.key, t, pctx, data_path, envelope)
|
|
35
|
-
else
|
|
36
|
-
# opaque / command / non-structured — publish the stored file as-is
|
|
37
|
-
target_abs = File.join(pctx.repo_root, t.to)
|
|
38
|
-
@publisher.publish(source: data_path, target: target_abs, store_root: pctx.root)
|
|
39
|
-
pctx.emit(:entry_published, key: entry.key, envelope: envelope, source: data_path, target: target_abs)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
{ kind: :built, value: { "key" => entry.key, "path" => data_path, "published_to" => targets.map(&:to) } }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def strip_meta?(entry)
|
|
49
|
-
%w[json yaml].include?(entry.format.to_s)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def render_bytes(target, content, renderer, pctx)
|
|
53
|
-
boot = target.inject_boot ? Textus::Boot.build(container: pctx.container) : nil
|
|
54
|
-
renderer.bytes_for(target: target, data: content, boot: boot)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Write bytes to a system temp, publish (recording the persistent data
|
|
58
|
-
# file as the sentinel source), then remove the temp — the store is
|
|
59
|
-
# never polluted with render artifacts.
|
|
60
|
-
def publish_bytes(bytes, key, target, pctx, data_path, envelope)
|
|
61
|
-
target_abs = File.join(pctx.repo_root, target.to)
|
|
62
|
-
Tempfile.create(["textus-publish", File.extname(target.to)]) do |f|
|
|
63
|
-
f.binmode
|
|
64
|
-
f.write(bytes)
|
|
65
|
-
f.flush
|
|
66
|
-
@publisher.publish(
|
|
67
|
-
source: f.path, target: target_abs, store_root: pctx.root, provenance_source: data_path,
|
|
68
|
-
)
|
|
69
|
-
end
|
|
70
|
-
pctx.emit(:entry_published, key: key, envelope: envelope, source: data_path, target: target_abs)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Publish
|
|
5
|
-
# publish_tree (ADR 0047): mirror this entry's whole subtree to one
|
|
6
|
-
# target dir by real path. No resolver, no keys — files are opaque
|
|
7
|
-
# payload (envelope nil). The prune honors `ignore` so a derived index
|
|
8
|
-
# (e.g. a SKILL.md written by a separate entry into the same dir)
|
|
9
|
-
# survives the whole-target prune (ADR 0047 D4).
|
|
10
|
-
class Tree < Mode
|
|
11
|
-
# Mirrored files are opaque payload, never addressable keys (ADR 0047).
|
|
12
|
-
def keyless? = true
|
|
13
|
-
|
|
14
|
-
def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
-
target_rel = entry.publish_tree
|
|
16
|
-
target_dir = repo_abs(pctx, target_rel)
|
|
17
|
-
unless inside_repo?(pctx, target_dir)
|
|
18
|
-
raise Textus::PublishError.new(
|
|
19
|
-
"entry '#{entry.key}': publish_tree target '#{target_rel}' escapes repo root",
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
result = SubtreeMirror.new(entry, pctx).mirror(
|
|
24
|
-
base: store_base(pctx),
|
|
25
|
-
walk_root: store_base(pctx),
|
|
26
|
-
target_dir: target_dir,
|
|
27
|
-
key: entry.key,
|
|
28
|
-
envelope: nil,
|
|
29
|
-
prune_honors_ignore: true,
|
|
30
|
-
)
|
|
31
|
-
{ kind: :leaves, value: result[:written], pruned: result[:pruned] }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def validate!
|
|
35
|
-
publish_tree = entry.publish_tree
|
|
36
|
-
raise UsageError.new("entry '#{entry.key}': publish.tree must be a string") unless publish_tree.is_a?(String)
|
|
37
|
-
|
|
38
|
-
used_vars = publish_tree.scan(Template::VAR_RE).flatten
|
|
39
|
-
return if used_vars.empty?
|
|
40
|
-
|
|
41
|
-
raise UsageError.new(
|
|
42
|
-
"entry '#{entry.key}': publish.tree names a single directory and takes no template variable(s) " \
|
|
43
|
-
"#{used_vars.map { |v| "{#{v}}" }.join(", ")} — it mirrors the whole subtree to one target dir.",
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
# ADR 0049: the publish design is a key-split concept (ADR 0047 table)
|
|
5
|
-
# realized as one resolved sum type. Each directory entry resolves, once,
|
|
6
|
-
# to one Publish::* mode that owns its publish algorithm — no nil-cascade,
|
|
7
|
-
# no pairwise exclusivity guards, one shared subtree mirror. ADR 0051
|
|
8
|
-
# removed `publish_each` (both leaf modes); ADR 0052 folded the two surviving
|
|
9
|
-
# keys into one `publish:` block (`to:` xor `tree:`). The surface is two modes:
|
|
10
|
-
#
|
|
11
|
-
# None — nothing to publish (no publish: block)
|
|
12
|
-
# ToPaths — publish: { to: [...] } — 1 stored file -> N fixed repo paths
|
|
13
|
-
# Tree — publish: { tree: "dir" } — whole entry subtree -> 1 dir, no keys
|
|
14
|
-
module Publish
|
|
15
|
-
# Resolve an entry to its single publish mode. The publish config is the
|
|
16
|
-
# ADR 0052 `publish:` block, sourced into entry.publish_to/publish_tree.
|
|
17
|
-
# Raises one UsageError if both `publish.to` and `publish.tree` are set —
|
|
18
|
-
# the block groups the two but does not make exclusivity structural, so
|
|
19
|
-
# this stays the one enforcement point (ADR 0052 D2).
|
|
20
|
-
def self.resolve(entry)
|
|
21
|
-
set = []
|
|
22
|
-
set << "publish.to" unless Array(entry.publish_to).empty?
|
|
23
|
-
set << "publish.tree" unless entry.publish_tree.nil?
|
|
24
|
-
|
|
25
|
-
if set.length > 1
|
|
26
|
-
raise Textus::UsageError.new(
|
|
27
|
-
"entry '#{entry.key}': #{set.join(" and ")} are mutually exclusive — an entry publishes exactly one way",
|
|
28
|
-
)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
mode_for(entry, set.first)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.mode_for(entry, key)
|
|
35
|
-
case key
|
|
36
|
-
when "publish.to" then ToPaths.new(entry)
|
|
37
|
-
when "publish.tree" then Tree.new(entry)
|
|
38
|
-
else None.new(entry)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
private_class_method :mode_for
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|