textus 0.51.0 → 0.53.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 +37 -0
- data/README.md +74 -66
- data/SPEC.md +100 -223
- data/docs/architecture/README.md +91 -80
- data/docs/reference/conventions.md +8 -8
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/background/retention/apply.rb +50 -0
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +59 -49
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +13 -13
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +19 -11
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +40 -36
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +93 -76
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +21 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +30 -15
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +4 -4
- data/lib/textus/{domain → manifest}/policy/source.rb +27 -28
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +18 -12
- data/lib/textus/manifest/schema/validator.rb +27 -25
- data/lib/textus/manifest/schema/vocabulary.rb +3 -3
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +3 -3
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +23 -21
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +17 -63
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/ports/build_lock.rb +1 -1
- data/lib/textus/ports/queue/job.rb +65 -0
- data/lib/textus/ports/queue.rb +130 -0
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/role.rb +1 -1
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -4
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +133 -107
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -52
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/reconcile.rb +0 -160
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -69
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
data/lib/textus/init.rb
CHANGED
|
@@ -10,32 +10,32 @@ module Textus
|
|
|
10
10
|
roles:
|
|
11
11
|
- { name: human, can: [author, propose] }
|
|
12
12
|
- { name: agent, can: [propose, keep] }
|
|
13
|
-
- { name: automation, can: [
|
|
14
|
-
|
|
13
|
+
- { name: automation, can: [converge] }
|
|
14
|
+
lanes:
|
|
15
15
|
- { name: knowledge, kind: canon, desc: "the maintained source of truth (identity.* lives here)" }
|
|
16
16
|
- { name: notebook, kind: workspace, owner: agent, desc: "the agent's own durable working notes" }
|
|
17
17
|
- { name: proposals, kind: queue, desc: "changes awaiting your accept" }
|
|
18
18
|
- { name: artifacts, kind: machine, desc: "machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)" }
|
|
19
19
|
entries:
|
|
20
|
-
- { key: knowledge.identity, path: knowledge/identity.md,
|
|
21
|
-
- { key: knowledge.notes, path: knowledge/notes,
|
|
22
|
-
- { key: notebook.notes, path: notebook/notes,
|
|
23
|
-
- { key: proposals.notes, path: proposals/notes,
|
|
24
|
-
# A per-host snapshot, refreshed from its declared intake by `textus
|
|
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: notebook.notes, path: data/notebook/notes, lane: notebook, 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, refreshed from its declared intake by `textus drain` (scheduled, or on demand).
|
|
25
25
|
# Nested so it grows to a fleet — add artifacts.feeds.machines.<host> leaves over SSH
|
|
26
26
|
# (see docs/cookbook/environment-scan.md) without renaming. tracked:false →
|
|
27
27
|
# gitignored (machine info can be sensitive/noisy) but still protocol-readable
|
|
28
28
|
# via `textus get artifacts.feeds.machines.local`. Delete to opt out. (ADR 0043)
|
|
29
29
|
- key: artifacts.feeds.machines
|
|
30
|
-
path: artifacts/feeds/machines
|
|
31
|
-
|
|
30
|
+
path: data/artifacts/feeds/machines
|
|
31
|
+
lane: artifacts
|
|
32
32
|
format: yaml
|
|
33
33
|
nested: true
|
|
34
34
|
tracked: false
|
|
35
35
|
kind: produced
|
|
36
36
|
source:
|
|
37
|
-
from:
|
|
38
|
-
handler:
|
|
37
|
+
from: fetch
|
|
38
|
+
handler: machine-intake
|
|
39
39
|
ttl: 1h # cadence on a long-running server
|
|
40
40
|
config:
|
|
41
41
|
machines:
|
|
@@ -43,59 +43,42 @@ module Textus
|
|
|
43
43
|
rules: []
|
|
44
44
|
YAML
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
#
|
|
46
|
+
STEPS_README = <<~MD
|
|
47
|
+
# Steps
|
|
48
48
|
|
|
49
|
-
Drop one Ruby file per
|
|
50
|
-
Files
|
|
51
|
-
startup
|
|
52
|
-
only — the registered event and name come from the DSL call, not the file path.
|
|
49
|
+
Drop one Ruby file per step. Steps are discovered by convention.
|
|
50
|
+
Files under `.textus/steps/<kind>/<name>.rb` are loaded at
|
|
51
|
+
startup and registered.
|
|
53
52
|
|
|
54
|
-
##
|
|
53
|
+
## Conventions
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
The directory name (`<kind>`) must be one of:
|
|
56
|
+
- `fetch`: Acquires data from outside the store.
|
|
57
|
+
- `transform`: Reshapes projection rows.
|
|
58
|
+
- `validate`: Validates data before writing.
|
|
59
|
+
- `observe`: Listens to store events.
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
The filename (`<name>.rb`) defines the step name. The class defined
|
|
62
|
+
in the file must be a subclass of `Textus::Step::<Kind>` (e.g.
|
|
63
|
+
`Textus::Step::Fetch`) and be wrapped in the `Textus::Step` module.
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
## Example
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
module Textus
|
|
69
|
+
module Step
|
|
70
|
+
class MyFetch < Fetch
|
|
71
|
+
def call(config:, args:, caps:, **)
|
|
72
|
+
{ content: { "foo" => "bar" } }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
77
|
```
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
`source:` block declares the handler and its refresh cadence
|
|
75
|
-
(`ttl`). Age GC (drop/archive) lives in a top-level `retention:`
|
|
76
|
-
rule, not on the entry:
|
|
77
|
-
|
|
78
|
-
```yaml
|
|
79
|
-
entries:
|
|
80
|
-
- key: artifacts.feeds.foo
|
|
81
|
-
kind: produced
|
|
82
|
-
path: artifacts/feeds/foo.md
|
|
83
|
-
zone: artifacts
|
|
84
|
-
source:
|
|
85
|
-
from: handler
|
|
86
|
-
handler: my_source
|
|
87
|
-
ttl: 10m # refresh cadence for this intake
|
|
88
|
-
|
|
89
|
-
rules:
|
|
90
|
-
- match: artifacts.feeds.foo
|
|
91
|
-
retention:
|
|
92
|
-
ttl: 30d
|
|
93
|
-
action: archive # drop | archive (age GC of stored rows)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Events: :resolve_handler, :transform_rows, :validate (rpc — return value used)
|
|
79
|
+
Events: :fetch, :transform, :validate (rpc — return value used)
|
|
97
80
|
:entry_written, :entry_deleted, :entry_fetched, :entry_renamed,
|
|
98
|
-
:entry_produced, :produce_failed,
|
|
81
|
+
:entry_produced, :produce_failed,
|
|
99
82
|
:proposal_accepted, :proposal_rejected,
|
|
100
83
|
:entry_published, :store_loaded, :session_opened,
|
|
101
84
|
:entry_fetch_started, :entry_fetch_failed (pub-sub — return discarded)
|
|
@@ -104,50 +87,83 @@ module Textus
|
|
|
104
87
|
MD
|
|
105
88
|
|
|
106
89
|
AGENT_ENTRIES = <<~YAML.gsub(/^/, " ")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- { key: knowledge.project, path: knowledge/project.md, zone: knowledge, schema: project, owner: human:self, kind: leaf }
|
|
110
|
-
- { key: knowledge.runbooks, path: knowledge/runbooks, zone: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
|
|
90
|
+
- { key: knowledge.project, path: data/knowledge/project.md, lane: knowledge, schema: project, owner: human:self, kind: leaf }
|
|
91
|
+
- { key: knowledge.runbooks, path: data/knowledge/runbooks, lane: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
|
|
111
92
|
- key: artifacts.derived.orientation
|
|
112
|
-
path: artifacts/derived/orientation.json
|
|
113
|
-
|
|
93
|
+
path: data/artifacts/derived/orientation.json
|
|
94
|
+
lane: artifacts
|
|
114
95
|
publish:
|
|
115
96
|
- { to: CLAUDE.md, template: orientation.mustache, inject_boot: true }
|
|
116
97
|
- { to: AGENTS.md, template: orientation.mustache, inject_boot: true }
|
|
117
98
|
source:
|
|
118
|
-
from:
|
|
99
|
+
from: derive
|
|
119
100
|
select:
|
|
120
101
|
- knowledge.project
|
|
121
102
|
- knowledge.runbooks
|
|
122
|
-
transform:
|
|
103
|
+
transform: orientation
|
|
123
104
|
kind: produced
|
|
124
105
|
YAML
|
|
125
106
|
|
|
126
107
|
def self.run(target_root, with_agent: false)
|
|
108
|
+
check_target!(target_root)
|
|
109
|
+
scaffold_dir = File.expand_path("init/templates", __dir__)
|
|
110
|
+
create_directories(target_root)
|
|
111
|
+
write_steps_readme(target_root, scaffold_dir)
|
|
112
|
+
write_manifest(target_root, with_agent:)
|
|
113
|
+
mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
114
|
+
setup_state_dirs(target_root)
|
|
115
|
+
write_gitignore(target_root)
|
|
116
|
+
build_result(target_root, with_agent:, mcp_status:)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.check_target!(target_root)
|
|
127
120
|
raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
|
|
121
|
+
end
|
|
128
122
|
|
|
123
|
+
def self.create_directories(target_root)
|
|
129
124
|
FileUtils.mkdir_p(File.join(target_root, "schemas"))
|
|
130
125
|
FileUtils.mkdir_p(File.join(target_root, "templates"))
|
|
131
|
-
FileUtils.mkdir_p(File.join(target_root, "
|
|
126
|
+
FileUtils.mkdir_p(File.join(target_root, "steps/fetch"))
|
|
127
|
+
FileUtils.mkdir_p(File.join(target_root, "steps/transform"))
|
|
128
|
+
FileUtils.mkdir_p(File.join(target_root, "steps/validate"))
|
|
129
|
+
FileUtils.mkdir_p(File.join(target_root, "steps/observe"))
|
|
132
130
|
ZONES.each do |z|
|
|
133
|
-
dir = File.join(target_root, "
|
|
131
|
+
dir = File.join(target_root, "data", z)
|
|
134
132
|
FileUtils.mkdir_p(dir)
|
|
135
133
|
File.write(File.join(dir, ".gitkeep"), "")
|
|
136
134
|
end
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.write_steps_readme(target_root, scaffold_dir)
|
|
138
|
+
File.write(File.join(target_root, "steps/README.md"), STEPS_README)
|
|
139
|
+
File.write(
|
|
140
|
+
File.join(target_root, "steps/fetch/machine-intake.rb"),
|
|
141
|
+
File.read(File.join(scaffold_dir, "machine_intake.rb")),
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.write_manifest(target_root, with_agent:)
|
|
141
146
|
File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
150
|
+
return nil unless with_agent
|
|
151
|
+
|
|
152
|
+
scaffold_agent_profile(target_root, scaffold_dir)
|
|
153
|
+
write_mcp_config(target_root, scaffold_dir)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.setup_state_dirs(target_root)
|
|
147
157
|
FileUtils.mkdir_p(Textus::Layout.audit_dir(target_root))
|
|
148
158
|
FileUtils.mkdir_p(Textus::Layout.state(target_root))
|
|
149
159
|
FileUtils.mkdir_p(Textus::Layout.locks(target_root))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.write_gitignore(target_root)
|
|
150
163
|
File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.build_result(target_root, with_agent:, mcp_status:)
|
|
151
167
|
result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
|
|
152
168
|
result["mcp_config"] = mcp_status if with_agent
|
|
153
169
|
result
|
|
@@ -168,7 +184,7 @@ module Textus
|
|
|
168
184
|
"project.schema.yaml" => File.join("schemas", "project.yaml"),
|
|
169
185
|
"runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
|
|
170
186
|
"orientation.mustache" => File.join("templates", "orientation.mustache"),
|
|
171
|
-
"orientation_reducer.rb" => File.join("
|
|
187
|
+
"orientation_reducer.rb" => File.join("steps/transform", "orientation.rb"),
|
|
172
188
|
}.each do |src, dest|
|
|
173
189
|
File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
|
|
174
190
|
end
|
|
@@ -193,8 +209,9 @@ module Textus
|
|
|
193
209
|
manifest = Textus::Manifest.load(target_root)
|
|
194
210
|
root = Pathname.new(target_root)
|
|
195
211
|
untracked = manifest.data.entries.reject(&:tracked?).map do |e|
|
|
196
|
-
if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* →
|
|
197
|
-
"
|
|
212
|
+
if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* → data/artifacts/feeds/machines/)
|
|
213
|
+
rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
|
|
214
|
+
"#{rel}/"
|
|
198
215
|
else
|
|
199
216
|
Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
|
|
200
217
|
end
|
data/lib/textus/key/path.rb
CHANGED
|
@@ -10,12 +10,19 @@ module Textus
|
|
|
10
10
|
# `manifest.data`.
|
|
11
11
|
def self.resolve(data, mentry)
|
|
12
12
|
primary_ext = Entry.for_format(mentry.format).extensions.first
|
|
13
|
+
rel_path = normalize_relative_path(mentry.path)
|
|
13
14
|
if File.extname(mentry.path) == ""
|
|
14
|
-
File.join(data.root,
|
|
15
|
+
File.join(data.root, rel_path + primary_ext)
|
|
15
16
|
else
|
|
16
|
-
File.join(data.root,
|
|
17
|
+
File.join(data.root, rel_path)
|
|
17
18
|
end
|
|
18
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
|
|
19
26
|
end
|
|
20
27
|
end
|
|
21
28
|
end
|
data/lib/textus/layout.rb
CHANGED
|
@@ -4,6 +4,15 @@ module Textus
|
|
|
4
4
|
# tracked/disposable boundary is a directory boundary. ADR 0038.
|
|
5
5
|
module Layout
|
|
6
6
|
RUN = ".run"
|
|
7
|
+
DATA = "data"
|
|
8
|
+
|
|
9
|
+
def self.data(root)
|
|
10
|
+
File.join(root, DATA)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.data_lane(root, lane_name)
|
|
14
|
+
File.join(data(root), lane_name)
|
|
15
|
+
end
|
|
7
16
|
|
|
8
17
|
def self.run(root)
|
|
9
18
|
File.join(root, RUN)
|
|
@@ -25,6 +34,18 @@ module Textus
|
|
|
25
34
|
File.join(run(root), "build.lock")
|
|
26
35
|
end
|
|
27
36
|
|
|
37
|
+
def self.watcher_lock(root)
|
|
38
|
+
File.join(run(root), "watcher.lock")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.queue(root)
|
|
42
|
+
File.join(run(root), "queue")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.queue_state(root, state)
|
|
46
|
+
File.join(queue(root), state.to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
28
49
|
def self.audit_dir(root)
|
|
29
50
|
File.join(run(root), "audit")
|
|
30
51
|
end
|
|
@@ -13,7 +13,7 @@ module Textus
|
|
|
13
13
|
DEFAULT_MAPPING = {
|
|
14
14
|
Textus::Role::HUMAN => %w[author propose].freeze,
|
|
15
15
|
Textus::Role::AGENT => %w[propose].freeze,
|
|
16
|
-
Textus::Role::AUTOMATION => %w[
|
|
16
|
+
Textus::Role::AUTOMATION => %w[converge].freeze,
|
|
17
17
|
}.freeze
|
|
18
18
|
|
|
19
19
|
# Returns { role_name => [verbs] }. When `roles:` is declared we use
|
data/lib/textus/manifest/data.rb
CHANGED
|
@@ -5,15 +5,16 @@ module Textus
|
|
|
5
5
|
class Manifest
|
|
6
6
|
# Immutable, parsed view of a manifest YAML document.
|
|
7
7
|
#
|
|
8
|
-
# Holds raw structural data (
|
|
9
|
-
# but no behaviour beyond accessors. Behaviour (
|
|
8
|
+
# Holds raw structural data (lanes, entries, audit_config, role_caps)
|
|
9
|
+
# but no behaviour beyond accessors. Behaviour (lane authority, key
|
|
10
10
|
# resolution, rules) lives on Manifest::Policy / Resolver / Rules.
|
|
11
11
|
class Data
|
|
12
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
|
|
13
14
|
|
|
14
|
-
attr_reader :raw, :root, :entries, :
|
|
15
|
-
:
|
|
16
|
-
:audit_config, :role_caps, :policy
|
|
15
|
+
attr_reader :raw, :root, :entries, :declared_lane_kinds,
|
|
16
|
+
:lane_descs, :lane_owners,
|
|
17
|
+
:audit_config, :worker_config, :role_caps, :policy
|
|
17
18
|
|
|
18
19
|
def self.validate_key!(key)
|
|
19
20
|
raise UsageError.new("empty key") if key.nil? || key.empty?
|
|
@@ -26,7 +27,7 @@ module Textus
|
|
|
26
27
|
def validate_key!(key) = self.class.validate_key!(key)
|
|
27
28
|
|
|
28
29
|
def self.parse(raw, root:)
|
|
29
|
-
raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare
|
|
30
|
+
raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare lanes:") if Array(raw["lanes"]).empty?
|
|
30
31
|
|
|
31
32
|
Schema.validate!(raw)
|
|
32
33
|
new(raw: raw, root: root)
|
|
@@ -35,18 +36,19 @@ module Textus
|
|
|
35
36
|
def initialize(raw:, root:)
|
|
36
37
|
@raw = raw
|
|
37
38
|
@root = root
|
|
38
|
-
# Write authority is derived from capabilities ×
|
|
39
|
-
# not a per-
|
|
40
|
-
# one kind-keyed map below (
|
|
41
|
-
# read-side callers (boot, maintenance/
|
|
42
|
-
@
|
|
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|
|
|
43
44
|
[z["name"], z["kind"]&.to_sym]
|
|
44
45
|
end
|
|
45
|
-
@
|
|
46
|
-
# Only
|
|
47
|
-
# future `
|
|
48
|
-
@
|
|
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
|
|
49
50
|
@audit_config = build_audit_config(raw)
|
|
51
|
+
@worker_config = build_worker_config(raw)
|
|
50
52
|
@role_caps = Capabilities.resolve(raw["roles"])
|
|
51
53
|
# Policy is constructed before entries because Entry validators
|
|
52
54
|
# use the entry's own `derived?` and similar helpers that call into
|
|
@@ -67,6 +69,19 @@ module Textus
|
|
|
67
69
|
}.freeze
|
|
68
70
|
end
|
|
69
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
|
+
|
|
70
85
|
def build_entries(raw)
|
|
71
86
|
Array(raw["entries"]).map do |e|
|
|
72
87
|
entry = Manifest::Entry::Parser.call(e)
|
|
@@ -2,14 +2,14 @@ module Textus
|
|
|
2
2
|
class Manifest
|
|
3
3
|
class Entry
|
|
4
4
|
class Base < Entry
|
|
5
|
-
attr_reader :raw, :key, :path, :
|
|
5
|
+
attr_reader :raw, :key, :path, :lane, :schema, :owner, :format, :publish_targets
|
|
6
6
|
|
|
7
7
|
# rubocop:disable Metrics/ParameterLists, Lint/MissingSuper
|
|
8
|
-
def initialize(raw:, key:, path:,
|
|
8
|
+
def initialize(raw:, key:, path:, lane:, schema:, owner:, format:, publish_targets: [])
|
|
9
9
|
@raw = raw
|
|
10
10
|
@key = key
|
|
11
11
|
@path = path
|
|
12
|
-
@
|
|
12
|
+
@lane = lane
|
|
13
13
|
@schema = schema
|
|
14
14
|
@owner = owner
|
|
15
15
|
@format = format
|
|
@@ -17,13 +17,14 @@ module Textus
|
|
|
17
17
|
end
|
|
18
18
|
# rubocop:enable Metrics/ParameterLists, Lint/MissingSuper
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
policy.
|
|
20
|
+
def lane_writers(policy)
|
|
21
|
+
verb = policy.verb_for_lane(@lane)
|
|
22
|
+
policy.roles_with_capability(verb)
|
|
22
23
|
rescue UsageError => e
|
|
23
24
|
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def
|
|
27
|
+
def in_proposal_lane?(policy) = policy.queue_lane?(@lane)
|
|
27
28
|
|
|
28
29
|
def nested? = false
|
|
29
30
|
def derived? = false
|
|
@@ -36,6 +37,9 @@ module Textus
|
|
|
36
37
|
def external? = false
|
|
37
38
|
def projection? = false
|
|
38
39
|
|
|
40
|
+
alias zone lane
|
|
41
|
+
alias in_proposal_zone? in_proposal_lane?
|
|
42
|
+
|
|
39
43
|
# Whether git should track this entry's file. Default true; an entry
|
|
40
44
|
# marked `tracked: false` in the manifest stays protocol-readable but is
|
|
41
45
|
# listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
|
|
@@ -60,19 +64,19 @@ module Textus
|
|
|
60
64
|
# Minimal context object passed into entry `publish_via` hooks.
|
|
61
65
|
# Everything beyond the three primitives is derived. Data.define
|
|
62
66
|
# instances are frozen, so we recompute per-call rather than
|
|
63
|
-
# memoizing — RoleScope/
|
|
67
|
+
# memoizing — RoleScope/Step::Context construction is cheap.
|
|
64
68
|
PublishContext = ::Data.define(:container, :call, :reader) do
|
|
65
69
|
def manifest = container.manifest
|
|
66
70
|
def root = container.root
|
|
67
71
|
def repo_root = File.dirname(container.root)
|
|
68
|
-
def
|
|
72
|
+
def steps = container.steps
|
|
69
73
|
|
|
70
74
|
def hook_context
|
|
71
|
-
Textus::
|
|
75
|
+
Textus::Step::Context.new(scope: scope_for_hooks)
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def emit(event, **payload)
|
|
75
|
-
|
|
79
|
+
steps.publish(event, ctx: hook_context, **payload)
|
|
76
80
|
end
|
|
77
81
|
|
|
78
82
|
# Read a named template from the store's templates/ directory.
|
|
@@ -91,7 +95,7 @@ module Textus
|
|
|
91
95
|
private
|
|
92
96
|
|
|
93
97
|
def scope_for_hooks
|
|
94
|
-
Textus::RoleScope.new(
|
|
98
|
+
Textus::Surfaces::RoleScope.new(
|
|
95
99
|
container: container, role: call.role, dry_run: call.dry_run,
|
|
96
100
|
)
|
|
97
101
|
end
|
|
@@ -5,7 +5,7 @@ module Textus
|
|
|
5
5
|
def self.call(raw)
|
|
6
6
|
key = raw["key"] or raise UsageError.new("manifest entry missing key")
|
|
7
7
|
path = raw["path"] or raise UsageError.new("manifest entry '#{key}' missing path")
|
|
8
|
-
|
|
8
|
+
lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
|
|
9
9
|
|
|
10
10
|
raw_kind = raw["kind"] or raise BadManifest.new("entry '#{key}' missing required `kind:` (#{Entry::REGISTRY.keys.join("|")})")
|
|
11
11
|
kind = raw_kind.to_sym
|
|
@@ -19,7 +19,7 @@ module Textus
|
|
|
19
19
|
|
|
20
20
|
common = {
|
|
21
21
|
raw: raw,
|
|
22
|
-
key: key, path: path,
|
|
22
|
+
key: key, path: path, lane: lane,
|
|
23
23
|
schema: raw["schema"], owner: raw["owner"],
|
|
24
24
|
format: format,
|
|
25
25
|
publish_targets: publish_targets(raw)
|
|
@@ -31,12 +31,12 @@ module Textus
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# ADR 0093: an entry's production block is the unified `source:`. Returns a
|
|
34
|
-
#
|
|
34
|
+
# Manifest::Policy::Source; kind (intake/derived) is read from source.from.
|
|
35
35
|
def self.parse_source(raw, key)
|
|
36
36
|
block = raw["source"] or
|
|
37
|
-
raise BadManifest.new("entry '#{key}' requires a source: { from:
|
|
37
|
+
raise BadManifest.new("entry '#{key}' requires a source: { from: derive|fetch|external, ... }")
|
|
38
38
|
|
|
39
|
-
Textus::
|
|
39
|
+
Textus::Manifest::Policy::Source.new(block)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# ADR 0094: `publish:` is a LIST of target objects — to-targets
|
|
@@ -52,7 +52,7 @@ module Textus
|
|
|
52
52
|
"[{to:, template:?} | {tree:}] (ADR 0094); the `publish: { … }` map form was retired",
|
|
53
53
|
)
|
|
54
54
|
end
|
|
55
|
-
block.map { |t| Textus::
|
|
55
|
+
block.map { |t| Textus::Manifest::Policy::PublishTarget.new(t) }
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def self.resolve_format(raw, path)
|
|
@@ -17,6 +17,8 @@ module Textus
|
|
|
17
17
|
def derived? = @source.kind == :derived
|
|
18
18
|
def external? = @source.external?
|
|
19
19
|
def projection? = @source.projection?
|
|
20
|
+
def fetch? = @source.fetch?
|
|
21
|
+
def derive? = @source.derive?
|
|
20
22
|
def nested? = !!@raw["nested"]
|
|
21
23
|
def handler = @source.handler
|
|
22
24
|
def config = @source.config
|
|
@@ -33,11 +35,10 @@ module Textus
|
|
|
33
35
|
def publish_via(pctx, prefix: nil)
|
|
34
36
|
built = false
|
|
35
37
|
if projection?
|
|
36
|
-
Textus::
|
|
38
|
+
Textus::Pipeline::Acquire::Projection.new(container: pctx.container, call: pctx.call).run(self)
|
|
37
39
|
built = true
|
|
38
40
|
pctx.emit(:entry_produced, key: @key, envelope: pctx.reader.call(@key), sources: Array(@source.select).compact)
|
|
39
41
|
end
|
|
40
|
-
|
|
41
42
|
emitted = publish_mode.publish(pctx, prefix: prefix)
|
|
42
43
|
return emitted if emitted
|
|
43
44
|
return nil unless built
|
|
@@ -16,11 +16,12 @@ module Textus
|
|
|
16
16
|
|
|
17
17
|
def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/AbcSize
|
|
18
18
|
targets = entry.publish_targets.select(&:to_target?)
|
|
19
|
+
|
|
19
20
|
return nil if targets.empty?
|
|
20
21
|
|
|
21
22
|
data_path = pctx.manifest.resolver.resolve(entry.key).path
|
|
22
23
|
envelope = pctx.reader.call(entry.key)
|
|
23
|
-
renderer = Textus::
|
|
24
|
+
renderer = Textus::Pipeline::Render.new(template_loader: ->(n) { pctx.read_template(n) })
|
|
24
25
|
content = nil # parsed lazily; the data's `content` (always _meta-free)
|
|
25
26
|
|
|
26
27
|
targets.each do |t|
|
|
@@ -4,7 +4,7 @@ module Textus
|
|
|
4
4
|
module Validators
|
|
5
5
|
module Events
|
|
6
6
|
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
7
|
-
pubsub_events = Textus::
|
|
7
|
+
pubsub_events = Textus::Step::Catalog::PUBSUB.keys
|
|
8
8
|
events = entry.events
|
|
9
9
|
events.each_key do |evt|
|
|
10
10
|
next if pubsub_events.include?(evt.to_sym)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class
|
|
2
|
+
class Manifest
|
|
3
|
+
class Policy
|
|
4
|
+
class HandlerPermit
|
|
5
5
|
attr_reader :handlers
|
|
6
6
|
|
|
7
7
|
def initialize(handlers:)
|
|
8
8
|
@handlers = Array(handlers).map(&:to_s).freeze
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def permits?(handler)
|
|
12
12
|
@handlers.include?(handler.to_s)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class Manifest
|
|
3
|
+
class Policy
|
|
4
|
+
class React
|
|
5
|
+
ALLOWED_KEYS = %w[on when do scope budget idempotency observe priority].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :raw
|
|
8
|
+
|
|
9
|
+
def initialize(raw:)
|
|
10
|
+
raise Textus::BadManifest.new("react: must be a map") unless raw.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
raw = raw.each_with_object({}) do |(key, value), out|
|
|
13
|
+
normalized = key == true ? "on" : key.to_s
|
|
14
|
+
out[normalized] = value
|
|
15
|
+
end
|
|
16
|
+
raise Textus::BadManifest.new("react.ttl is invalid; ttl belongs only to source.ttl or retention.ttl") if raw.key?("ttl")
|
|
17
|
+
|
|
18
|
+
unknown = raw.keys - ALLOWED_KEYS
|
|
19
|
+
raise Textus::BadManifest.new("react: unknown key(s): #{unknown.join(", ")}") unless unknown.empty?
|
|
20
|
+
|
|
21
|
+
@raw = raw
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
@raw
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|