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,80 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Write
|
|
4
|
-
class MoveKey
|
|
5
|
-
def initialize(container:, manifest:)
|
|
6
|
-
@container = container
|
|
7
|
-
@manifest = manifest
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def call(command, call)
|
|
11
|
-
Textus::Manifest::Data.validate_key!(command.old_key)
|
|
12
|
-
Textus::Manifest::Data.validate_key!(command.new_key)
|
|
13
|
-
|
|
14
|
-
return Value::Result.failure(:usage_error, "mv: old and new keys are identical") if command.old_key == command.new_key
|
|
15
|
-
|
|
16
|
-
old_res = @manifest.resolver.resolve(command.old_key)
|
|
17
|
-
new_res = @manifest.resolver.resolve(command.new_key)
|
|
18
|
-
|
|
19
|
-
reader = Store::Entry::Reader.from(container: @container)
|
|
20
|
-
|
|
21
|
-
unless reader.exists?(command.old_key)
|
|
22
|
-
return Value::Result.failure(:not_found,
|
|
23
|
-
"source key '#{command.old_key}' not found")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
zone_check = validate_zone(old_res.entry, new_res.entry)
|
|
27
|
-
return zone_check if zone_check
|
|
28
|
-
|
|
29
|
-
if reader.exists?(command.new_key)
|
|
30
|
-
return Value::Result.failure(:usage_error, "mv: target '#{command.new_key}' already exists at #{new_res.path}")
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
pre_env = reader.read(command.old_key)
|
|
34
|
-
writer = Store::Entry::Writer.from(container: @container, call: call)
|
|
35
|
-
unless pre_env.uid
|
|
36
|
-
writer.put(
|
|
37
|
-
command.old_key, mentry: old_res.entry,
|
|
38
|
-
payload: Textus::Value::Payload.new(meta: pre_env.meta, body: pre_env.body, content: pre_env.content)
|
|
39
|
-
)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
if command.dry_run
|
|
43
|
-
return Value::Result.success({
|
|
44
|
-
"protocol" => Textus::PROTOCOL, "ok" => true, "dry_run" => true,
|
|
45
|
-
"from_key" => command.old_key, "to_key" => command.new_key,
|
|
46
|
-
"from_path" => old_res.path, "to_path" => new_res.path,
|
|
47
|
-
"uid" => pre_env.uid
|
|
48
|
-
})
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
envelope = writer.move(
|
|
52
|
-
from_key: command.old_key, to_key: command.new_key,
|
|
53
|
-
new_mentry: new_res.entry
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
Value::Result.success({
|
|
57
|
-
"protocol" => Textus::PROTOCOL, "ok" => true,
|
|
58
|
-
"from_key" => command.old_key, "to_key" => command.new_key,
|
|
59
|
-
"from_path" => old_res.path, "to_path" => new_res.path,
|
|
60
|
-
"uid" => envelope.uid, "envelope" => envelope.to_h_for_wire
|
|
61
|
-
})
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def validate_zone(old_mentry, new_mentry)
|
|
67
|
-
if old_mentry.lane != new_mentry.lane
|
|
68
|
-
return Value::Result.failure(:usage_error,
|
|
69
|
-
"mv: cross-zone refused (#{old_mentry.lane} -> #{new_mentry.lane}). Use put+delete.")
|
|
70
|
-
end
|
|
71
|
-
if old_mentry.format != new_mentry.format
|
|
72
|
-
return Value::Result.failure(:usage_error,
|
|
73
|
-
"mv: format mismatch (#{old_mentry.format} -> #{new_mentry.format}); refusing.")
|
|
74
|
-
end
|
|
75
|
-
nil
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Write
|
|
4
|
-
class ProposeEntry
|
|
5
|
-
def initialize(container:)
|
|
6
|
-
@container = container
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(command, call)
|
|
10
|
-
zone = @container.manifest.policy.propose_lane_for(call.role)
|
|
11
|
-
unless zone
|
|
12
|
-
return Value::Result.failure(:propose_forbidden,
|
|
13
|
-
"role '#{call.role}' has no writable propose_lane",
|
|
14
|
-
details: { "role" => call.role })
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
key = "#{zone}.#{command.key}"
|
|
18
|
-
mentry = @container.manifest.resolver.resolve(key).entry
|
|
19
|
-
writer = Store::Entry::Writer.from(container: @container, call: call)
|
|
20
|
-
envelope = writer.put(
|
|
21
|
-
key, mentry: mentry,
|
|
22
|
-
payload: Textus::Value::Payload.new(meta: command.meta || {}, body: command.body, content: command.content)
|
|
23
|
-
)
|
|
24
|
-
Value::Result.success(envelope)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Write
|
|
4
|
-
class PutEntry
|
|
5
|
-
def initialize(container:)
|
|
6
|
-
@container = container
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(command, call)
|
|
10
|
-
Textus::Manifest::Data.validate_key!(command.key)
|
|
11
|
-
mentry = @container.manifest.resolver.resolve(command.key).entry
|
|
12
|
-
|
|
13
|
-
writer = Store::Entry::Writer.from(container: @container, call: call)
|
|
14
|
-
envelope = writer.put(
|
|
15
|
-
command.key,
|
|
16
|
-
mentry: mentry,
|
|
17
|
-
payload: Textus::Value::Payload.new(
|
|
18
|
-
meta: command.meta,
|
|
19
|
-
body: command.body,
|
|
20
|
-
content: command.content,
|
|
21
|
-
),
|
|
22
|
-
if_etag: command.if_etag,
|
|
23
|
-
)
|
|
24
|
-
Value::Result.success(envelope)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Handlers
|
|
3
|
-
module Write
|
|
4
|
-
class RejectProposal
|
|
5
|
-
def initialize(container:)
|
|
6
|
-
@container = container
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(command, call)
|
|
10
|
-
mentry = @container.manifest.resolver.resolve(command.pending_key).entry
|
|
11
|
-
unless mentry.in_proposal_lane?(@container.manifest.policy)
|
|
12
|
-
return Value::Result.failure(:proposal_error,
|
|
13
|
-
"reject: '#{command.pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
reader = Store::Entry::Reader.from(container: @container)
|
|
17
|
-
env = reader.read(command.pending_key)
|
|
18
|
-
proposal = env&.meta&.dig("proposal") or
|
|
19
|
-
return Value::Result.failure(:proposal_error, "entry has no proposal block: #{command.pending_key}")
|
|
20
|
-
target_key = proposal["target_key"]
|
|
21
|
-
|
|
22
|
-
writer = Store::Entry::Writer.from(container: @container, call: call)
|
|
23
|
-
writer.delete(command.pending_key, mentry: mentry)
|
|
24
|
-
Value::Result.success("protocol" => Textus::PROTOCOL, "rejected" => command.pending_key, "target_key" => target_key)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
data/lib/textus/init.rb
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "pathname"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module Init
|
|
6
|
-
ZONES = %w[knowledge scratchpad proposals artifacts raw].freeze
|
|
7
|
-
|
|
8
|
-
DEFAULT_MANIFEST = <<~YAML
|
|
9
|
-
version: textus/4
|
|
10
|
-
roles:
|
|
11
|
-
- { name: human, can: [author, propose] }
|
|
12
|
-
- { name: agent, can: [propose, keep] }
|
|
13
|
-
- { name: automation, can: [converge] }
|
|
14
|
-
lanes:
|
|
15
|
-
- { name: knowledge, kind: canon, desc: "the maintained source of truth (identity.* lives here)" }
|
|
16
|
-
- { name: scratchpad, kind: workspace, owner: agent, desc: "the agent's own durable working notes" }
|
|
17
|
-
- { name: proposals, kind: queue, desc: "changes awaiting your accept" }
|
|
18
|
-
- { name: artifacts, kind: machine, desc: "machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)" }
|
|
19
|
-
entries:
|
|
20
|
-
- { key: knowledge.identity, path: data/knowledge/identity.md, lane: knowledge, schema: null, owner: human:self, kind: leaf }
|
|
21
|
-
- { key: knowledge.notes, path: data/knowledge/notes, lane: knowledge, schema: null, owner: human:self, nested: true, kind: nested }
|
|
22
|
-
- { key: scratchpad.notes, path: data/scratchpad/notes, lane: scratchpad, schema: null, owner: agent:self, nested: true, kind: nested }
|
|
23
|
-
- { key: proposals.notes, path: data/proposals/notes, lane: proposals, schema: null, owner: agent:self, nested: true, kind: nested }
|
|
24
|
-
# A per-host snapshot, populated by a registered workflow. Nested so it
|
|
25
|
-
# grows to a fleet — add leaves over SSH without renaming. tracked:false →
|
|
26
|
-
# gitignored (machine info can be sensitive/noisy) but still protocol-readable
|
|
27
|
-
# via `textus get artifacts.feeds.machines.local`. Delete to opt out. (ADR 0043)
|
|
28
|
-
- key: artifacts.feeds.machines
|
|
29
|
-
path: data/artifacts/feeds/machines
|
|
30
|
-
lane: artifacts
|
|
31
|
-
format: yaml
|
|
32
|
-
nested: true
|
|
33
|
-
tracked: false
|
|
34
|
-
kind: nested
|
|
35
|
-
rules: []
|
|
36
|
-
YAML
|
|
37
|
-
|
|
38
|
-
AGENT_ENTRIES = <<~YAML.gsub(/^/, " ")
|
|
39
|
-
- { key: knowledge.project, path: data/knowledge/project.md, lane: knowledge, schema: project, owner: human:self, kind: leaf }
|
|
40
|
-
- { key: knowledge.runbooks, path: data/knowledge/runbooks, lane: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
|
|
41
|
-
- key: artifacts.derived.orientation
|
|
42
|
-
path: data/artifacts/derived/orientation.json
|
|
43
|
-
lane: artifacts
|
|
44
|
-
publish:
|
|
45
|
-
- { to: CLAUDE.md, template: orientation.erb, inject_boot: true }
|
|
46
|
-
- { to: AGENTS.md, template: orientation.erb, inject_boot: true }
|
|
47
|
-
kind: produced
|
|
48
|
-
YAML
|
|
49
|
-
|
|
50
|
-
def self.run(target_root, with_agent: false)
|
|
51
|
-
check_target!(target_root)
|
|
52
|
-
scaffold_dir = File.expand_path("init/templates", __dir__)
|
|
53
|
-
create_directories(target_root)
|
|
54
|
-
write_manifest(target_root, with_agent:)
|
|
55
|
-
mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
56
|
-
setup_state_dirs(target_root)
|
|
57
|
-
write_gitignore(target_root)
|
|
58
|
-
build_result(target_root, with_agent:, mcp_status:)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def self.check_target!(target_root)
|
|
62
|
-
raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
ASSET_ZONES = %w[raw].freeze
|
|
66
|
-
|
|
67
|
-
def self.create_directories(target_root)
|
|
68
|
-
FileUtils.mkdir_p(File.join(target_root, "schemas"))
|
|
69
|
-
FileUtils.mkdir_p(File.join(target_root, "templates"))
|
|
70
|
-
FileUtils.mkdir_p(File.join(target_root, "workflows"))
|
|
71
|
-
ZONES.each do |z|
|
|
72
|
-
dir = File.join(target_root, "data", z)
|
|
73
|
-
FileUtils.mkdir_p(dir)
|
|
74
|
-
File.write(File.join(dir, ".gitkeep"), "")
|
|
75
|
-
end
|
|
76
|
-
ASSET_ZONES.each do |z|
|
|
77
|
-
dir = File.join(target_root, "assets", z)
|
|
78
|
-
FileUtils.mkdir_p(dir)
|
|
79
|
-
File.write(File.join(dir, ".gitkeep"), "")
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.write_manifest(target_root, with_agent:)
|
|
84
|
-
File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
88
|
-
return nil unless with_agent
|
|
89
|
-
|
|
90
|
-
scaffold_agent_profile(target_root, scaffold_dir)
|
|
91
|
-
write_mcp_config(target_root, scaffold_dir)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.setup_state_dirs(target_root)
|
|
95
|
-
FileUtils.mkdir_p(Textus::Store::Layout.new(target_root).audit_dir_path)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def self.write_gitignore(target_root)
|
|
99
|
-
File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def self.build_result(target_root, with_agent:, mcp_status:)
|
|
103
|
-
result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
|
|
104
|
-
result["mcp_config"] = mcp_status if with_agent
|
|
105
|
-
result
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Composes the agent profile by inserting AGENT_ENTRIES immediately before the
|
|
109
|
-
# top-level `rules:` block of DEFAULT_MANIFEST — that block is load-bearing for
|
|
110
|
-
# this `.sub`; removing it from DEFAULT_MANIFEST would silently drop the entries.
|
|
111
|
-
def self.manifest_yaml(with_agent:)
|
|
112
|
-
return DEFAULT_MANIFEST unless with_agent
|
|
113
|
-
|
|
114
|
-
DEFAULT_MANIFEST.sub(/^rules:/, "#{AGENT_ENTRIES}rules:")
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Copies the proven orientation bundle into a freshly-init'd store.
|
|
118
|
-
def self.scaffold_agent_profile(target_root, scaffold_dir)
|
|
119
|
-
{
|
|
120
|
-
"project.schema.yaml" => File.join("schemas", "project.yaml"),
|
|
121
|
-
"runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
|
|
122
|
-
"orientation.erb" => File.join("templates", "orientation.erb"),
|
|
123
|
-
}.each do |src, dest|
|
|
124
|
-
File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# The one file init writes outside .textus/: a starter .mcp.json at the
|
|
129
|
-
# project root. Write-once — never clobber a hand-authored config. The
|
|
130
|
-
# command form assumes a gem-installed `textus` on PATH; the user owns
|
|
131
|
-
# the file after this first write.
|
|
132
|
-
def self.write_mcp_config(target_root, scaffold_dir)
|
|
133
|
-
dest = File.join(File.dirname(target_root), ".mcp.json")
|
|
134
|
-
return "skipped" if File.exist?(dest)
|
|
135
|
-
|
|
136
|
-
File.write(dest, File.read(File.join(scaffold_dir, "mcp.json")))
|
|
137
|
-
"written"
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# The store's `.gitignore` is generated, never hand-kept (ADR 0038), and now
|
|
141
|
-
# derived from the manifest: the run subtree plus every `tracked: false`
|
|
142
|
-
# entry's resolved path (ADR 0043).
|
|
143
|
-
def self.derived_gitignore(target_root)
|
|
144
|
-
manifest = Textus::Manifest.load(target_root)
|
|
145
|
-
root = Pathname.new(target_root)
|
|
146
|
-
untracked = manifest.data.entries.reject(&:tracked?).map do |e|
|
|
147
|
-
if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* → data/artifacts/feeds/machines/)
|
|
148
|
-
rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
|
|
149
|
-
"#{rel}/"
|
|
150
|
-
else
|
|
151
|
-
Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
Textus::Store::Layout.new(target_root).gitignore_body(untracked_entries: untracked)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
data/lib/textus/jobs.rb
DELETED
data/lib/textus/key/distance.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Key
|
|
3
|
-
# Small utilities for ranking key suggestions. Bounded inputs only —
|
|
4
|
-
# Levenshtein is O(n*m) so we refuse to compute on long strings.
|
|
5
|
-
class Distance
|
|
6
|
-
MAX_LEN = 200
|
|
7
|
-
|
|
8
|
-
# Length of the shared dot-separated prefix between two dotted keys.
|
|
9
|
-
def self.shared_prefix_segments(left, right)
|
|
10
|
-
asegs = left.split(".")
|
|
11
|
-
bsegs = right.split(".")
|
|
12
|
-
n = [asegs.length, bsegs.length].min
|
|
13
|
-
i = 0
|
|
14
|
-
i += 1 while i < n && asegs[i] == bsegs[i]
|
|
15
|
-
i
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Classic iterative Levenshtein with two rows. Bounded to MAX_LEN.
|
|
19
|
-
def self.levenshtein(left, right)
|
|
20
|
-
return nil if left.length > MAX_LEN || right.length > MAX_LEN
|
|
21
|
-
return right.length if left.empty?
|
|
22
|
-
return left.length if right.empty?
|
|
23
|
-
|
|
24
|
-
prev = (0..right.length).to_a
|
|
25
|
-
curr = Array.new(right.length + 1, 0)
|
|
26
|
-
(1..left.length).each do |i|
|
|
27
|
-
curr[0] = i
|
|
28
|
-
(1..right.length).each do |j|
|
|
29
|
-
cost = left[i - 1] == right[j - 1] ? 0 : 1
|
|
30
|
-
curr[j] = [
|
|
31
|
-
curr[j - 1] + 1, # insertion
|
|
32
|
-
prev[j] + 1, # deletion
|
|
33
|
-
prev[j - 1] + cost, # substitution
|
|
34
|
-
].min
|
|
35
|
-
end
|
|
36
|
-
prev, curr = curr, prev
|
|
37
|
-
end
|
|
38
|
-
prev[right.length]
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Rank candidate keys against requested. Returns up to `limit` keys.
|
|
42
|
-
# Sort: longer shared prefix first; then smaller Levenshtein distance.
|
|
43
|
-
def self.suggest(requested, candidates, limit: 5)
|
|
44
|
-
return [] if requested.nil? || requested.empty?
|
|
45
|
-
|
|
46
|
-
scored = candidates.first(200).map do |k|
|
|
47
|
-
prefix = shared_prefix_segments(requested, k)
|
|
48
|
-
dist = levenshtein(requested, k) || Float::INFINITY
|
|
49
|
-
[k, prefix, dist]
|
|
50
|
-
end
|
|
51
|
-
scored.sort_by { |(_, prefix, dist)| [-prefix, dist] }.first(limit).map(&:first)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
data/lib/textus/key/grammar.rb
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Key
|
|
3
|
-
module Grammar
|
|
4
|
-
SEGMENT = /\A[a-z0-9][a-z0-9-]*\z/
|
|
5
|
-
MAX_SEGMENTS = 8
|
|
6
|
-
MAX_SEGMENT_LEN = 64
|
|
7
|
-
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def validate!(key) # rubocop:disable Naming/PredicateMethod
|
|
11
|
-
raise UsageError.new("key must be a String") unless key.is_a?(String)
|
|
12
|
-
raise UsageError.new("empty key") if key.empty?
|
|
13
|
-
|
|
14
|
-
segs = key.split(".")
|
|
15
|
-
raise UsageError.new("key '#{key}' has #{segs.length} segments (max #{MAX_SEGMENTS})") if segs.length > MAX_SEGMENTS
|
|
16
|
-
|
|
17
|
-
segs.each do |seg|
|
|
18
|
-
if seg.empty?
|
|
19
|
-
raise UsageError.new("empty segment in key '#{key}'")
|
|
20
|
-
elsif seg.length > MAX_SEGMENT_LEN
|
|
21
|
-
raise UsageError.new("segment '#{seg}' in key '#{key}' exceeds #{MAX_SEGMENT_LEN} chars")
|
|
22
|
-
elsif !seg.match?(SEGMENT)
|
|
23
|
-
raise UsageError.new(
|
|
24
|
-
"invalid key segment '#{seg}' in '#{key}': must match [a-z0-9][a-z0-9-]* " \
|
|
25
|
-
"(lowercase, digits, hyphens; no underscores or uppercase)",
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
true
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
data/lib/textus/key/matching.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Key
|
|
3
|
-
# Dotted-key scope matching, shared by all prefix-scoped sweeps
|
|
4
|
-
# (WS4 / ADR 0089-era cleanup). Canonicalised here so every consumer
|
|
5
|
-
# uses a consistent dotted-boundary check with proper Nested ancestor
|
|
6
|
-
# handling. ADR 0093: Produce is the sole engine calling this.
|
|
7
|
-
module Matching
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
# Is `key` within the `prefix` scope?
|
|
11
|
-
# - exact match, or a dotted descendant (the `prefix.` boundary, so
|
|
12
|
-
# prefix "art" does NOT match key "artifacts"), and
|
|
13
|
-
# - for a nested entry, also when `prefix` descends INTO it — the nested
|
|
14
|
-
# parent owns the leaf the prefix names (e.g. prefix
|
|
15
|
-
# "feeds.machines.host1" still selects the nested entry
|
|
16
|
-
# "feeds.machines").
|
|
17
|
-
def matches_prefix?(key, prefix, nested: false)
|
|
18
|
-
return true if key == prefix || key.start_with?("#{prefix}.")
|
|
19
|
-
|
|
20
|
-
nested && prefix.start_with?("#{key}.")
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
data/lib/textus/key/path.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Key
|
|
3
|
-
module Path
|
|
4
|
-
# Returns the absolute filesystem path for a manifest entry (the leaf file,
|
|
5
|
-
# not a nested directory). Adds the format's primary extension when the
|
|
6
|
-
# manifest entry's `path:` is extensionless.
|
|
7
|
-
#
|
|
8
|
-
# The first argument is a Manifest::Data (or anything responding to .root);
|
|
9
|
-
# callers historically passed the whole Manifest but should now pass
|
|
10
|
-
# `manifest.data`.
|
|
11
|
-
def self.resolve(data, mentry)
|
|
12
|
-
primary_ext = Format.for(mentry.format).extensions.first
|
|
13
|
-
rel_path = normalize_relative_path(mentry.path)
|
|
14
|
-
if File.extname(mentry.path) == ""
|
|
15
|
-
File.join(data.root, rel_path + primary_ext)
|
|
16
|
-
else
|
|
17
|
-
File.join(data.root, rel_path)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.normalize_relative_path(path)
|
|
22
|
-
return path if path.start_with?("data/")
|
|
23
|
-
|
|
24
|
-
File.join("data", path)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
# Resolves a manifest's `roles:` block (or the absence of one) into a
|
|
4
|
-
# capability map: { role_name => [verbs] }. Verbs are a subset of the
|
|
5
|
-
# closed capability set (Schema::CAPABILITIES). See ADR 0030.
|
|
6
|
-
module Capabilities
|
|
7
|
-
# Fallback role set for a manifest that omits `roles:` entirely. Agent
|
|
8
|
-
# is intentionally minimal here (`propose` only) — narrower than the
|
|
9
|
-
# `textus init` scaffold, which declares `agent: [propose, keep]` so the
|
|
10
|
-
# default `scratchpad` workspace is writable. A roles-less manifest that
|
|
11
|
-
# declares a `kind: workspace` zone is therefore rejected at load (no
|
|
12
|
-
# `keep`-holder); declare `roles:` to opt into a workspace lane (ADR 0033).
|
|
13
|
-
DEFAULT_MAPPING = {
|
|
14
|
-
Textus::Value::Role::HUMAN => %w[author propose].freeze,
|
|
15
|
-
Textus::Value::Role::AGENT => %w[propose].freeze,
|
|
16
|
-
Textus::Value::Role::AUTOMATION => %w[converge].freeze,
|
|
17
|
-
}.freeze
|
|
18
|
-
|
|
19
|
-
# Returns { role_name => [verbs] }. When `roles:` is declared we use
|
|
20
|
-
# exactly that; defaults are *not* layered in (declaring roles is an
|
|
21
|
-
# opt-in to a fully user-defined vocabulary).
|
|
22
|
-
def self.resolve(raw_roles)
|
|
23
|
-
return DEFAULT_MAPPING if raw_roles.nil?
|
|
24
|
-
|
|
25
|
-
raw_roles.to_h { |r| [r["name"], Array(r["can"]).freeze] }.freeze
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
data/lib/textus/manifest/data.rb
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
require_relative "schema"
|
|
2
|
-
require_relative "capabilities"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
class Manifest
|
|
6
|
-
# Immutable, parsed view of a manifest YAML document.
|
|
7
|
-
#
|
|
8
|
-
# Holds raw structural data (lanes, entries, audit_config, role_caps)
|
|
9
|
-
# but no behaviour beyond accessors. Behaviour (lane authority, key
|
|
10
|
-
# resolution, rules) lives on Manifest::Policy / Resolver / Rules.
|
|
11
|
-
class Data
|
|
12
|
-
AUDIT_DEFAULTS = { max_size: 10_485_760, keep: 5 }.freeze
|
|
13
|
-
WORKER_DEFAULTS = { pool: 4, poll: 5, lease_ttl: 60, max_attempts: 3 }.freeze
|
|
14
|
-
|
|
15
|
-
attr_reader :raw, :root, :entries, :declared_lane_kinds,
|
|
16
|
-
:lane_descs, :lane_owners,
|
|
17
|
-
:audit_config, :worker_config, :role_caps, :policy
|
|
18
|
-
|
|
19
|
-
def self.validate_key!(key)
|
|
20
|
-
raise UsageError.new("empty key") if key.nil? || key.empty?
|
|
21
|
-
|
|
22
|
-
Key::Grammar.validate!(key)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Forwarder used by Resolver and Entry classes that received a Data
|
|
26
|
-
# but were written against the historical Manifest API.
|
|
27
|
-
def validate_key!(key) = self.class.validate_key!(key)
|
|
28
|
-
|
|
29
|
-
def self.parse(raw, root:)
|
|
30
|
-
raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare lanes:") if Array(raw["lanes"]).empty?
|
|
31
|
-
|
|
32
|
-
Schema.validate!(raw)
|
|
33
|
-
new(raw: raw, root: root)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def initialize(raw:, root:)
|
|
37
|
-
@raw = raw
|
|
38
|
-
@root = root
|
|
39
|
-
# Write authority is derived from capabilities × lane-kind (ADR 0030),
|
|
40
|
-
# not a per-lane writer list. "Which lanes are declared" lives in the
|
|
41
|
-
# one kind-keyed map below (declared_lane_kinds); membership checks by
|
|
42
|
-
# read-side callers (boot, maintenance/data_mv) use its keyset (ADR 0034).
|
|
43
|
-
@declared_lane_kinds = Array(raw["lanes"]).to_h do |z|
|
|
44
|
-
[z["name"], z["kind"]&.to_sym]
|
|
45
|
-
end
|
|
46
|
-
@lane_descs = Array(raw["lanes"]).to_h { |z| [z["name"], z["desc"]] }
|
|
47
|
-
# Only lanes that actually declare an owner — keep nil-tombstones out so a
|
|
48
|
-
# future `lane_owners.key?(name)` means "owner declared", not "lane exists".
|
|
49
|
-
@lane_owners = Array(raw["lanes"]).to_h { |z| [z["name"], z["owner"]] }.compact
|
|
50
|
-
@audit_config = build_audit_config(raw)
|
|
51
|
-
@worker_config = build_worker_config(raw)
|
|
52
|
-
@role_caps = Capabilities.resolve(raw["roles"])
|
|
53
|
-
# Policy is constructed before entries because Entry validators
|
|
54
|
-
# use the entry's own `derived?` and similar helpers that call into
|
|
55
|
-
# Policy; Policy must exist before entries are built.
|
|
56
|
-
@policy = Policy.new(self)
|
|
57
|
-
@entries = build_entries(raw)
|
|
58
|
-
validate_declared_keys!
|
|
59
|
-
freeze
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def build_audit_config(raw)
|
|
65
|
-
a = raw["audit"] || {}
|
|
66
|
-
{
|
|
67
|
-
max_size: a["max_size"] || AUDIT_DEFAULTS[:max_size],
|
|
68
|
-
keep: a["keep"] || AUDIT_DEFAULTS[:keep],
|
|
69
|
-
}.freeze
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Worker/queue tunables (ADR: job-queue execution model). All optional;
|
|
73
|
-
# the daemon (serve) and batch drain read these, falling back to defaults
|
|
74
|
-
# so a manifest with no `worker:` block runs the queue out of the box.
|
|
75
|
-
def build_worker_config(raw)
|
|
76
|
-
w = raw["worker"] || {}
|
|
77
|
-
{
|
|
78
|
-
pool: w["pool"] || WORKER_DEFAULTS[:pool],
|
|
79
|
-
poll: w["poll"] || WORKER_DEFAULTS[:poll],
|
|
80
|
-
lease_ttl: w["lease_ttl"] || WORKER_DEFAULTS[:lease_ttl],
|
|
81
|
-
max_attempts: w["max_attempts"] || WORKER_DEFAULTS[:max_attempts],
|
|
82
|
-
}.freeze
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def build_entries(raw)
|
|
86
|
-
Array(raw["entries"]).map do |e|
|
|
87
|
-
entry = Manifest::Entry::Parser.call(e)
|
|
88
|
-
Manifest::Entry::Validators.run_all(entry, policy: @policy)
|
|
89
|
-
entry
|
|
90
|
-
end.freeze
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def validate_declared_keys!
|
|
94
|
-
@entries.each do |e|
|
|
95
|
-
raise UsageError.new("empty key") if e.key.nil? || e.key.empty?
|
|
96
|
-
|
|
97
|
-
Key::Grammar.validate!(e.key)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|