textus 0.22.0 → 0.29.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/ARCHITECTURE.md +195 -48
- data/CHANGELOG.md +178 -0
- data/README.md +55 -13
- data/SPEC.md +79 -42
- data/docs/conventions.md +10 -0
- data/lib/textus/boot.rb +31 -29
- data/lib/textus/builder/pipeline.rb +13 -12
- data/lib/textus/call.rb +28 -0
- data/lib/textus/cli/group/mcp.rb +9 -0
- data/lib/textus/cli/group/zone.rb +9 -0
- data/lib/textus/cli/verb/accept.rb +1 -1
- data/lib/textus/cli/verb/audit.rb +2 -2
- data/lib/textus/cli/verb/blame.rb +1 -1
- data/lib/textus/cli/verb/boot.rb +1 -1
- data/lib/textus/cli/verb/build.rb +3 -3
- data/lib/textus/cli/verb/delete.rb +1 -1
- data/lib/textus/cli/verb/deps.rb +1 -1
- data/lib/textus/cli/verb/doctor.rb +1 -1
- data/lib/textus/cli/verb/freshness.rb +1 -1
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -4
- data/lib/textus/cli/verb/hooks.rb +11 -14
- data/lib/textus/cli/verb/key_delete.rb +24 -0
- data/lib/textus/cli/verb/list.rb +1 -1
- data/lib/textus/cli/verb/mcp_serve.rb +17 -0
- data/lib/textus/cli/verb/migrate.rb +18 -0
- data/lib/textus/cli/verb/mv.rb +11 -3
- data/lib/textus/cli/verb/published.rb +1 -1
- data/lib/textus/cli/verb/pulse.rb +1 -1
- data/lib/textus/cli/verb/put.rb +8 -6
- data/lib/textus/cli/verb/rdeps.rb +1 -1
- data/lib/textus/cli/verb/refresh.rb +1 -1
- data/lib/textus/cli/verb/refresh_stale.rb +1 -1
- data/lib/textus/cli/verb/reject.rb +1 -1
- data/lib/textus/cli/verb/rule_explain.rb +1 -1
- data/lib/textus/cli/verb/rule_lint.rb +18 -0
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb/uid.rb +1 -1
- data/lib/textus/cli/verb/where.rb +1 -1
- data/lib/textus/cli/verb/zone_mv.rb +19 -0
- data/lib/textus/cli/verb.rb +7 -7
- data/lib/textus/cli.rb +0 -7
- data/lib/textus/container.rb +23 -0
- data/lib/textus/dispatcher.rb +49 -0
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
- data/lib/textus/doctor/check/hooks.rb +4 -3
- data/lib/textus/doctor/check/illegal_keys.rb +2 -2
- data/lib/textus/doctor/check/intake_registration.rb +2 -2
- data/lib/textus/doctor/check/manifest_files.rb +2 -2
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/refresh_locks.rb +2 -2
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/schemas.rb +2 -2
- data/lib/textus/doctor/check/sentinels.rb +11 -9
- data/lib/textus/doctor/check/templates.rb +2 -2
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +12 -3
- data/lib/textus/doctor.rb +24 -27
- data/lib/textus/domain/authorizer.rb +6 -6
- data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
- data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
- data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
- data/lib/textus/domain/sentinel.rb +9 -65
- data/lib/textus/domain/staleness/generator_check.rb +46 -26
- data/lib/textus/domain/staleness/intake_check.rb +20 -12
- data/lib/textus/domain/staleness.rb +4 -4
- data/lib/textus/envelope/io/reader.rb +44 -0
- data/lib/textus/{application/writes/envelope_io.rb → envelope/io/writer.rb} +32 -58
- data/lib/textus/hooks/builtin.rb +14 -14
- data/lib/textus/hooks/context.rb +30 -13
- data/lib/textus/hooks/error_log.rb +32 -0
- data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
- data/lib/textus/hooks/loader.rb +29 -3
- data/lib/textus/hooks/rpc_registry.rb +77 -0
- data/lib/textus/key/path.rb +7 -3
- data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
- data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
- data/lib/textus/maintenance/migrate.rb +51 -0
- data/lib/textus/maintenance/rule_lint.rb +56 -0
- data/lib/textus/maintenance/zone_mv.rb +51 -0
- data/lib/textus/maintenance.rb +15 -0
- data/lib/textus/manifest/data.rb +79 -0
- data/lib/textus/manifest/entry/base.rb +38 -18
- data/lib/textus/manifest/entry/derived.rb +8 -9
- data/lib/textus/manifest/entry/nested.rb +7 -9
- data/lib/textus/manifest/entry/parser.rb +2 -2
- data/lib/textus/manifest/entry/validators/events.rb +2 -2
- data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
- data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
- data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
- data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +2 -2
- data/lib/textus/manifest/entry.rb +0 -5
- data/lib/textus/manifest/policy.rb +48 -0
- data/lib/textus/manifest/resolver.rb +14 -14
- data/lib/textus/manifest/rules.rb +1 -1
- data/lib/textus/manifest.rb +47 -110
- data/lib/textus/mcp/errors.rb +32 -0
- data/lib/textus/mcp/server.rb +126 -0
- data/lib/textus/mcp/session.rb +40 -0
- data/lib/textus/mcp/tool_schemas.rb +71 -0
- data/lib/textus/mcp/tools.rb +129 -0
- data/lib/textus/mcp.rb +6 -0
- data/lib/textus/{infra → ports}/audit_log.rb +1 -1
- data/lib/textus/{infra → ports}/audit_subscriber.rb +7 -8
- data/lib/textus/{infra → ports}/build_lock.rb +1 -1
- data/lib/textus/{infra → ports}/clock.rb +1 -1
- data/lib/textus/{infra → ports}/publisher.rb +6 -6
- data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
- data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +67 -0
- data/lib/textus/ports/storage/file_stat.rb +19 -0
- data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
- data/lib/textus/projection.rb +91 -0
- data/lib/textus/read/audit.rb +111 -0
- data/lib/textus/read/blame.rb +81 -0
- data/lib/textus/read/boot.rb +18 -0
- data/lib/textus/read/deps.rb +24 -0
- data/lib/textus/read/doctor.rb +19 -0
- data/lib/textus/read/freshness.rb +101 -0
- data/lib/textus/read/get.rb +66 -0
- data/lib/textus/read/get_or_refresh.rb +69 -0
- data/lib/textus/read/list.rb +15 -0
- data/lib/textus/read/policy_explain.rb +37 -0
- data/lib/textus/read/published.rb +15 -0
- data/lib/textus/read/pulse.rb +89 -0
- data/lib/textus/read/rdeps.rb +25 -0
- data/lib/textus/read/schema_envelope.rb +16 -0
- data/lib/textus/read/stale.rb +17 -0
- data/lib/textus/read/uid.rb +20 -0
- data/lib/textus/read/validate_all.rb +22 -0
- data/lib/textus/read/validator.rb +84 -0
- data/lib/textus/read/where.rb +16 -0
- data/lib/textus/role_scope.rb +49 -0
- data/lib/textus/schema/tools.rb +14 -10
- data/lib/textus/store.rb +25 -11
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +86 -0
- data/lib/textus/write/authority_gate.rb +24 -0
- data/lib/textus/write/delete.rb +54 -0
- data/lib/textus/write/materializer.rb +48 -0
- data/lib/textus/write/mv.rb +123 -0
- data/lib/textus/write/publish.rb +66 -0
- data/lib/textus/write/put.rb +59 -0
- data/lib/textus/write/refresh_all.rb +44 -0
- data/lib/textus/write/refresh_orchestrator.rb +102 -0
- data/lib/textus/write/refresh_worker.rb +138 -0
- data/lib/textus/write/reject.rb +54 -0
- data/lib/textus.rb +7 -1
- metadata +75 -46
- data/lib/textus/application/context.rb +0 -34
- data/lib/textus/application/projection.rb +0 -91
- data/lib/textus/application/reads/audit.rb +0 -94
- data/lib/textus/application/reads/blame.rb +0 -82
- data/lib/textus/application/reads/deps.rb +0 -26
- data/lib/textus/application/reads/freshness.rb +0 -88
- data/lib/textus/application/reads/get.rb +0 -67
- data/lib/textus/application/reads/get_or_refresh.rb +0 -51
- data/lib/textus/application/reads/list.rb +0 -17
- data/lib/textus/application/reads/policy_explain.rb +0 -39
- data/lib/textus/application/reads/published.rb +0 -17
- data/lib/textus/application/reads/pulse.rb +0 -63
- data/lib/textus/application/reads/rdeps.rb +0 -27
- data/lib/textus/application/reads/schema_envelope.rb +0 -18
- data/lib/textus/application/reads/stale.rb +0 -15
- data/lib/textus/application/reads/uid.rb +0 -23
- data/lib/textus/application/reads/validate_all.rb +0 -24
- data/lib/textus/application/reads/validator.rb +0 -86
- data/lib/textus/application/reads/where.rb +0 -18
- data/lib/textus/application/refresh/all.rb +0 -52
- data/lib/textus/application/refresh/orchestrator.rb +0 -78
- data/lib/textus/application/refresh/worker.rb +0 -116
- data/lib/textus/application/writes/accept.rb +0 -89
- data/lib/textus/application/writes/authority_gate.rb +0 -26
- data/lib/textus/application/writes/delete.rb +0 -33
- data/lib/textus/application/writes/materializer.rb +0 -50
- data/lib/textus/application/writes/mv.rb +0 -105
- data/lib/textus/application/writes/publish.rb +0 -81
- data/lib/textus/application/writes/put.rb +0 -37
- data/lib/textus/application/writes/reject.rb +0 -50
- data/lib/textus/infra/event_bus.rb +0 -27
- data/lib/textus/operations.rb +0 -176
data/SPEC.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
## 1. What textus is
|
|
12
12
|
|
|
13
|
-
A storage convention and JSON wire protocol
|
|
13
|
+
A storage convention and JSON wire protocol for humans, agents, and runners to read and write structured project memory **deterministically**. It provides addressable dotted keys, schema validation, role-based write gates, declarative compute, and copy-based publish targets.
|
|
14
14
|
|
|
15
15
|
The storage lives in a `.textus/` directory at the project root. Each entry is a Markdown file with YAML frontmatter. A manifest binds dotted keys to subtrees and declares which roles may write to each zone. Schemas (also YAML) define what frontmatter shape each entry must have. Derived entries are computed from other entries via pure projections and a vendored Mustache template engine, then optionally published to repo-relative paths as byte-for-byte file copies. The CLI surface (`textus get/put/list/where/schema/build/...` `--output=json`) returns a versioned envelope any caller can parse without knowing Markdown.
|
|
16
16
|
|
|
@@ -68,10 +68,10 @@ The root is `.textus/` at the project working directory. A typical tree:
|
|
|
68
68
|
.textus/
|
|
69
69
|
manifest.yaml # internal: key → subtree mapping + zones declarations
|
|
70
70
|
audit.log # internal, append-only NDJSON log of every successful write
|
|
71
|
-
role # internal, role token (one line, e.g. "human")
|
|
72
71
|
schemas/ # internal: YAML schema files
|
|
73
72
|
templates/ # internal: Mustache templates referenced by derived entries
|
|
74
|
-
|
|
73
|
+
hooks/ # internal: one Ruby file per hook
|
|
74
|
+
sentinels/ # internal: bookkeeping for byte-copied publish targets (see §5.3)
|
|
75
75
|
zones/ # ALL user content lives here
|
|
76
76
|
identity/ # zone: identity (human-only)
|
|
77
77
|
working/ # zone: working (human, agent, runner)
|
|
@@ -80,7 +80,7 @@ The root is `.textus/` at the project working directory. A typical tree:
|
|
|
80
80
|
output/ # zone: output (builder only — computed outputs)
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Textus internals (`manifest.yaml`, `audit.log`, `
|
|
83
|
+
Textus internals (`manifest.yaml`, `audit.log`, `schemas/`, `templates/`, `hooks/`, `sentinels/`) live directly under `.textus/`. **All user content lives under `.textus/zones/`.** Manifest `path:` fields are relative to `.textus/zones/` — they do **not** include the `zones/` prefix. Implementations MUST prepend `zones/` to every `path:` when resolving a key to a filesystem location.
|
|
84
84
|
|
|
85
85
|
Zone directories under `zones/` are conventional; their write semantics are declared in the manifest, not the directory name.
|
|
86
86
|
|
|
@@ -138,6 +138,10 @@ entries:
|
|
|
138
138
|
rules:
|
|
139
139
|
- match: intake.**
|
|
140
140
|
refresh: { ttl: 6h, on_stale: warn }
|
|
141
|
+
|
|
142
|
+
audit:
|
|
143
|
+
max_size: 10485760 # bytes before rotating (default: 10 485 760 = 10 MiB)
|
|
144
|
+
keep: 5 # rotated files to retain (default: 5)
|
|
141
145
|
```
|
|
142
146
|
|
|
143
147
|
Zone names are conventional — the manifest is the source of truth for write permissions; rename freely.
|
|
@@ -397,7 +401,7 @@ In intake mode the handler MUST return one of three shapes, all normalized by th
|
|
|
397
401
|
|
|
398
402
|
**Refresh paths.** Two are supported:
|
|
399
403
|
|
|
400
|
-
1. **In-process** — `textus refresh KEY --as=runner` resolves the entry's `intake.handler`, invokes the registered `:resolve_intake` hook with `(
|
|
404
|
+
1. **In-process** — `textus refresh KEY --as=runner` resolves the entry's `intake.handler`, invokes the registered `:resolve_intake` hook with `(caps:, config:, args: {})`, and writes the result under role `runner`.
|
|
401
405
|
2. **External runner** — a cron job or agent harness reads `textus list --zone=intake --stale --output=json`, fetches the source out of band, and pipes bytes back through `textus put KEY --as=runner --stdin`. The CLI verb `textus refresh stale [--prefix=K] [--zone=Z]` drives this loop in one shot.
|
|
402
406
|
|
|
403
407
|
Both paths share the same role gate, audit-log entry, and `:entry_refreshed` event. User-supplied hooks live in `.textus/hooks/**/*.rb` and auto-load at `Store#initialize` — see §5.10 for the full hook contract.
|
|
@@ -439,6 +443,35 @@ Schema (one JSON object per line, no interior whitespace):
|
|
|
439
443
|
|
|
440
444
|
For `mv`, the structural fields `from_key`, `to_key`, and `uid` appear at the top level of the JSON object. Remaining verb-specific data (e.g. `from_path`, `to_path`) is nested under an `extras` key. The `extras` key is omitted entirely when empty.
|
|
441
445
|
|
|
446
|
+
**Rotation.** After every successful append the implementation checks whether `audit.log` exceeds `max_size` bytes (checked inside the held `flock`, so the check sees the post-write size). If it does, the active log is rotated:
|
|
447
|
+
|
|
448
|
+
1. The seq range (`min_seq`, `max_seq`) of the active log is scanned, and a JSON sidecar (`audit.log.1.meta.json`) is written with those values plus a `rotated_at` ISO 8601 timestamp.
|
|
449
|
+
2. Existing rotated files are shifted: `audit.log.(N)` → `audit.log.(N+1)` for N = `keep-1` down to 1 (with their `.meta.json` sidecars).
|
|
450
|
+
3. `audit.log` is renamed to `audit.log.1`.
|
|
451
|
+
4. The file that would be shifted to `audit.log.(keep+1)` — i.e., `audit.log.keep` and its sidecar — is deleted before the shift.
|
|
452
|
+
5. The next append creates a fresh `audit.log` via `O_CREAT`. Seq numbering continues from the previous maximum; there is no reset.
|
|
453
|
+
|
|
454
|
+
Rotation is triggered by **byte size only** — there is no row-count or time-based trigger.
|
|
455
|
+
|
|
456
|
+
**Rotation knobs** (configured via the optional `audit:` block in `manifest.yaml`):
|
|
457
|
+
|
|
458
|
+
| Key | Default | Meaning |
|
|
459
|
+
|------------|--------------|---------|
|
|
460
|
+
| `max_size` | `10485760` | Maximum size of `audit.log` in bytes (10 MiB) before rotation is triggered. |
|
|
461
|
+
| `keep` | `5` | Number of rotated files retained on disk. When this limit is exceeded the oldest rotated file and its sidecar are deleted. |
|
|
462
|
+
|
|
463
|
+
Both keys are optional. Omitting `audit:` entirely uses the defaults above.
|
|
464
|
+
|
|
465
|
+
**`CursorExpired`.** When `audit --seq-since=N` or `pulse --since=N` is called with a cursor `N`, the implementation checks whether `N` is below the oldest sequence number still available on disk (`min_available_seq`, derived from the oldest retained rotated file's sidecar). The condition that raises `CursorExpired` is:
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
N < min_available_seq - 1
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The error includes `requested` (the supplied cursor value) and `min_available` (the oldest seq still on disk).
|
|
472
|
+
|
|
473
|
+
**Recommended caller behavior on `CursorExpired`.** Call `textus boot` (without `--since`) to obtain a fresh `latest_seq` from the current audit log state, then resume `pulse` calls using that new cursor. Do not attempt to replay from an expired cursor — the intervening rows are gone.
|
|
474
|
+
|
|
442
475
|
### 5.7 Security bounds
|
|
443
476
|
|
|
444
477
|
textus enforces fixed bounds to keep behavior predictable under hostile or buggy input:
|
|
@@ -478,7 +511,7 @@ evolution:
|
|
|
478
511
|
|
|
479
512
|
**Defaults:** when `fields:` and `evolution:` are absent, `schema.maintained_by(field)` returns `nil` for every field and `schema.evolution` returns `{}`.
|
|
480
513
|
|
|
481
|
-
**Override rule:** the role `human` is permitted to write any `maintained_by` field, regardless of declared owner.
|
|
514
|
+
**Override rule:** the role `human` is permitted to write any `maintained_by` field, regardless of declared owner. Humans override agent-maintained fields by design: schema field ownership (`maintained_by:`) makes the boundary explicit, not implicit. All other role mismatches are reported by `doctor --check=schema_violations` with code `role_authority`, including fields `key`, `field`, `expected`, and `last_writer`.
|
|
482
515
|
|
|
483
516
|
### 5.9 Row transforms
|
|
484
517
|
|
|
@@ -495,11 +528,11 @@ The subdirectory layout under `hooks/` is organizational only; the registered ev
|
|
|
495
528
|
```ruby
|
|
496
529
|
# Canonical form — works for every event:
|
|
497
530
|
Textus.hook do |reg|
|
|
498
|
-
reg.on(:resolve_intake, :my_source) { |config:, args:, **| … }
|
|
499
|
-
reg.on(:transform_rows, :rank_by_recency) { |rows:, **| … }
|
|
500
|
-
reg.on(:validate, :storage_writable) { |
|
|
501
|
-
reg.on(:entry_put, :audit, keys: ["working.*"]) { |key:, envelope:, **| … }
|
|
502
|
-
reg.on(:file_published, :git_add, keys: ["derived.*"]) { |target:, **| `git add #{target.shellescape}` }
|
|
531
|
+
reg.on(:resolve_intake, :my_source) { |caps:, config:, args:, **| … }
|
|
532
|
+
reg.on(:transform_rows, :rank_by_recency) { |caps:, rows:, **| … }
|
|
533
|
+
reg.on(:validate, :storage_writable) { |caps:| … }
|
|
534
|
+
reg.on(:entry_put, :audit, keys: ["working.*"]) { |ctx:, key:, envelope:, **| … }
|
|
535
|
+
reg.on(:file_published, :git_add, keys: ["derived.*"]) { |ctx:, target:, **| `git add #{target.shellescape}` }
|
|
503
536
|
end
|
|
504
537
|
```
|
|
505
538
|
|
|
@@ -509,21 +542,21 @@ end
|
|
|
509
542
|
|
|
510
543
|
| Event | Mode | Args | Return | Failure |
|
|
511
544
|
|-------------------------|---------|-----------------------------------------------------------|-----------------------|---------------|
|
|
512
|
-
| `:resolve_intake` | rpc |
|
|
513
|
-
| `:transform_rows` | rpc |
|
|
514
|
-
| `:validate` | rpc |
|
|
515
|
-
| `:entry_put` | pubsub |
|
|
516
|
-
| `:entry_deleted` | pubsub |
|
|
517
|
-
| `:entry_refreshed` | pubsub |
|
|
518
|
-
| `:build_completed` | pubsub |
|
|
519
|
-
| `:proposal_accepted` | pubsub |
|
|
520
|
-
| `:file_published` | pubsub |
|
|
521
|
-
| `:entry_renamed` | pubsub |
|
|
522
|
-
| `:proposal_rejected` | pubsub |
|
|
523
|
-
| `:store_loaded` | pubsub |
|
|
524
|
-
| `:refresh_started` | pubsub |
|
|
525
|
-
| `:refresh_failed` | pubsub |
|
|
526
|
-
| `:refresh_backgrounded` | pubsub |
|
|
545
|
+
| `:resolve_intake` | rpc | caps:, config:, args: | {_meta:, body:} | aborts op |
|
|
546
|
+
| `:transform_rows` | rpc | caps:, rows:, config: | rows array | aborts op |
|
|
547
|
+
| `:validate` | rpc | caps: | issues array | aborts doctor |
|
|
548
|
+
| `:entry_put` | pubsub | ctx:, key:, envelope: | (discarded) | logged |
|
|
549
|
+
| `:entry_deleted` | pubsub | ctx:, key: | (discarded) | logged |
|
|
550
|
+
| `:entry_refreshed` | pubsub | ctx:, key:, envelope:, change: | (discarded) | logged |
|
|
551
|
+
| `:build_completed` | pubsub | ctx:, key:, envelope:, sources: | (discarded) | logged |
|
|
552
|
+
| `:proposal_accepted` | pubsub | ctx:, key:, target_key: | (discarded) | logged |
|
|
553
|
+
| `:file_published` | pubsub | ctx:, key:, envelope:, source:, target: | (discarded) | logged |
|
|
554
|
+
| `:entry_renamed` | pubsub | ctx:, key:, from_key:, to_key:, envelope: | (discarded) | logged |
|
|
555
|
+
| `:proposal_rejected` | pubsub | ctx:, key:, target_key: | (discarded) | logged |
|
|
556
|
+
| `:store_loaded` | pubsub | ctx: | (discarded) | logged |
|
|
557
|
+
| `:refresh_started` | pubsub | ctx:, key:, mode: | (discarded) | logged |
|
|
558
|
+
| `:refresh_failed` | pubsub | ctx:, key:, error_class:, error_message: | (discarded) | logged |
|
|
559
|
+
| `:refresh_backgrounded` | pubsub | ctx:, key:, started_at:, budget_ms: | (discarded) | logged |
|
|
527
560
|
|
|
528
561
|
The three `:refresh_*` lifecycle events report the progress and failures of background (timed_sync) refreshes.
|
|
529
562
|
|
|
@@ -533,14 +566,19 @@ The three `:refresh_*` lifecycle events report the progress and failures of back
|
|
|
533
566
|
|
|
534
567
|
**`:refresh_backgrounded`** fires when a `timed_sync` refresh exceeds its budget and is handed off to a background thread. `started_at:` is an ISO-8601 UTC string; `budget_ms:` is the configured deadline as an integer.
|
|
535
568
|
|
|
536
|
-
**Signature invariant** —
|
|
569
|
+
**Signature invariant** — hooks receive a capability handle as their first keyword argument; the name depends on the mode:
|
|
570
|
+
|
|
571
|
+
- **RPC hooks** (`rpc` mode) receive `caps:` — a `Textus::Container`. Event-specific kwargs (`config:`, `args:`, `rows:`) follow in the stable order shown in the table above.
|
|
572
|
+
- **Pub-sub hooks** (`pubsub` mode) receive `ctx:` — a `Textus::Hooks::Context` that exposes a narrow surface: `get`, `list`, `deps`, `freshness` (reads), `put`, `delete`, `audit` (authorized writes), `publish_followup`, plus `role` and `correlation_id`. The raw `Store` is not handed out.
|
|
573
|
+
|
|
574
|
+
Declaring `store:` instead of `caps:` in an RPC callable will pass registration but raise `UsageError` at call time (`Hooks::RpcRegistry#invoke` rejects `store:` — there is no shim).
|
|
575
|
+
|
|
576
|
+
The primary entity is always `key:` (for `:proposal_accepted`, `key:` is the pending key being accepted and `target_key:` is the destination). For `:entry_renamed`, `key:` is present and equals `to_key:` — it is the entry's post-move home, present so `keys:` glob filters route correctly; `from_key:` is the prior key. For `:proposal_rejected`, `key:` is the pending key being rejected. For `:store_loaded`, no key — the event observes store readiness, not an entry.
|
|
537
577
|
|
|
538
578
|
**RPC mode** — exactly one handler per (event, name). The manifest references the handler by name (`intake.handler: NAME`, `compute.transform: NAME`). Failure or timeout aborts the calling operation.
|
|
539
579
|
|
|
540
580
|
**Pub-sub mode** — zero or more handlers per event. All matching handlers fire. The `keys:` option restricts a handler to keys matching one of the given globs (`File.fnmatch?` rules). Absence of `keys:` fires on every event of that type. Handler failures and 2s timeouts are logged to `audit.log` as `event_error` rows; they NEVER abort the triggering operation.
|
|
541
581
|
|
|
542
|
-
The `store:` argument is always a read-only store proxy. Write attempts raise `UsageError`.
|
|
543
|
-
|
|
544
582
|
Each handler runs under `Timeout.timeout(2)`.
|
|
545
583
|
|
|
546
584
|
### 5.11 Rules
|
|
@@ -888,10 +926,10 @@ Given a review entry `review.identity.self.patch` proposing a change to `identit
|
|
|
888
926
|
|
|
889
927
|
Textus internals are organized into four layers. The dependency rule is one-way — each layer may only import from the layer beneath it.
|
|
890
928
|
|
|
891
|
-
- **Interface** (`lib/textus/cli/`) — CLI verbs. Parses flags, calls a use case, formats JSON.
|
|
892
|
-
- **Application** (`lib/textus/application/`) — Use cases: `
|
|
893
|
-
- **Domain** (`lib/textus/domain/`) — Pure values: `Freshness::Policy`, `Action`, `Outcome`, `
|
|
894
|
-
- **Infrastructure** (`lib/textus/infra/`) — Adapters: `
|
|
929
|
+
- **Interface** (`lib/textus/cli/`, `lib/textus/mcp/`) — CLI verbs and the MCP gate. Parses flags / RPC, calls a use case, formats JSON.
|
|
930
|
+
- **Application** (`lib/textus/application/`) — Use cases: `Read::Get`, `Write::Put`, `Write::RefreshWorker`, `Write::RefreshOrchestrator`, `Write::RefreshAll`, `Maintenance::Migrate`, etc. Orchestrate domain + infra; no business rules.
|
|
931
|
+
- **Domain** (`lib/textus/domain/`) — Pure values: `Authorizer`, `Permission`, `Freshness::{Policy,Verdict,Evaluator}`, `Action`, `Outcome`, `Sentinel`, `Staleness`. No I/O, no globals, testable without disk.
|
|
932
|
+
- **Infrastructure** (`lib/textus/infra/`) — Adapters: `Storage::FileStore`, `AuditLog`, `AuditSubscriber`, `Publisher`, `Clock`, `Refresh::Lock`, `Refresh::Detached`, `BuildLock`. Wrap OS / library primitives.
|
|
895
933
|
|
|
896
934
|
The `lib/textus/store/`, `lib/textus/manifest/`, `lib/textus/hooks/` namespaces are infrastructure adapters that predate this split and remain at their existing paths for backward-compat with the plugin DSL.
|
|
897
935
|
|
|
@@ -899,14 +937,13 @@ Plugin authors interact only with the Hook DSL (`Textus.hook { |reg| reg.on(:res
|
|
|
899
937
|
|
|
900
938
|
Both read and write paths flow through the application layer:
|
|
901
939
|
|
|
902
|
-
- **Reads** flow through `
|
|
903
|
-
- **Writes** flow through `
|
|
904
|
-
- `
|
|
905
|
-
- `Textus::
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
`lib/textus/application/{reads,writes,refresh}/`.
|
|
940
|
+
- **Reads** flow through `Textus::Read::Get` (pure read + freshness annotation) or `Read::GetOrRefresh` (composes Get with `Write::RefreshOrchestrator`). Each takes a `container:` and a `call:`.
|
|
941
|
+
- **Writes** flow through `Textus::Write::{Put,Delete,Mv,Accept,Reject,Publish,RefreshWorker}`. Permission checks happen at the use-case layer (via `Domain::Authorizer#authorize_write!`); the audit-append invariant lives in `Textus::Envelope::IO::Writer`.
|
|
942
|
+
- `Textus::Call` is the slim per-invocation record: `role`, `correlation_id`, `now`, `dry_run`. Ports come from `Textus::Container`, not from the Call.
|
|
943
|
+
- `Textus::Store` is the composition root and verb dispatcher. CLI verbs and the
|
|
944
|
+
MCP gate call `store.<verb>(..., role:)` (or `store.as(role).<verb>(...)`).
|
|
945
|
+
Verbs are looked up in the static `Textus::Dispatcher::VERBS` table; adding a
|
|
946
|
+
use case is a single entry in `VERBS` plus the class.
|
|
910
947
|
|
|
911
948
|
See `ARCHITECTURE.md` for an ASCII diagram and the full read-path walkthrough.
|
|
912
949
|
|
|
@@ -914,7 +951,7 @@ See `ARCHITECTURE.md` for an ASCII diagram and the full read-path walkthrough.
|
|
|
914
951
|
|
|
915
952
|
- **Locking on `put`:** the reference impl uses sha256 etags. Should the spec also define a file-lock fallback for systems where read-before-write is racy?
|
|
916
953
|
- **Schema imports:** can one schema reference another (`type: $ref: person`)?
|
|
917
|
-
- **Internationalization:** non-ASCII in keys? Spec currently restricts segments to `[a-z0-
|
|
954
|
+
- **Internationalization:** non-ASCII in keys? Spec currently restricts segments to `[a-z0-9][a-z0-9-]*`. Revisit if community wants Unicode.
|
|
918
955
|
- **Generated content in `derived/`:** the spec says `schema: null` is allowed, but should there be a separate marker (`generated: true`) for clarity?
|
|
919
956
|
|
|
920
957
|
## 15. Implementation checklist
|
data/docs/conventions.md
CHANGED
|
@@ -126,6 +126,16 @@ Build always uses the pure path; injecting refresh into materialization caused t
|
|
|
126
126
|
|
|
127
127
|
For multi-writer environments, **always pass `if_etag`** on `put`. The gem treats etag-less writes as last-writer-wins on purpose (single-writer scripts, fresh-file creation), but anything resembling a daemon or a long-running agent should round-trip the etag.
|
|
128
128
|
|
|
129
|
+
## Application layering
|
|
130
|
+
|
|
131
|
+
The application layer is organised around three shapes — `Manifest` as a composition record, a single `Container` capability record handed to every use case, and a split envelope reader/writer. See [ADR 0018](architecture/decisions/0018-manifest-carving.md), [ADR 0017](architecture/decisions/0017-envelope-io-split.md), [ADR 0022](architecture/decisions/0022-container-call-dispatcher.md), and [ADR 0023](architecture/decisions/0023-uniform-use-case-shape.md).
|
|
132
|
+
|
|
133
|
+
- **`Manifest` is a composition record** (`Data.define(:data, :resolver, :policy, :rules)`). Reach individual concerns through the field accessors: `manifest.data.entries`, `manifest.policy.permission_for(zone)`, `manifest.resolver.resolve(key)`, `manifest.rules.for(key)`.
|
|
134
|
+
- **Use cases are plain `(container:, call:)` classes.** Each is a single class under `lib/textus/{read,write,maintenance}/` with `def initialize(container:, call:)` and a `#call(...)` method; verbs are looked up in the static `Textus::Dispatcher::VERBS` table. `Container` is a `Data.define` record bundling the wired ports + manifest (`manifest`, `file_store`, `schemas`, `root`, `audit_log`, `events`, `rpc`, `authorizer`); `Call` is the immutable per-invocation value (`role`, `correlation_id`, `now`, `dry_run`). A use case that emits events derives its `Hooks::Context` from `(container, call)` — nothing is injected. Use cases pull only what they need into ivars; nobody passes the raw `Store` around the application layer. `store.as(role)` returns a `RoleScope` that forwards verbs to the dispatcher.
|
|
135
|
+
- **Write path is split**: `Envelope::IO::Reader` owns read/parse (existing-uid lookup, raw read, parse), and `Envelope::IO::Writer` owns put/delete/move + the audit-append invariant (every public method's final action is `@audit_log.append(...)`).
|
|
136
|
+
|
|
137
|
+
The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/3`) are unchanged.
|
|
138
|
+
|
|
129
139
|
## Pairing with other tools
|
|
130
140
|
|
|
131
141
|
- **MCP servers**: a thin server that exposes `textus get` and `textus put` as tools is the recommended way to give Claude/agents access. Don't bake MCP into this gem.
|
data/lib/textus/boot.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
"edit files in identity/working zones, then 'textus put KEY --as=#{name}'"
|
|
27
27
|
end,
|
|
28
28
|
proposer: lambda do |name, manifest|
|
|
29
|
-
authority = manifest.roles_with_kind(:accept_authority).first || "accept_authority"
|
|
29
|
+
authority = manifest.policy.roles_with_kind(:accept_authority).first || "accept_authority"
|
|
30
30
|
"propose changes by writing review.* entries with --as=#{name} and a 'proposal:' frontmatter block; " \
|
|
31
31
|
"the #{authority} role runs 'textus accept' to apply"
|
|
32
32
|
end,
|
|
@@ -39,7 +39,7 @@ module Textus
|
|
|
39
39
|
}.freeze
|
|
40
40
|
|
|
41
41
|
def self.write_flows_for(manifest)
|
|
42
|
-
manifest.role_mapping.each_with_object({}) do |(name, kind), acc|
|
|
42
|
+
manifest.policy.role_mapping.each_with_object({}) do |(name, kind), acc|
|
|
43
43
|
tmpl = WRITE_FLOW_TEMPLATES[kind]
|
|
44
44
|
acc[name] = tmpl.call(name, manifest) if tmpl
|
|
45
45
|
end
|
|
@@ -120,11 +120,11 @@ module Textus
|
|
|
120
120
|
"summary" => "delta since cursor — changed entries, stale, pending review, doctor summary" },
|
|
121
121
|
].freeze
|
|
122
122
|
|
|
123
|
-
def self.agent_quickstart(manifest,
|
|
124
|
-
proposer_roles = manifest.roles_with_kind(:proposer)
|
|
123
|
+
def self.agent_quickstart(manifest, audit_log)
|
|
124
|
+
proposer_roles = manifest.policy.roles_with_kind(:proposer)
|
|
125
125
|
agent_role = proposer_roles.first
|
|
126
126
|
|
|
127
|
-
writable_zones = manifest.zones.each_with_object([]) do |(zname, writers), acc|
|
|
127
|
+
writable_zones = manifest.data.zones.each_with_object([]) do |(zname, writers), acc|
|
|
128
128
|
acc << zname if agent_role && writers.include?(agent_role)
|
|
129
129
|
end
|
|
130
130
|
|
|
@@ -135,7 +135,7 @@ module Textus
|
|
|
135
135
|
"write_verbs" => agent_role ? ["put KEY --as=#{agent_role} --stdin"] : [],
|
|
136
136
|
"writable_zones" => writable_zones,
|
|
137
137
|
"propose_zone" => propose_zone,
|
|
138
|
-
"latest_seq" =>
|
|
138
|
+
"latest_seq" => audit_log.latest_seq,
|
|
139
139
|
}
|
|
140
140
|
end
|
|
141
141
|
|
|
@@ -144,29 +144,30 @@ module Textus
|
|
|
144
144
|
"role_resolution" => {
|
|
145
145
|
"summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
|
|
146
146
|
"default 'human'",
|
|
147
|
-
"roles" => manifest.role_mapping.keys,
|
|
147
|
+
"roles" => manifest.policy.role_mapping.keys,
|
|
148
148
|
"ref" => "SPEC.md §5",
|
|
149
149
|
},
|
|
150
150
|
)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
def self.
|
|
153
|
+
def self.build(container:)
|
|
154
|
+
manifest = container.manifest
|
|
154
155
|
{
|
|
155
156
|
"protocol" => PROTOCOL_ID,
|
|
156
|
-
"store_root" =>
|
|
157
|
-
"zones" => zones_for(
|
|
158
|
-
"entries" => entries_for(
|
|
159
|
-
"hooks" =>
|
|
160
|
-
"write_flows" => write_flows_for(
|
|
157
|
+
"store_root" => container.root,
|
|
158
|
+
"zones" => zones_for(manifest),
|
|
159
|
+
"entries" => entries_for(manifest),
|
|
160
|
+
"hooks" => hooks_for_container(container),
|
|
161
|
+
"write_flows" => write_flows_for(manifest),
|
|
161
162
|
"cli_verbs" => CLI_VERBS.map(&:dup),
|
|
162
|
-
"agent_protocol" => agent_protocol(
|
|
163
|
-
"agent_quickstart" => agent_quickstart(
|
|
163
|
+
"agent_protocol" => agent_protocol(manifest),
|
|
164
|
+
"agent_quickstart" => agent_quickstart(manifest, container.audit_log),
|
|
164
165
|
"docs" => { "spec" => "SPEC.md", "example" => "examples/claude-plugin/" },
|
|
165
166
|
}
|
|
166
167
|
end
|
|
167
168
|
|
|
168
|
-
def self.zones_for(
|
|
169
|
-
|
|
169
|
+
def self.zones_for(manifest)
|
|
170
|
+
manifest.data.zones.map do |name, writers|
|
|
170
171
|
row = { "name" => name, "writers" => Array(writers) }
|
|
171
172
|
purpose = ZONE_PURPOSES[name]
|
|
172
173
|
row["purpose"] = purpose if purpose
|
|
@@ -174,9 +175,9 @@ module Textus
|
|
|
174
175
|
end
|
|
175
176
|
end
|
|
176
177
|
|
|
177
|
-
def self.entries_for(
|
|
178
|
-
|
|
179
|
-
derived =
|
|
178
|
+
def self.entries_for(manifest)
|
|
179
|
+
manifest.data.entries.map do |e|
|
|
180
|
+
derived = manifest.policy.zone_kinds(e.zone).include?(:generator)
|
|
180
181
|
{
|
|
181
182
|
"key" => e.key,
|
|
182
183
|
"zone" => e.zone,
|
|
@@ -192,16 +193,17 @@ module Textus
|
|
|
192
193
|
end
|
|
193
194
|
end
|
|
194
195
|
|
|
195
|
-
def self.
|
|
196
|
-
|
|
196
|
+
def self.hooks_for_container(container)
|
|
197
|
+
hooks_for_container_internal(rpc: container.rpc, events: container.events)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.hooks_for_container_internal(rpc:, events:)
|
|
197
201
|
sections = {}
|
|
198
|
-
Hooks::
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
sections[event.to_s] = bus.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
|
|
204
|
-
end
|
|
202
|
+
Hooks::RpcRegistry::EVENTS.each_key do |event|
|
|
203
|
+
sections[event.to_s] = rpc.names(event).map(&:to_s).sort
|
|
204
|
+
end
|
|
205
|
+
Hooks::EventBus::EVENTS.each_key do |event|
|
|
206
|
+
sections[event.to_s] = events.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
|
|
205
207
|
end
|
|
206
208
|
sections
|
|
207
209
|
end
|
|
@@ -53,6 +53,10 @@ module Textus
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
module Pipeline
|
|
56
|
+
Deps = Data.define(
|
|
57
|
+
:manifest, :reader, :lister, :rpc, :template_loader, :transform_context, :inject_boot
|
|
58
|
+
)
|
|
59
|
+
|
|
56
60
|
def self.renderers
|
|
57
61
|
@renderers ||= {
|
|
58
62
|
"markdown" => Renderer::Markdown,
|
|
@@ -62,37 +66,34 @@ module Textus
|
|
|
62
66
|
}
|
|
63
67
|
end
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
def self.run(mentry:, manifest:, reader:, lister:, transform_resolver:, template_loader:,
|
|
67
|
-
transform_context: nil, inject_boot: nil)
|
|
69
|
+
def self.run(mentry:, deps:)
|
|
68
70
|
# 1. Load sources + project + reduce
|
|
69
71
|
data =
|
|
70
72
|
if mentry.is_a?(Textus::Manifest::Entry::Derived) && mentry.projection?
|
|
71
|
-
|
|
72
|
-
reader: reader,
|
|
73
|
+
Textus::Projection.new(
|
|
74
|
+
reader: deps.reader,
|
|
73
75
|
spec: mentry.source.to_h.transform_keys(&:to_s),
|
|
74
|
-
lister: lister,
|
|
75
|
-
|
|
76
|
-
transform_context: transform_context,
|
|
76
|
+
lister: deps.lister,
|
|
77
|
+
rpc: deps.rpc,
|
|
78
|
+
transform_context: deps.transform_context,
|
|
77
79
|
).run
|
|
78
80
|
else
|
|
79
81
|
{ "entries" => [], "count" => 0, "generated_at" => Time.now.utc.iso8601 }
|
|
80
82
|
end
|
|
81
|
-
data = data.merge("boot" => inject_boot.call) if mentry.inject_boot && inject_boot
|
|
83
|
+
data = data.merge("boot" => deps.inject_boot.call) if mentry.inject_boot && deps.inject_boot
|
|
82
84
|
|
|
83
85
|
# 2. Render
|
|
84
86
|
klass = renderers[mentry.format] or
|
|
85
87
|
raise UsageError.new("builder: unsupported format #{mentry.format.inspect} for '#{mentry.key}'")
|
|
86
|
-
bytes = klass.new(template_loader: template_loader).call(mentry: mentry, data: data)
|
|
88
|
+
bytes = klass.new(template_loader: deps.template_loader).call(mentry: mentry, data: data)
|
|
87
89
|
|
|
88
90
|
# 3. Write (idempotent: skip if only generated_at would differ)
|
|
89
|
-
target_path = Key::Path.resolve(manifest, mentry)
|
|
91
|
+
target_path = Key::Path.resolve(deps.manifest.data, mentry)
|
|
90
92
|
FileUtils.mkdir_p(File.dirname(target_path))
|
|
91
93
|
write_if_changed(target_path, bytes, mentry.format)
|
|
92
94
|
|
|
93
95
|
target_path
|
|
94
96
|
end
|
|
95
|
-
# rubocop:enable Metrics/ParameterLists
|
|
96
97
|
|
|
97
98
|
def self.write_if_changed(target_path, bytes, format)
|
|
98
99
|
if File.exist?(target_path)
|
data/lib/textus/call.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
# Immutable per-invocation value. Carries who is acting (role), the
|
|
5
|
+
# request correlation id, the wall clock, and the dry_run flag — the
|
|
6
|
+
# bits Use Cases need that are not part of the Container.
|
|
7
|
+
Call = Data.define(:role, :correlation_id, :now, :dry_run) do
|
|
8
|
+
def self.build(role:, correlation_id: nil, now: nil, dry_run: false)
|
|
9
|
+
new(
|
|
10
|
+
role: role.to_s,
|
|
11
|
+
correlation_id: correlation_id || SecureRandom.uuid,
|
|
12
|
+
now: now || Textus::Ports::Clock.now,
|
|
13
|
+
dry_run: dry_run,
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dry_run? = dry_run
|
|
18
|
+
|
|
19
|
+
def with_role(new_role)
|
|
20
|
+
self.class.new(
|
|
21
|
+
role: new_role.to_s,
|
|
22
|
+
correlation_id: correlation_id,
|
|
23
|
+
now: now,
|
|
24
|
+
dry_run: dry_run,
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -14,8 +14,8 @@ module Textus
|
|
|
14
14
|
option :limit, "--limit=N"
|
|
15
15
|
|
|
16
16
|
def call(store)
|
|
17
|
-
ops =
|
|
18
|
-
since_time = since && Textus::
|
|
17
|
+
ops = session_for(store)
|
|
18
|
+
since_time = since && Textus::Read::Audit.parse_since(since, now: Time.now)
|
|
19
19
|
rows = ops.audit(
|
|
20
20
|
key: key_filter,
|
|
21
21
|
zone: zone,
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
10
|
key = positional.shift or raise UsageError.new("blame requires a key")
|
|
11
|
-
rows =
|
|
11
|
+
rows = session_for(store).blame(key: key, limit: limit&.to_i)
|
|
12
12
|
emit({ "verb" => "blame", "key" => key, "rows" => rows })
|
|
13
13
|
end
|
|
14
14
|
end
|
data/lib/textus/cli/verb/boot.rb
CHANGED
|
@@ -7,9 +7,9 @@ module Textus
|
|
|
7
7
|
option :prefix, "--prefix=K"
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
|
-
Textus::
|
|
11
|
-
role = store.manifest.roles_with_kind(:generator).first || "builder"
|
|
12
|
-
ops =
|
|
10
|
+
Textus::Ports::BuildLock.with(root: store.root) do
|
|
11
|
+
role = store.manifest.policy.roles_with_kind(:generator).first || "builder"
|
|
12
|
+
ops = store.as(role)
|
|
13
13
|
result = ops.publish(prefix: prefix)
|
|
14
14
|
emit(result)
|
|
15
15
|
end
|
data/lib/textus/cli/verb/deps.rb
CHANGED
data/lib/textus/cli/verb/get.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
|
|
9
9
|
def call(store)
|
|
10
10
|
key = positional.shift or raise UsageError.new("get requires a key")
|
|
11
|
-
result =
|
|
11
|
+
result = session_for(store).get_or_refresh(key)
|
|
12
12
|
raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
|
|
13
13
|
|
|
14
14
|
emit(result.to_h_for_wire)
|
|
@@ -27,15 +27,14 @@ module Textus
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
30
|
-
callable = store.bus.rpc_callable(:resolve_intake, name)
|
|
31
30
|
|
|
32
31
|
begin
|
|
33
|
-
Timeout.timeout(Textus::
|
|
34
|
-
|
|
32
|
+
Timeout.timeout(Textus::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS) do
|
|
33
|
+
store.rpc.invoke(:resolve_intake, name, caps: nil, config: {}, args: args)
|
|
35
34
|
end
|
|
36
35
|
rescue Timeout::Error
|
|
37
36
|
raise UsageError.new(
|
|
38
|
-
"hook run '#{name}' exceeded #{Textus::
|
|
37
|
+
"hook run '#{name}' exceeded #{Textus::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
39
38
|
)
|
|
40
39
|
rescue Textus::Error
|
|
41
40
|
raise
|