textus 0.52.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 +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- 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/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- 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 +11 -11
- 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 +1 -1
- 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.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 +17 -9
- 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 +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- 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 +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- 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 +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- 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 +7 -5
- 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/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- 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/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 -3
- 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 +132 -118
- 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/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- 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/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- 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 -73
- 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/jobs.rb +0 -31
- 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/enqueue.rb +0 -50
- 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/docs/architecture/README.md
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
# Textus architecture
|
|
2
2
|
|
|
3
3
|
> **Explanation** · for contributors · **read this first** for orientation before SPEC
|
|
4
|
-
> **SSoT for** the Ruby implementation layout (layers, container, ports,
|
|
4
|
+
> **SSoT for** the Ruby implementation layout (layers, container, ports, dispatch/pipeline paths) · **reviewed** 2026-06 (v0.46)
|
|
5
5
|
|
|
6
6
|
```mermaid
|
|
7
7
|
flowchart TD
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
surfaces["Surfaces — CLI verbs · MCP gate (JSON-RPC) · RoleScope"]
|
|
9
|
+
contract["Contract — per-verb DSL (source of truth for public interfaces)"]
|
|
10
|
+
dispatch["Dispatch — Gate · Auth · Ledger · Executor · Actions<br/>planner/ · pipeline/ · runtime/ · catalog/"]
|
|
11
|
+
manifest["Manifest — declarative config, no IO (policy/, schema/, entry/)"]
|
|
12
|
+
core["Core — pure value types (Freshness, Job, Duration, Sentinel)"]
|
|
13
|
+
ports["Ports — IO adapters (FileStore, AuditLog, Queue, Publisher…)"]
|
|
14
|
+
step["Step — user-injectable wrappers (Fetch, Transform, Validate, Observe)"]
|
|
15
|
+
surfaces --> contract
|
|
16
|
+
contract --> dispatch
|
|
17
|
+
dispatch --> manifest
|
|
18
|
+
dispatch --> core
|
|
19
|
+
dispatch --> ports
|
|
20
|
+
dispatch --> step
|
|
16
21
|
```
|
|
17
22
|
|
|
18
|
-
*Dependency rule:
|
|
23
|
+
*Dependency rule: inward only.* `dispatch/planner/`, `dispatch/pipeline/`, and `dispatch/runtime/` are private sub-namespaces of `dispatch/` — never referenced directly from `surfaces/` or `contract/`. Use cases are plain classes receiving `(container:, call:)`. Verbs are looked up in the static `Dispatcher::VERBS` table.
|
|
19
24
|
|
|
20
25
|
### What lives in each layer
|
|
21
26
|
|
|
@@ -44,35 +49,44 @@ generatable) — stay hand-authored, plus commands with no dispatcher verb (`ini
|
|
|
44
49
|
`hook`, `mcp serve`, `schema diff/init`). `boot` is auto-generated from its
|
|
45
50
|
contract. Total reconciliation specs make name/dispatch/facet drift unrepresentable.
|
|
46
51
|
|
|
47
|
-
**
|
|
52
|
+
**Surfaces**
|
|
48
53
|
|
|
49
54
|
```
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
RoleScope
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
CLI verbs: store.<verb>(..., role:)
|
|
56
|
+
store.as(role).<verb>(...)
|
|
57
|
+
|
|
58
|
+
MCP gate: textus mcp serve — same actions, JSON-RPC.
|
|
59
|
+
RoleScope (Store#as(role) — builds Call, forwards to Dispatcher)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Dispatch (all runtime)**
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Gate (thin coordinator: Auth → Ledger → Executor)
|
|
66
|
+
Auth (authorization engine — FLOOR predicates + rule guards)
|
|
67
|
+
Ledger (append event to audit before execution)
|
|
68
|
+
Executor (sync/async routing per action BURN mode)
|
|
69
|
+
Event (Data.define: name, actor, target, payload, actions)
|
|
70
|
+
|
|
71
|
+
actions/{get,list,put,key_delete,key_mv,accept,reject,propose,
|
|
72
|
+
drain,materialize,refresh_data,sweep,observe,
|
|
73
|
+
enqueue,audit,blame,deps,rdeps,published,boot,doctor,
|
|
74
|
+
rule_explain,rule_list,rule_lint,pulse,
|
|
75
|
+
data_mv,key_mv_prefix,key_delete_prefix,
|
|
76
|
+
schema_envelope,where,uid,jobs}.rb
|
|
77
|
+
|
|
78
|
+
planner/{planner,scheduler,seeder}.rb (rules-driven job planning)
|
|
79
|
+
pipeline/{engine,render,acquire/{intake,handler,projection,serializer}}.rb
|
|
80
|
+
runtime/{worker,watch,retention/apply,plan}.rb
|
|
81
|
+
catalog/events.rb (dotted event name constants)
|
|
66
82
|
```
|
|
67
83
|
|
|
68
|
-
**
|
|
84
|
+
**Core (pure value types)**
|
|
69
85
|
|
|
70
86
|
```
|
|
71
|
-
Permission (write predicate per zone)
|
|
72
87
|
Freshness::{Verdict,Evaluator}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Predicates::{ZoneWritableBy,SchemaValid,AuthorHeld,TargetIsCanon,EtagMatch,FreshWithin}}
|
|
88
|
+
Jobs::Job (immutable job value object)
|
|
89
|
+
Duration Sentinel
|
|
76
90
|
```
|
|
77
91
|
|
|
78
92
|
**Infrastructure**
|
|
@@ -80,50 +94,45 @@ Policy::{Guard,GuardFactory,BaseGuards,Evaluation,Fetch,Matcher,HandlerAllowlist
|
|
|
80
94
|
```
|
|
81
95
|
Store (composition root — wires ports,
|
|
82
96
|
vends a Container + dispatches verbs)
|
|
83
|
-
Storage::FileStore (bytes-only port: read/write/delete/
|
|
84
|
-
exists?/etag)
|
|
97
|
+
Storage::FileStore (bytes-only port: read/write/delete/exists?/etag)
|
|
85
98
|
Manifest (Data, Resolver, Policy, Rules)
|
|
86
99
|
Schemas (eager-load cache)
|
|
87
100
|
Ports::{AuditLog,AuditSubscriber,Publisher,Clock,
|
|
88
|
-
BuildLock,Queue,
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
BuildLock,Queue,SentinelStore,WatcherLock}
|
|
102
|
+
Step::{EventBus,RegistryStore,Loader,Context,FireReport,
|
|
103
|
+
Signature,Builtin,ErrorLog,Fetch,Transform,Validate,Observe}
|
|
91
104
|
Entry::{Markdown,Json,Yaml,Text} (format strategies)
|
|
105
|
+
Doctor::Validator (schema + role-authority validation — called by doctor check)
|
|
92
106
|
```
|
|
93
107
|
|
|
94
108
|
## How a verb becomes a method
|
|
95
109
|
|
|
96
|
-
|
|
110
|
+
All actions live under `lib/textus/dispatch/actions/`. The shape is uniform:
|
|
97
111
|
|
|
98
112
|
```ruby
|
|
99
113
|
module Textus
|
|
100
|
-
module
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
...
|
|
114
|
+
module Dispatch
|
|
115
|
+
module Actions
|
|
116
|
+
class Get < Base
|
|
117
|
+
BURN = :sync
|
|
118
|
+
|
|
119
|
+
def call(container:, call:)
|
|
120
|
+
...
|
|
121
|
+
end
|
|
109
122
|
end
|
|
110
123
|
end
|
|
111
124
|
end
|
|
112
125
|
end
|
|
113
126
|
```
|
|
114
127
|
|
|
115
|
-
Verbs are looked up in a static frozen table (`Textus::Dispatcher::VERBS`) that maps `:get →
|
|
128
|
+
Verbs are looked up in a static frozen table (`Textus::Dispatcher::VERBS`) that maps `:get → Dispatch::Actions::Get`, `:put → Dispatch::Actions::Put`, etc. Adding a new verb is **one entry in `Dispatcher::VERBS`** plus the class — no metaprogramming.
|
|
116
129
|
|
|
117
|
-
The instantiate-and-call step
|
|
130
|
+
The instantiate-and-call step lives in `Dispatcher.invoke`. `RoleScope` builds the `Call` (request state) and delegates to `Dispatcher.invoke`. Every system interaction flows through `Dispatch::Gate#fire(event)` — surfaces, internal cascades (rdeps), and async job workers all use the same path. Gate runs Auth → Ledger → Executor in sequence.
|
|
118
131
|
|
|
119
|
-
`boot` and `doctor` are
|
|
120
|
-
are thin `(container:, call:)` use cases that delegate to the `Textus::Boot` /
|
|
121
|
-
`Textus::Doctor` report-building libraries (`build(container:, ...)`). They are
|
|
122
|
-
reached through `Dispatcher::VERBS`, not a special method on `RoleScope`.
|
|
132
|
+
`boot` and `doctor` are actions like any other — reached through `Dispatcher::VERBS`.
|
|
123
133
|
|
|
124
|
-
|
|
134
|
+
One collaborator lives outside the dispatcher because it's composed by actions, not invoked as a verb:
|
|
125
135
|
|
|
126
|
-
- `Produce::Engine` — runs the produce pipeline that `drain`/`serve` invoke via the `materialize` job handler; composes `Acquire::Intake` (external pull via handler) with `Produce::Render` (template-driven publish) per entry. Reactive re-produce is enqueued as `materialize` jobs by `Ports::ProduceOnWriteSubscriber` and run by a worker (no in-process thread runner).
|
|
127
136
|
- `Envelope::IO::{Reader,Writer}` — own the parse and persist halves of the write pipeline; the audit-append-as-final-step invariant lives in `Writer`.
|
|
128
137
|
|
|
129
138
|
## Container
|
|
@@ -133,11 +142,11 @@ Use cases never see the raw `Store`. `Textus::Container` is a single record hold
|
|
|
133
142
|
```ruby
|
|
134
143
|
Container = Data.define(
|
|
135
144
|
:manifest, :file_store, :schemas, :root,
|
|
136
|
-
:audit_log, :
|
|
145
|
+
:audit_log, :steps, :gate
|
|
137
146
|
)
|
|
138
147
|
```
|
|
139
148
|
|
|
140
|
-
The `Store` builds one `Container` at boot; every
|
|
149
|
+
The `Store` builds one `Container` at boot; every action receives it via `(container:, call:)`. Step handlers (Fetch, Transform, Observe) receive `caps: <Container>` — they access `caps.manifest`, `caps.steps`, etc.
|
|
141
150
|
|
|
142
151
|
## Ports
|
|
143
152
|
|
|
@@ -150,8 +159,9 @@ Ports are infrastructure adapters with an interface defined by the domain. Each
|
|
|
150
159
|
| `Ports::Clock` | Supplies `Time.now` — a module-function so tests can swap it without dependency injection boilerplate. |
|
|
151
160
|
| `Ports::Publisher` | Copies a built artifact to a repo-relative consumer path and writes a sentinel so the next publish can confirm the target is managed. |
|
|
152
161
|
| `Ports::BuildLock` | Process-exclusive `flock` guard over the produce pipeline. Raises `BuildInProgress` if a build is already running. |
|
|
153
|
-
| `Ports::
|
|
162
|
+
| `Ports::Queue` | Persistent job queue used by `drain`/`watch` workers; tracks ready/leased/done/failed jobs and powers async dispatch actions (`materialize`, `observe`). |
|
|
154
163
|
| `Ports::SentinelStore` | Reads and writes the per-target sentinel file that `Publisher` uses to detect unmanaged overwrites. |
|
|
164
|
+
| `Ports::WatcherLock` | Single-watcher `flock` guard used by `Dispatch::Runtime::Watch` to ensure only one watcher loop is active per store root. |
|
|
155
165
|
|
|
156
166
|
Application use cases access ports only through `Container` fields — never through the raw `Store`.
|
|
157
167
|
|
|
@@ -192,35 +202,36 @@ The four members are wired in `Manifest.build` (`lib/textus/manifest.rb`). `Mani
|
|
|
192
202
|
2. `Store#get` looks up `Dispatcher::VERBS[:get] → Read::Get`, builds a `Call`, instantiates `Read::Get.new(container:, call:).call(key)`. The verb takes only `key` — there is no `fetch` flag on any surface.
|
|
193
203
|
3. `Read::Get#call(key)` resolves the path through `container.manifest`, reads bytes via `container.file_store`, parses the envelope, and annotates a freshness verdict (`stale`, `reason`, `fetching: false`). When the key has no `upkeep` rule, the envelope is annotated fresh. A stale entry with `upkeep: { ttl:, action: refresh }` is returned **stale** — the read does not refresh it; the next `drain` does.
|
|
194
204
|
|
|
195
|
-
Because the read is always pure, every caller — interactive reads, dashboards, and the direct in-process callers (accept/reject/publish, materializer, uid,
|
|
205
|
+
Because the read is always pure, every caller — interactive reads, dashboards, and the direct in-process callers (accept/reject/publish, materializer, uid, schema/tools, hooks/context) — gets the same orchestrator-free, side-effect-free read. The prior read-through path (`get_or_fetch`, then the `fetch:`-flagged `Read::Get`, ADR 0062) and its `Write::FetchOrchestrator` are gone (ADR 0089).
|
|
196
206
|
|
|
197
207
|
## Write path (`store.put(key, ...)`)
|
|
198
208
|
|
|
199
|
-
1. CLI
|
|
200
|
-
2. `
|
|
201
|
-
3.
|
|
202
|
-
4.
|
|
209
|
+
1. CLI/MCP surface calls `store.as(role).put(key, meta:, body:, content:, if_etag:)`.
|
|
210
|
+
2. `Surfaces::RoleScope#dispatch_bound` fires `Gate.fire(Event.new("entry.put", actor: role, ...))`.
|
|
211
|
+
3. `Dispatch::Gate` runs Auth → Ledger → Executor. `Auth#check_event!` evaluates FLOOR predicates (`lane_writable_by`) plus any rule-declared guards — raises `WriteForbidden` / `GuardFailed` on failure.
|
|
212
|
+
4. `Actions::Put#call` validates the key, resolves the manifest entry, delegates persistence to `Envelope::IO::Writer#put` (serialize → schema-validate → etag-check → `FileStore#write` → `AuditLog#append`).
|
|
213
|
+
5. Publishes `:entry_written` via `container.steps` and fires a cascade Gate event for rdep materialization.
|
|
203
214
|
|
|
204
|
-
`
|
|
215
|
+
`Actions::{KeyDelete,KeyMv,Accept,Reject,Propose}` follow the same shape. All write actions inherit `WriteVerb#run_with_cascade`, which enqueues `materialize` jobs for rdeps after the write completes.
|
|
205
216
|
|
|
206
|
-
|
|
217
|
+
## Pipeline path (`drain` + reactive `entry.written`)
|
|
207
218
|
|
|
208
|
-
|
|
219
|
+
The pipeline handles two concerns — **acquire** (pull live data via an intake handler) and **render** (template-driven artifact publish) — unified under `Dispatch::Pipeline::Engine`.
|
|
209
220
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
`Produce::Engine.converge(container:, call:, keys:)` is the entry point the `materialize` job handler calls. Both the batch path (`drain`/`serve` seed jobs) and the reactive path (`Ports::ProduceOnWriteSubscriber` enqueues `materialize` jobs on `entry_written`/`entry_deleted`/`entry_renamed`) flow through the queue worker into `converge`.
|
|
221
|
+
`Pipeline::Engine.converge(container:, call:, keys:)` is the entry point `Actions::Materialize` calls. Both the batch path (`drain` seeds jobs via `Planner::Seeder`) and the reactive path (write actions enqueue `materialize` jobs via `WriteVerb#cascade_to_rdeps`) flow through the queue worker into `converge`.
|
|
213
222
|
|
|
214
223
|
For each key, `Engine#produce_one`:
|
|
215
224
|
|
|
216
|
-
1. **Acquire phase** — `
|
|
217
|
-
- Resolves the manifest entry; looks up the
|
|
218
|
-
- Publishes `:entry_fetch_started` via `
|
|
219
|
-
- Invokes the handler under a timeout deadline.
|
|
225
|
+
1. **Acquire phase** — `Pipeline::Acquire::Intake#run(key)`:
|
|
226
|
+
- Resolves the manifest entry; looks up the step handler via `container.steps`.
|
|
227
|
+
- Publishes `:entry_fetch_started` via `container.steps`.
|
|
228
|
+
- Invokes the `Step::Fetch` handler under a timeout deadline.
|
|
220
229
|
- On error: publishes `:entry_fetch_failed`, re-raises.
|
|
221
|
-
- On success: normalises the handler result
|
|
222
|
-
- `Acquire::Handler` resolves and invokes the
|
|
223
|
-
2. **Render phase** — `entry.publish_via(context)` calls `
|
|
230
|
+
- On success: normalises the handler result, checks auth, persists via `Envelope::IO::Writer`, publishes `:entry_fetched` unless the etag is unchanged.
|
|
231
|
+
- `Acquire::Handler` resolves and invokes the step under the timeout deadline. (The sibling **projection** sub-path — `from: derive` entries — runs `Acquire::Projection`, which renders data files through `Acquire::Serializer::{Json,Yaml,Text}` before persisting.)
|
|
232
|
+
2. **Render phase** — `entry.publish_via(context)` calls `Pipeline::Render#bytes_for(target:, data:, boot:)` to expand the Mustache template and copy the result to the publish target via `Ports::Publisher`. Returns `nil` if no publish is configured (skipped).
|
|
233
|
+
|
|
234
|
+
Per-entry failures are published as `:produce_failed` by `Actions::Materialize` after `Engine.converge` returns. A held `BuildLock` is a soft miss — the in-flight build already produces fresh output.
|
|
224
235
|
|
|
225
236
|
Reactive produce is enqueued as `materialize` jobs onto `Ports::Queue` when `entry_written`/`entry_deleted`/`entry_renamed` fires; a worker (`drain`/`serve`) runs them through `converge`. A held `BuildLock` is a soft miss — the in-flight build already produces fresh output.
|
|
226
237
|
|
|
@@ -257,4 +268,4 @@ Contract drift surfaces as `ContractDrift` (contract_etag mismatch — a change
|
|
|
257
268
|
|
|
258
269
|
`Hooks::Signature` is the single home of callable keyword-introspection — both `EventBus` (pub-sub dispatch) and `RpcRegistry` (RPC dispatch) delegate to it for `accepts_keyrest?`, `declared_keys`, `missing`, and `filter` rather than each maintaining a hand-rolled copy (ADR 0027). RPC handlers declare `caps:` (single handler); pub-sub handlers declare `ctx:` (0..N handlers).
|
|
259
270
|
|
|
260
|
-
The event names, payloads, and per-verb firing order are documented once in [`reference/events.md`](../reference/events.md) (the friendly SSoT); the authoritative source is `lib/textus/
|
|
271
|
+
The event names, payloads, and per-verb firing order are documented once in [`reference/events.md`](../reference/events.md) (the friendly SSoT); the authoritative source is `lib/textus/step/catalog.rb` (`Catalog::RPC` and `Catalog::PUBSUB`) and `lib/textus/dispatch/catalog/events.rb` (dotted Gate event name constants).
|
data/exe/textus
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Accept < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :accept
|
|
9
|
+
summary "apply a queued proposal to its target zone; requires the author capability"
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "accept"
|
|
12
|
+
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(pending_key:)
|
|
17
|
+
super()
|
|
18
|
+
@pending_key = pending_key
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, call:)
|
|
22
|
+
env = Textus::Action::Get.new(key: @pending_key).call(container: container, call: call)
|
|
23
|
+
proposal = env.meta["proposal"] or raise Textus::ProposalError.new("entry has no proposal block: #{@pending_key}")
|
|
24
|
+
target = proposal["target_key"] or raise Textus::ProposalError.new("proposal missing target_key")
|
|
25
|
+
action = proposal["action"] || "put"
|
|
26
|
+
|
|
27
|
+
case action
|
|
28
|
+
when "put"
|
|
29
|
+
Textus::Action::Put.new(
|
|
30
|
+
key: target,
|
|
31
|
+
meta: env.meta["_meta"] || {},
|
|
32
|
+
body: env.body,
|
|
33
|
+
).call(container: container, call: call)
|
|
34
|
+
when "delete"
|
|
35
|
+
Textus::Action::KeyDelete.new(key: target).call(container: container, call: call)
|
|
36
|
+
else
|
|
37
|
+
raise Textus::ProposalError.new("unknown action: #{action}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Textus::Action::KeyDelete.new(key: @pending_key).call(container: container, call: call)
|
|
41
|
+
|
|
42
|
+
container.steps.publish(
|
|
43
|
+
:proposal_accepted,
|
|
44
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
45
|
+
key: @pending_key,
|
|
46
|
+
target_key: target,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
{ "protocol" => Textus::PROTOCOL, "accepted" => @pending_key, "target_key" => target, "action" => action }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Textus
|
|
7
|
+
module Action
|
|
8
|
+
class Audit < Base
|
|
9
|
+
extend Textus::Contract::DSL
|
|
10
|
+
|
|
11
|
+
verb :audit
|
|
12
|
+
summary "Query the audit log with optional filters."
|
|
13
|
+
surfaces :cli
|
|
14
|
+
cli "audit"
|
|
15
|
+
arg :key, String, required: false, description: "filter to rows for this key"
|
|
16
|
+
arg :lane, String, required: false, description: "filter to keys in this lane"
|
|
17
|
+
arg :role, String, required: false, description: "filter to rows written under this role"
|
|
18
|
+
arg :verb, String, required: false, description: "filter to rows for this verb"
|
|
19
|
+
arg :since, String, required: false,
|
|
20
|
+
coerce: ->(s) { Textus::Action::Audit.parse_since(s, now: Time.now) },
|
|
21
|
+
description: "ISO-8601 timestamp or relative offset (e.g. 1h, 30m)"
|
|
22
|
+
arg :seq_since, Integer, required: false, description: "return rows with seq > this cursor value"
|
|
23
|
+
arg :correlation_id, String, required: false, description: "filter to rows with this correlation_id"
|
|
24
|
+
arg :limit, Integer, required: false, description: "maximum number of rows to return"
|
|
25
|
+
view(:cli) { |rows, _i| { "verb" => "audit", "rows" => rows } }
|
|
26
|
+
|
|
27
|
+
BURN = :sync
|
|
28
|
+
|
|
29
|
+
def initialize(**kwargs)
|
|
30
|
+
super()
|
|
31
|
+
@query = Query.build(**kwargs.slice(:key, :lane, :role, :verb, :since, :seq_since, :correlation_id, :limit))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def args
|
|
35
|
+
@query.to_h.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call(container:, **)
|
|
39
|
+
@manifest = container.manifest
|
|
40
|
+
@root = container.root
|
|
41
|
+
@log_path = Textus::Layout.audit_log(container.root)
|
|
42
|
+
@audit_log = container.audit_log
|
|
43
|
+
|
|
44
|
+
query = @query
|
|
45
|
+
check_cursor_expiry!(query.seq_since)
|
|
46
|
+
|
|
47
|
+
files = all_log_files
|
|
48
|
+
return [] if files.empty?
|
|
49
|
+
|
|
50
|
+
rows = []
|
|
51
|
+
files.each do |file|
|
|
52
|
+
File.foreach(file) do |line|
|
|
53
|
+
parsed = parse_row(line.chomp)
|
|
54
|
+
next unless parsed
|
|
55
|
+
next unless query.matches?(parsed)
|
|
56
|
+
next if query.lane && !key_in_lane?(parsed["key"], query.lane)
|
|
57
|
+
|
|
58
|
+
rows << parsed
|
|
59
|
+
break if limit_reached?(rows, query)
|
|
60
|
+
end
|
|
61
|
+
break if limit_reached?(rows, query)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
rows
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.parse_since(str, now: Time.now.utc)
|
|
68
|
+
return nil if str.nil? || str.empty?
|
|
69
|
+
return Time.parse(str) if str =~ /\A\d{4}-\d{2}-\d{2}/
|
|
70
|
+
|
|
71
|
+
match = str.match(/\A(\d+)([smhd])\z/) or return nil
|
|
72
|
+
mult = { "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }[match[2]]
|
|
73
|
+
now - (match[1].to_i * mult)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
Query = Data.define(:key, :lane, :role, :verb, :since, :seq_since, :correlation_id, :limit) do
|
|
77
|
+
# rubocop:disable Metrics/ParameterLists
|
|
78
|
+
def self.build(key: nil, lane: nil, role: nil, verb: nil,
|
|
79
|
+
since: nil, seq_since: nil, correlation_id: nil, limit: nil)
|
|
80
|
+
new(key:, lane:, role:, verb:, since:, seq_since:, correlation_id:, limit:)
|
|
81
|
+
end
|
|
82
|
+
# rubocop:enable Metrics/ParameterLists
|
|
83
|
+
|
|
84
|
+
def matches?(row)
|
|
85
|
+
return false if key && row["key"] != key
|
|
86
|
+
return false if role && row["role"] != role
|
|
87
|
+
return false if verb && row["verb"] != verb
|
|
88
|
+
return false if since && (row["ts"].nil? || Time.parse(row["ts"]) < since)
|
|
89
|
+
return false if seq_since && (row["seq"].nil? || row["seq"] <= seq_since)
|
|
90
|
+
return false if correlation_id && row.dig("extras", "correlation_id") != correlation_id
|
|
91
|
+
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def limit_reached?(rows, query) = query.limit && rows.length >= query.limit
|
|
99
|
+
|
|
100
|
+
def check_cursor_expiry!(seq_since)
|
|
101
|
+
return unless seq_since
|
|
102
|
+
|
|
103
|
+
log = @audit_log || Textus::Ports::AuditLog.new(@root)
|
|
104
|
+
min = log.min_available_seq
|
|
105
|
+
raise Textus::CursorExpired.new(requested: seq_since, min_available: min) if min && seq_since < min - 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def all_log_files
|
|
109
|
+
rotated = Dir.glob(File.join(Textus::Layout.audit_dir(@root), "audit.log.*"))
|
|
110
|
+
.reject { |path| path.end_with?(".meta.json") }
|
|
111
|
+
.sort_by { |path| -path.scan(/\d+$/).first.to_i }
|
|
112
|
+
active = File.exist?(@log_path) ? [@log_path] : []
|
|
113
|
+
rotated + active
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_row(line)
|
|
117
|
+
return nil if line.empty?
|
|
118
|
+
return nil unless line.start_with?("{")
|
|
119
|
+
|
|
120
|
+
JSON.parse(line)
|
|
121
|
+
rescue JSON::ParserError
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def key_in_lane?(key, lane)
|
|
126
|
+
mentry = @manifest.resolver.resolve(key).entry
|
|
127
|
+
mentry && mentry.lane == lane
|
|
128
|
+
rescue Textus::Error
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
@registry = {}
|
|
6
|
+
|
|
7
|
+
def self.registry = @registry
|
|
8
|
+
|
|
9
|
+
def self.register(klass)
|
|
10
|
+
@registry[klass.name.gsub("::", "/").downcase] = klass
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.fetch(type)
|
|
14
|
+
return @registry[type] if @registry[type]
|
|
15
|
+
|
|
16
|
+
match = @registry.values.find { |k| k.const_defined?(:TYPE, false) && type == k::TYPE }
|
|
17
|
+
raise Textus::UsageError.new("unknown action type: #{type}") unless match
|
|
18
|
+
|
|
19
|
+
@registry[type] = match
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Base
|
|
23
|
+
def self.inherited(subclass)
|
|
24
|
+
super
|
|
25
|
+
Textus::Action.register(subclass) if subclass.name
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call(**)
|
|
29
|
+
raise NotImplementedError.new("#{self.class}#call")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def args
|
|
33
|
+
params = self.class.instance_method(:initialize).parameters
|
|
34
|
+
names = params.select { |t,| %i[key keyreq].include?(t) }.map(&:last)
|
|
35
|
+
names.each_with_object({}) do |name, h|
|
|
36
|
+
val = instance_variable_get(:"@#{name}")
|
|
37
|
+
h[name] = val unless val.nil?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -1,34 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "open3"
|
|
2
4
|
|
|
3
5
|
module Textus
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
# author, date, subject) that introduced the file state at that audit
|
|
7
|
-
# row. Falls back to `git => nil` when not in a git repo or when the
|
|
8
|
-
# file is untracked.
|
|
9
|
-
class Blame
|
|
6
|
+
module Action
|
|
7
|
+
class Blame < Base
|
|
10
8
|
extend Textus::Contract::DSL
|
|
11
9
|
|
|
12
|
-
verb
|
|
13
|
-
summary
|
|
10
|
+
verb :blame
|
|
11
|
+
summary "Annotate audit rows for a key with the git commit that introduced each file state."
|
|
14
12
|
surfaces :cli
|
|
15
|
-
cli
|
|
16
|
-
arg :key,
|
|
13
|
+
cli "blame"
|
|
14
|
+
arg :key, String, required: true, positional: true, description: "entry key to blame"
|
|
17
15
|
arg :limit, Integer, required: false, description: "maximum number of audit rows to return"
|
|
18
16
|
view(:cli) { |rows, inputs| { "verb" => "blame", "key" => inputs[:key], "rows" => rows } }
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
BURN = :sync
|
|
19
|
+
|
|
20
|
+
def initialize(key:, limit: nil)
|
|
21
|
+
super()
|
|
22
|
+
@key = key
|
|
23
|
+
@limit = limit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(container:, **)
|
|
21
27
|
@container = container
|
|
22
|
-
@manifest
|
|
23
|
-
@root
|
|
28
|
+
@manifest = container.manifest
|
|
29
|
+
@root = container.root
|
|
30
|
+
|
|
31
|
+
audit_rows = Textus::Action::Audit.new(key: @key, limit: @limit).call(container: container)
|
|
32
|
+
path = resolve_path(@key)
|
|
33
|
+
return audit_rows.map { |row| row.merge("git" => nil) } unless git_tracked?(path)
|
|
34
|
+
|
|
35
|
+
audit_rows.map { |row| row.merge("git" => git_commit_at(path, timestamp: row["ts"])) }
|
|
24
36
|
end
|
|
25
37
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
path = resolve_path(key)
|
|
29
|
-
return audit_rows.map { |r| r.merge("git" => nil) } unless git_tracked?(path)
|
|
38
|
+
def self.new(*args, **kwargs)
|
|
39
|
+
return super(**kwargs) unless args.any?
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
42
|
+
mapped = positional.zip(args).to_h
|
|
43
|
+
super(**mapped.merge(kwargs))
|
|
32
44
|
end
|
|
33
45
|
|
|
34
46
|
private
|
|
@@ -37,9 +49,6 @@ module Textus
|
|
|
37
49
|
res = @manifest.resolver.resolve(key)
|
|
38
50
|
mentry = res.entry
|
|
39
51
|
path = res.path
|
|
40
|
-
# Nested entries resolve to a file under the entry path; leaf entries
|
|
41
|
-
# already have a fully-resolved path. Either way `path` is what git
|
|
42
|
-
# needs to know about.
|
|
43
52
|
path || Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
44
53
|
rescue Textus::Error
|
|
45
54
|
nil
|
|
@@ -60,7 +69,6 @@ module Textus
|
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
def git_repo?
|
|
63
|
-
# Walk up from store root to find a .git directory.
|
|
64
72
|
dir = @root
|
|
65
73
|
loop do
|
|
66
74
|
return true if File.directory?(File.join(dir, ".git"))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Boot < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :boot
|
|
9
|
+
summary "Return the orientation contract: zones, entries, schemas, write_flows, agent_quickstart."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :lean, :boolean,
|
|
12
|
+
description: "return only orientation essentials (zones, agent_quickstart, contract_etag) for cheap session-start injection"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(lean: nil)
|
|
17
|
+
super()
|
|
18
|
+
@lean = lean
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, **)
|
|
22
|
+
Textus::Boot.build(container: container, lean: !@lean.nil?)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|