textus 0.55.1 → 0.55.2
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 +1 -1
- data/README.md +9 -9
- data/SPEC.md +14 -13
- data/docs/architecture/README.md +3 -3
- data/docs/reference/conventions.md +5 -2
- data/lib/textus/boot.rb +64 -85
- data/lib/textus/{gate → dispatch}/binder.rb +8 -10
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +1 -1
- data/lib/textus/doctor/check.rb +8 -6
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/errors.rb +2 -0
- data/lib/textus/format/base.rb +36 -8
- data/lib/textus/format/json.rb +0 -21
- data/lib/textus/format/markdown.rb +0 -21
- data/lib/textus/format/yaml.rb +0 -21
- data/lib/textus/format.rb +16 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -5
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.rb +1 -1
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/port/audit_log.rb +6 -6
- data/lib/textus/port/build_lock.rb +1 -1
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/port/sentinel_store.rb +3 -3
- data/lib/textus/port/storage/file_store.rb +23 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +58 -2
- data/lib/textus/port/watcher_lock.rb +2 -2
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +3 -10
- data/lib/textus/store/container.rb +140 -10
- data/lib/textus/store/cursor.rb +1 -1
- data/lib/textus/store/{envelope → entry}/reader.rb +8 -4
- data/lib/textus/store/{envelope → entry}/writer.rb +53 -29
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +5 -3
- data/lib/textus/store/jobs/planner.rb +27 -7
- data/lib/textus/store/jobs/queue.rb +9 -1
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +1 -43
- data/lib/textus/store/jobs/sweep.rb +2 -2
- data/lib/textus/store/{geometry.rb → layout.rb} +19 -3
- data/lib/textus/store.rb +53 -30
- data/lib/textus/surface/cli/runner.rb +8 -9
- data/lib/textus/surface/cli/verb/doctor.rb +3 -2
- data/lib/textus/surface/cli/verb/get.rb +5 -3
- data/lib/textus/surface/cli/verb/put.rb +5 -3
- data/lib/textus/surface/mcp/catalog.rb +26 -62
- data/lib/textus/surface/mcp/errors.rb +0 -10
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/surface/mcp/server.rb +20 -31
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +5 -4
- data/lib/textus/value/etag.rb +1 -1
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +36 -16
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +1 -1
- data/lib/textus/workflow/runner.rb +10 -18
- data/lib/textus.rb +0 -64
- metadata +70 -70
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- data/lib/textus/meta.rb +0 -54
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68d4176e50949378dcf62b98617c28bcf6f553c196b24f9263a97d8d5c6f382f
|
|
4
|
+
data.tar.gz: 7de0c2d9ee56d71e09cc0821d19b8ffcc00e3573fccc20ef3e610988ec5ff43c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34322a4bfd59c817bdbaec436caba5cd8a25a7896065e16633270eb22bed7db20af339f45f7ee8ed632732f3679469328939d45490e3a6ce3528f654b00feba9
|
|
7
|
+
data.tar.gz: 6c4f94a584d8334e7dc68e543852bb687fa662c6e6eb1334cadf9d70ff01576552f71d773d462f03f3c4f0955496fe094c167df090880f473d3e850469f4c3cd
|
data/CHANGELOG.md
CHANGED
|
@@ -904,7 +904,7 @@ Protocol remains `textus/3`.
|
|
|
904
904
|
`refresh`, `refresh_stale`, `schema`, `rules`). Session state (cursor,
|
|
905
905
|
role, manifest_etag) held server-side. Manifest drift surfaces as
|
|
906
906
|
`ContractDrift` (-32001); cursor expiry as `CursorExpired` (-32002).
|
|
907
|
-
See [
|
|
907
|
+
See [ADR 0015](docs/architecture/decisions/0015-agent-gate-mcp.md).
|
|
908
908
|
- `examples/claude-plugin/.mcp.json` and migrated skills/commands/agents —
|
|
909
909
|
zero `textus <verb>` shell strings remain in plugin markdown.
|
|
910
910
|
|
data/README.md
CHANGED
|
@@ -39,7 +39,7 @@ flowchart LR
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
human -->|author| knowledge["knowledge<br/>(canon)"]
|
|
42
|
-
agent -->|keep|
|
|
42
|
+
agent -->|keep| scratchpad["scratchpad<br/>(workspace)"]
|
|
43
43
|
agent -->|propose| proposals["proposals<br/>(queue)"]
|
|
44
44
|
automation -->|drain| artifacts["artifacts<br/>(machine)"]
|
|
45
45
|
human -->|ingest| raw["raw<br/>(intake)"]
|
|
@@ -68,7 +68,7 @@ The point of those lanes is to **build context you can trust**. Place each lane
|
|
|
68
68
|
LOW TRUST HIGH TRUST
|
|
69
69
|
(unreviewed) (authoritative)
|
|
70
70
|
┌──────────────────────────┬───────────────────────────────┐
|
|
71
|
-
DURABLE │
|
|
71
|
+
DURABLE │ scratchpad │ knowledge ★ the goal │
|
|
72
72
|
(kept) │ agent's working truth │ canon — a human authors │
|
|
73
73
|
│ durable, but low-trust │ here · the context you ship │
|
|
74
74
|
├──────────────────────────┼───────────────────────────────┤
|
|
@@ -84,12 +84,12 @@ Without coordination, they overwrite each other and nothing remembers why. textu
|
|
|
84
84
|
|
|
85
85
|
```
|
|
86
86
|
knowledge/ author only — who you are, what you decide, how you sound
|
|
87
|
-
|
|
87
|
+
scratchpad/ keep only — agent's own durable lane (bytes climb to knowledge only via propose→accept)
|
|
88
88
|
proposals/ propose (agent+human) — proposals waiting on a human accept
|
|
89
89
|
artifacts/ converge only — machine-maintained: computed outputs + external inputs
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
An agent that tries to write directly into `knowledge/` gets `write_forbidden`. It writes to `proposals/` (to change authoritative content) or its own `
|
|
92
|
+
An agent that tries to write directly into `knowledge/` gets `write_forbidden`. It writes to `proposals/` (to change authoritative content) or its own `scratchpad/` (for working memory). You accept the good proposals; textus promotes them, records the move, and audits both halves. Stable per-entry `uid:` means a reorganization doesn't break references. A monotonic audit cursor (`textus pulse --since=N`) means the next session — possibly a different agent, possibly a different model — picks up exactly where the last one left off.
|
|
93
93
|
|
|
94
94
|
That's the load-bearing claim: **coordination is a protocol invariant, not a library convenience.**
|
|
95
95
|
|
|
@@ -156,7 +156,7 @@ roles:
|
|
|
156
156
|
|
|
157
157
|
lanes:
|
|
158
158
|
- { name: knowledge, kind: canon } # author — canonical truth
|
|
159
|
-
- { name:
|
|
159
|
+
- { name: scratchpad, kind: workspace } # keep — agent's own durable lane
|
|
160
160
|
- { name: proposals, kind: queue } # propose — proposals awaiting accept
|
|
161
161
|
- { name: artifacts, kind: machine } # converge — computed outputs + external inputs
|
|
162
162
|
```
|
|
@@ -170,7 +170,7 @@ lanes:
|
|
|
170
170
|
.gitignore # generated — ignores .state/ and any tracked:false entries
|
|
171
171
|
data/ # one dir per lane; kinds + capabilities are in the manifest above
|
|
172
172
|
knowledge/ # e.g. identity (knowledge.identity.*), voice, decisions, notes
|
|
173
|
-
|
|
173
|
+
scratchpad/
|
|
174
174
|
proposals/
|
|
175
175
|
artifacts/ # machine lane: computed outputs + external inputs
|
|
176
176
|
.state/ # disposable runtime state — gitignored, safe to delete (ADR 0038)
|
|
@@ -207,7 +207,7 @@ For a worked store — knowledge entries, a staged proposal, schemas, ERB templa
|
|
|
207
207
|
- **Agent loop.** `textus boot` orients a fresh session; `textus pulse --since=N` is the per-turn heartbeat (changed entries, pending proposals, index etag for catalog drift detection). ([docs/how-to/agents-mcp.md](docs/how-to/agents-mcp.md))
|
|
208
208
|
- **MCP surface.** The official `mcp` Ruby SDK drives the stdio JSON-RPC server; protocol version auto-negotiated up to `2025-11-25`. Wire textus into Claude Code, Cursor, or any MCP host in one config block.
|
|
209
209
|
- **`textus doctor`.** Health checks across schemas, workflow registrations, keys, sentinels, and the audit log.
|
|
210
|
-
- **`raw` lane and `ingest` verb.** Write-once intake lane for external URL bookmarks, files, and binary assets. Three source kinds (`url`/`file`/`asset`); daily key derivation;
|
|
210
|
+
- **`raw` lane and `ingest` verb.** Write-once intake lane for external URL bookmarks, files, and binary assets. Three source kinds (`url`/`file`/`asset`); daily key derivation; scratchpad stub per ingest. See "Intake and ingest" section below.
|
|
211
211
|
|
|
212
212
|
## CLI and lanes
|
|
213
213
|
|
|
@@ -281,8 +281,8 @@ textus ingest url agentskills-io-brainstorming \
|
|
|
281
281
|
# see what landed in the raw lane
|
|
282
282
|
textus list --lane=raw
|
|
283
283
|
|
|
284
|
-
# a
|
|
285
|
-
textus get
|
|
284
|
+
# a scratchpad stub was created alongside — annotate it
|
|
285
|
+
textus get scratchpad.notes.raw
|
|
286
286
|
```
|
|
287
287
|
|
|
288
288
|
Stale produced entries are re-materialised by `drain`, not by reads — `get` is a pure read (ADR 0089).
|
data/SPEC.md
CHANGED
|
@@ -57,7 +57,7 @@ You **shape your own memory structure** inside `.textus/`. The protocol manages
|
|
|
57
57
|
textus/4 names its concepts along six axes. Reviewers who internalize these can map any part of the spec to the right category:
|
|
58
58
|
|
|
59
59
|
- **Actor** — who is interacting: roles such as `human`, `agent`, `automation`, each holding a set of capabilities (`propose`, `author`, `keep`, `converge`).
|
|
60
|
-
- **Place** — where data lives: lanes such as `knowledge`, `
|
|
60
|
+
- **Place** — where data lives: lanes such as `knowledge`, `scratchpad`, `raw`, `proposals`, `artifacts`.
|
|
61
61
|
- **Thing** — what is stored: entries, fields, keys.
|
|
62
62
|
- **Operation** — how you act on things: RPC and CLI verbs (`get`, `put`, `drain`, `serve`, `ingest`, …).
|
|
63
63
|
- **Event** — what gets fired after an operation: pub-sub events (`:entry_written`, `:entry_produced`, `:entry_published`, …).
|
|
@@ -111,7 +111,7 @@ The root is `.textus/` at the project working directory. A typical tree:
|
|
|
111
111
|
sentinels/ # byte-copied publish bookkeeping (see §5.3)
|
|
112
112
|
data/ # ALL user content lives here
|
|
113
113
|
knowledge/ # lane: knowledge (kind: canon — author-holders write)
|
|
114
|
-
|
|
114
|
+
scratchpad/ # lane: scratchpad (kind: workspace — keep-holders write; agent's own durable lane)
|
|
115
115
|
proposals/ # lane: proposals (kind: queue — propose-holders write)
|
|
116
116
|
artifacts/ # lane: artifacts (kind: machine — converge-holders write)
|
|
117
117
|
raw/ # lane: raw (kind: raw — ingest-holders write; write-once)
|
|
@@ -149,7 +149,7 @@ roles:
|
|
|
149
149
|
lanes:
|
|
150
150
|
- name: knowledge
|
|
151
151
|
kind: canon
|
|
152
|
-
- name:
|
|
152
|
+
- name: scratchpad
|
|
153
153
|
kind: workspace
|
|
154
154
|
owner: agent # optional, informational — agent's own lane
|
|
155
155
|
desc: "agent's durable working memory; bytes climb to knowledge only via propose→accept"
|
|
@@ -241,7 +241,7 @@ Default scaffold — Setup-1 (roles `human=[author, propose, ingest]`, `agent=[p
|
|
|
241
241
|
| Lane | `kind` | Required capability | Writable by (default) | Use case |
|
|
242
242
|
|---|---|---|---|---|
|
|
243
243
|
| `knowledge` | `canon` | `author` | `human` | Authored truth: identity, voice, decisions, network. |
|
|
244
|
-
| `
|
|
244
|
+
| `scratchpad` | `workspace` | `keep` | `agent` | Agent's own durable working memory. Bytes climb to `knowledge` only via propose→accept. |
|
|
245
245
|
| `proposals` | `queue` | `propose` | `agent`, `human` | Proposals awaiting human review via `textus accept`. |
|
|
246
246
|
| `artifacts` | `machine` | `converge` | `automation` | Computed outputs produced by `drain` via the workflow DSL. |
|
|
247
247
|
| `raw` | `raw` | `ingest` | `human`, `agent`, `automation` | Write-once external source material: URL bookmarks, files, binary assets. |
|
|
@@ -284,7 +284,7 @@ Every successful write records the resolved role and a wall-clock timestamp in `
|
|
|
284
284
|
|
|
285
285
|
#### 5.1.1 Capabilities
|
|
286
286
|
|
|
287
|
-
Roles declare **capabilities** — verbs from a closed
|
|
287
|
+
Roles declare **capabilities** — verbs from a closed five-element set. A
|
|
288
288
|
manifest declares a `roles:` block mapping each role name to the capabilities
|
|
289
289
|
it holds via `can:`:
|
|
290
290
|
|
|
@@ -296,7 +296,7 @@ roles:
|
|
|
296
296
|
- { name: keeper, can: [keep] }
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
Capability allow-list: `propose`, `author`, `keep`, `converge`. The mapping from
|
|
299
|
+
Capability allow-list: `propose`, `author`, `keep`, `converge`, `ingest`. The mapping from
|
|
300
300
|
lane-kind to its required capability is a **bijection** (ADR 0091, which folded
|
|
301
301
|
the former `quarantine` + `derived` kinds back into one `machine` kind — undoing
|
|
302
302
|
the two-kind split of ADR 0090): each capability authorizes exactly one
|
|
@@ -308,8 +308,9 @@ lane-kind:
|
|
|
308
308
|
| `keep` | `workspace` |
|
|
309
309
|
| `propose` | `queue` |
|
|
310
310
|
| `converge` | `machine` |
|
|
311
|
+
| `ingest` | `raw` |
|
|
311
312
|
|
|
312
|
-
A manifest naming a folded capability — `
|
|
313
|
+
A manifest naming a folded capability — `build` or the pre-0088
|
|
313
314
|
spelling `fetch` — in a `can:` list is rejected at load with a hint pointing to
|
|
314
315
|
`converge` (ADR 0090, 0091, 0111).
|
|
315
316
|
|
|
@@ -425,7 +426,7 @@ The `raw` lane (`kind: raw`) is a write-once intake lane for external source mat
|
|
|
425
426
|
|
|
426
427
|
**`access` field** — entries MAY carry `source.access: public | private` (field is `maintained_by: human`). Set `private` for sources not safe to reproduce publicly.
|
|
427
428
|
|
|
428
|
-
**Notebook stub** — every ingest creates a `
|
|
429
|
+
**Notebook stub** — every ingest creates a `scratchpad.notes` stub with a backlink (`Ingested from raw.<key>`) so the agent or human can annotate the ingested material without touching the write-once record.
|
|
429
430
|
|
|
430
431
|
**Example — URL bookmark:**
|
|
431
432
|
|
|
@@ -689,7 +690,7 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
|
|
|
689
690
|
"schema_ref": "person",
|
|
690
691
|
"uid": "a1b2c3d4e5f60718",
|
|
691
692
|
"sources": [
|
|
692
|
-
"raw.2026.06.20.url-mcp-spec"
|
|
693
|
+
{ "key": "raw.2026.06.20.url-mcp-spec", "etag": "sha256:1a2b…", "suspended": false }
|
|
693
694
|
],
|
|
694
695
|
"stale": false,
|
|
695
696
|
"stale_reason": null,
|
|
@@ -700,7 +701,7 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
|
|
|
700
701
|
**Field rules:**
|
|
701
702
|
- `protocol` MUST be the exact string `textus/4`.
|
|
702
703
|
- `key` MUST be the canonical resolved key.
|
|
703
|
-
- `lane` MUST be one of the lanes declared in the manifest (`knowledge`, `
|
|
704
|
+
- `lane` MUST be one of the lanes declared in the manifest (`knowledge`, `scratchpad`, `proposals`, `artifacts`, `raw` in the default Setup-1 scaffold).
|
|
704
705
|
- `path` MUST be an absolute filesystem path.
|
|
705
706
|
- `format` MUST be one of `markdown`, `json`, `yaml`, `text` (§5.12). Absent envelopes are treated as `markdown` for back-compat.
|
|
706
707
|
- `body` is the raw on-disk bytes as a UTF-8 string for every format.
|
|
@@ -708,7 +709,7 @@ Every successful CLI response (`--output=json`) is a single JSON envelope:
|
|
|
708
709
|
- `etag` MUST be `sha256:<hex>` of the raw file bytes, computed identically for every format.
|
|
709
710
|
- `schema_ref` MAY be `null` for entries in subtrees with `schema: null`.
|
|
710
711
|
- `uid` is the stable Textus UID (§7) if the entry carries one, else `null`. Always present in the envelope.
|
|
711
|
-
- `sources` is an array of
|
|
712
|
+
- `sources` is an array of source objects. Each object has `key` (the referenced entry's key), `etag` (sha256 snapshot taken at write time, or absent when no snapshot exists), and `suspended` (`true` when the referenced entry's current on-disk etag differs from the stored snapshot — the source changed after this entry was last written). Present only when non-empty.
|
|
712
713
|
- `stale` is `true` when the entry's `source.ttl` has elapsed and the entry has not yet been re-materialised; `false` otherwise. Only populated for produced entries with a declared `ttl`; always `false` for other entries.
|
|
713
714
|
- `stale_reason` is a short human-readable string describing why the entry is stale (e.g. `"ttl_exceeded"`, `"never_fetched"`), or `null` when `stale` is `false`.
|
|
714
715
|
- `fetching` is `true` when a background re-pull is in flight for this entry; `false` otherwise. Callers observing `stale: true, fetching: true` SHOULD retry after a short delay.
|
|
@@ -791,7 +792,7 @@ All verbs accept `--output=json` and emit a canonical envelope (success or error
|
|
|
791
792
|
|
|
792
793
|
`read_verbs` is derived from the MCP verb catalog — the verbs the agent can actually call over its transport — so it lists the read/discovery verbs (`schema_show` for an entry's field shape, `rule_explain` for its retention/guard policy, and the graph reads `where`/`deps`/`rdeps`, ADR 0060) and never the CLI-only `audit`/`doctor`, nor `freshness` (the Ruby-only internal lifecycle scan, ADR 0085) (ADR 0056). An agent learns an entry's `_meta` shape by calling the `schema_show` verb before a `put`/`propose`, not by shelling out to a CLI. The graph reads `deps`/`rdeps` return a structured `{key, deps}`/`{key, rdeps}` envelope on every surface (CLI, Ruby, MCP) — a hash, not a bare array, consistent with the other structured read responses such as `where` (ADR 0060 amendment).
|
|
793
794
|
|
|
794
|
-
The agent's MCP write surface includes the single-key `key_delete` and `key_mv` tools alongside their bulk `key_delete_prefix`/`key_mv_prefix` cousins (ADR 0060 amendment; the single-key tools were renamed from `delete`/`mv` to share the `key_` family stem in ADR 0082, which also removed the `migrate` YAML-plan orchestrator — its `
|
|
795
|
+
The agent's MCP write surface includes the single-key `key_delete` and `key_mv` tools alongside their bulk `key_delete_prefix`/`key_mv_prefix` cousins (ADR 0060 amendment; the single-key tools were renamed from `delete`/`mv` to share the `key_` family stem in ADR 0082, which also removed the `migrate` YAML-plan orchestrator — its `data_mv`/`key_mv_prefix`/`key_delete_prefix` ops remain individually callable). The structural mutation verbs (`key_mv`, `key_mv_prefix`, `key_delete_prefix`, `data_mv`) accept `dry_run: true` as an opt-in preview that returns a Plan without mutating (ADR 0071). `drain` does not support `dry_run` (it is async-only, ADR 0110). Single-key `key_delete` additionally accepts an optional `if_etag` optimistic-concurrency check. The blast-radius reads (`where`/`deps`/`rdeps`) remain on MCP so an agent can look before it leaps. The promotion verbs `accept` and `reject` are also on MCP (ADR 0072): they are gated by the `author_held` capability floor, not by transport absence — a default-`agent` connection is refused, while a connection launched as a role holding `author` (`--as`/`TEXTUS_ROLE`/`.textus/role`, resolved once at launch per ADR 0040) can promote, closing the propose→accept loop over one transport. `drain` is also on MCP (ADR 0076, ADR 0087, ADR 0110): it is caller-agnostic and its produce jobs self-elevate — materialization always runs as the manifest's `converge`-capable actor regardless of the calling role, granting no authority over content (materialization is a pure, idempotent function of already-accepted canon, ADR 0070); the destructive retention sweep runs as the caller. Each produce job self-acquires the single-writer build lock, so a concurrent CLI, reactive, or background pass cannot collide with an MCP-triggered one — a held lock is a graceful soft-miss (ADR 0110).
|
|
795
796
|
|
|
796
797
|
`latest_seq` is the current high-water mark of the audit log; agents should use it as the starting cursor for `pulse`.
|
|
797
798
|
|
|
@@ -819,7 +820,7 @@ The agent's MCP write surface includes the single-key `key_delete` and `key_mv`
|
|
|
819
820
|
|
|
820
821
|
`if_etag` is optional on both `put` and `key_delete`. When provided, the write fails with `etag_mismatch` if the on-disk file's etag differs. When omitted, the write is unconditional (last-writer-wins).
|
|
821
822
|
|
|
822
|
-
The lifecycle scan
|
|
823
|
+
The lifecycle scan runs per-entry at `get` time — each `get` response carries `stale`/`stale_reason` when the entry has a TTL rule. ADR 0085 removed the standalone `freshness` verb; human drill-down into a single entry's verdict is `textus get KEY` (carries `stale`/`stale_reason`) plus `textus rule_explain KEY` (the `source.ttl` and retention policy). `pulse` does not include a `stale` list. `textus drain` enqueues the convergence jobs — produce every in-scope derived entry, re-pull every stale intake entry, and a retention sweep — then drains the queue to empty (§5.11). Convergence is async-only (ADR 0110): there is no `--dry-run`.
|
|
823
824
|
|
|
824
825
|
`textus accept K --as=human` promotes a pending entry into its target lane: it copies the patch body into the target key, deletes the pending entry, and writes one audit line per side (§audit). Only a role holding the `author` capability (the trust anchor — `human` by default) may invoke `accept`.
|
|
825
826
|
|
data/docs/architecture/README.md
CHANGED
|
@@ -41,7 +41,7 @@ surfaces/cli/ CLI command generation from contracts
|
|
|
41
41
|
sources.rb CLI-only input acquisition (stdin parsing, file sourcing, coercion)
|
|
42
42
|
surfaces/mcp/ MCP server — stdio JSON-RPC 2.0, tools derived from contracts
|
|
43
43
|
surfaces/role_scope.rb
|
|
44
|
-
(Store#
|
|
44
|
+
(Store#with_role(role)) — holds (container, role, dry_run, correlation_id);
|
|
45
45
|
all verb methods injected via define_method in textus.rb
|
|
46
46
|
```
|
|
47
47
|
|
|
@@ -214,7 +214,7 @@ Both are built from a `Container` via named constructors — `Writer.from(contai
|
|
|
214
214
|
|
|
215
215
|
`Action::Get` is a **pure read** — it resolves the path, reads bytes, parses the envelope, and annotates a freshness verdict. It never ingests and never mutates.
|
|
216
216
|
|
|
217
|
-
1. CLI/MCP surface calls `store.
|
|
217
|
+
1. CLI/MCP surface calls `store.with_role(role).get(key)`.
|
|
218
218
|
2. `Gate#dispatch` runs Auth → `Action::Get#call`.
|
|
219
219
|
3. `Get` resolves path via `manifest.resolver`, reads bytes via `file_store`, parses the envelope, annotates `freshness` based on retention-rule TTL (if any).
|
|
220
220
|
|
|
@@ -222,7 +222,7 @@ Staleness is age-based (retention-rule TTL vs file mtime). A stale entry is retu
|
|
|
222
222
|
|
|
223
223
|
## Write path (`store.put(key, ...)`)
|
|
224
224
|
|
|
225
|
-
1. CLI/MCP surface calls `store.
|
|
225
|
+
1. CLI/MCP surface calls `store.with_role(role).put(key, meta:, body:)`.
|
|
226
226
|
2. `Gate#dispatch` runs Auth → `Action::Put#call`.
|
|
227
227
|
3. `Put` validates, resolves manifest entry, delegates to `Envelope::Writer#put` (serialize → schema-validate → etag-check → FileStore#write → AuditLog#append).
|
|
228
228
|
4. `WriteVerb#cascade_to_rdeps` enqueues `materialize` jobs for any entries with publish_tree that depend on the written key.
|
|
@@ -24,7 +24,7 @@ Recommended top-level layout — the spec allows alternatives, but this is what
|
|
|
24
24
|
templates/ # ERB templates for publish rendering
|
|
25
25
|
data/
|
|
26
26
|
knowledge/ # authored truth: identity, voice, decisions — author-holders write
|
|
27
|
-
|
|
27
|
+
scratchpad/ # agent's own durable lane (workspace) — keep-holders write
|
|
28
28
|
proposals/ # AI proposals awaiting accept (propose)
|
|
29
29
|
artifacts/ # computed outputs produced by drain — never edit by hand
|
|
30
30
|
raw/ # write-once external source material (ingest)
|
|
@@ -126,7 +126,10 @@ For multi-writer environments, **always pass `if_etag`** on `put`. The gem treat
|
|
|
126
126
|
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).
|
|
127
127
|
|
|
128
128
|
- **`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(lane)`, `manifest.resolver.resolve(key)`, `manifest.rules.for(key)`.
|
|
129
|
-
-
|
|
129
|
+
- **`Store` is the single dispatch module.** `Store` holds the `Container`, the active role, session state (cursor, propose_lane, contract_etag), and dispatches verbs through `method_missing`. Each verb call (`store.get(key: ...)`, `store.put(key:, meta:, body:)`) goes through `VerbRegistry`, binds inputs via `Dispatch::Binder`, routes through `Dispatch::Pipeline` (middleware chain: auth, cascade, audit), and reaches the handler. `Store#with_role(role)` returns a new `Store` bound to that role.
|
|
130
|
+
- **`Container` is a `Data.define` composition** of `Infrastructure` (`file_store`, `schemas`, `audit_log`, `job_store`, `geometry`) and `Coordination` (`manifest`, `workflows`, `pipeline`). Built in a single pass — no circular dependencies or lazy proxies.
|
|
131
|
+
- **Dispatch lives in `lib/textus/dispatch/`**: `Dispatch.dispatch` orchestrates verb dispatch, `Dispatch::Pipeline` runs middleware then invokes the handler, `Dispatch::Binder` handles input binding (wire format, CLI flags, session defaults). Auth predicates moved to `Manifest::Policy::Predicates`.
|
|
132
|
+
- **Handlers receive `container:`** and access I/O via `@container.pipeline.read/write/delete/move` and manifest config via `@container.manifest`.
|
|
130
133
|
- **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(...)`).
|
|
131
134
|
|
|
132
135
|
The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/4`) are unchanged.
|
data/lib/textus/boot.rb
CHANGED
|
@@ -57,12 +57,8 @@ module Textus
|
|
|
57
57
|
{ "name" => "pulse" },
|
|
58
58
|
].freeze
|
|
59
59
|
|
|
60
|
-
# verb token => contract.summary, for every Dispatcher verb that carries a
|
|
61
|
-
# contract. The single source for a verb's one-line summary (ADR 0039).
|
|
62
60
|
def self.contract_summaries
|
|
63
|
-
Textus::
|
|
64
|
-
.select { |k| k.respond_to?(:contract?) && k.contract? }
|
|
65
|
-
.to_h { |k| [k.contract.verb.to_s, k.contract.summary] }
|
|
61
|
+
Textus::VerbRegistry.registered.to_h { |s| [s.verb.to_s, s.summary] }
|
|
66
62
|
end
|
|
67
63
|
|
|
68
64
|
# Build the CLI verb catalog: each summary is derived from its contract when
|
|
@@ -76,69 +72,70 @@ module Textus
|
|
|
76
72
|
end
|
|
77
73
|
end
|
|
78
74
|
|
|
79
|
-
def self.
|
|
80
|
-
|
|
75
|
+
def self.build(container:)
|
|
76
|
+
etag = Textus::Value::Etag.for_contract(container.root)
|
|
77
|
+
latest_seq = container.audit_log.latest_seq
|
|
78
|
+
artifact = read_artifact_content(container, "artifacts.boot")
|
|
79
|
+
context = read_boot_context(container)
|
|
80
|
+
|
|
81
|
+
# Prefer pre-computed artifact (drain computes, boot reads).
|
|
82
|
+
# Fall back to inline manifest projection for stores that have not yet
|
|
83
|
+
# run drain (test fixtures, fresh inits).
|
|
84
|
+
stable = artifact || inline_boot_content(container.manifest, latest_seq)
|
|
85
|
+
|
|
86
|
+
if stable["agent_quickstart"]
|
|
87
|
+
stable = stable.merge(
|
|
88
|
+
"agent_quickstart" => stable["agent_quickstart"].merge("latest_seq" => latest_seq),
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
payload = {
|
|
93
|
+
"protocol" => PROTOCOL_ID,
|
|
94
|
+
"store_root" => container.root,
|
|
95
|
+
"contract_etag" => etag,
|
|
96
|
+
}.merge(stable)
|
|
97
|
+
payload["context"] = context if context
|
|
98
|
+
payload
|
|
99
|
+
end
|
|
81
100
|
|
|
82
|
-
|
|
101
|
+
def self.inline_boot_content(manifest, _latest_seq)
|
|
102
|
+
agent_role = manifest.policy.proposer_role
|
|
103
|
+
writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |ln, acc|
|
|
83
104
|
next unless agent_role
|
|
84
105
|
|
|
85
|
-
verb
|
|
106
|
+
verb = manifest.policy.verb_for_lane(ln)
|
|
86
107
|
writers = manifest.policy.roles_with_capability(verb)
|
|
87
|
-
acc <<
|
|
108
|
+
acc << ln if writers.include?(agent_role)
|
|
88
109
|
end
|
|
89
110
|
|
|
90
|
-
propose_lane = manifest.policy.propose_lane_for(agent_role)
|
|
91
|
-
|
|
92
111
|
{
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def self.recipes(manifest)
|
|
102
|
-
queue = manifest.policy.queue_lane
|
|
103
|
-
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
104
|
-
{
|
|
105
|
-
"read" => {
|
|
106
|
-
"purpose" => "find and read an entry",
|
|
107
|
-
"steps" => [
|
|
108
|
-
"list (lane:, prefix:) — discover keys without reading bodies",
|
|
109
|
-
"get KEY — returns the entry envelope",
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
"write" => {
|
|
113
|
-
"purpose" => "create or update an entry",
|
|
114
|
-
"steps" => [
|
|
115
|
-
"schema KEY — learn the _meta field shape (required, optional, field types) before writing",
|
|
116
|
-
"assemble an envelope: { _meta: {…}, body: \"…\" }",
|
|
117
|
-
"put KEY — persist it (role-gated); pass if_etag to guard a concurrent edit",
|
|
118
|
-
],
|
|
119
|
-
},
|
|
120
|
-
"propose" => {
|
|
121
|
-
"purpose" => "agent suggests a change for human review",
|
|
122
|
-
"agent_steps" => [
|
|
123
|
-
"propose KEY — writes the change into the #{queue} lane for review",
|
|
124
|
-
],
|
|
125
|
-
"human_steps" => [
|
|
126
|
-
"accept #{queue}.KEY — promotes the proposal into its target lane",
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
"drain" => {
|
|
130
|
-
"purpose" => "keep the machine-maintained lanes fresh — re-pull stale intake entries from their declared source",
|
|
131
|
-
"steps" => [
|
|
132
|
-
"pulse — its `stale` list names entries past their ttl",
|
|
133
|
-
"drain (lane: #{feeds}) — re-pull the stale entries",
|
|
134
|
-
],
|
|
112
|
+
"lanes" => lanes_for(manifest),
|
|
113
|
+
"agent_quickstart" => {
|
|
114
|
+
"read_verbs" => Textus::Surface::MCP::Catalog.read_verbs,
|
|
115
|
+
"write_verbs" => agent_role ? Textus::Surface::MCP::Catalog.write_verbs : [],
|
|
116
|
+
"writable_lanes" => writable_lanes,
|
|
117
|
+
"propose_lane" => manifest.policy.propose_lane_for(agent_role),
|
|
135
118
|
},
|
|
119
|
+
"agent_protocol" => agent_protocol(manifest),
|
|
136
120
|
}
|
|
137
121
|
end
|
|
138
122
|
|
|
139
123
|
def self.agent_protocol(manifest)
|
|
124
|
+
queue = manifest.policy.queue_lane
|
|
125
|
+
feeds = lane_label(manifest, :machine, "the machine lane")
|
|
140
126
|
AGENT_PROTOCOL_TEMPLATE.merge(
|
|
141
|
-
"recipes" =>
|
|
127
|
+
"recipes" => {
|
|
128
|
+
"read" => { "purpose" => "find and read an entry",
|
|
129
|
+
"steps" => ["list (lane:, prefix:) — discover keys", "get KEY — returns the entry envelope"] },
|
|
130
|
+
"write" => { "purpose" => "create or update an entry",
|
|
131
|
+
"steps" => ["schema KEY — learn field shape", "put KEY — persist it (role-gated)"] },
|
|
132
|
+
"propose" => { "purpose" => "agent suggests a change for human review",
|
|
133
|
+
"agent_steps" => ["propose KEY — writes to #{queue} lane"],
|
|
134
|
+
"human_steps" => ["accept #{queue}.KEY — promotes to target lane"] },
|
|
135
|
+
"drain" => { "purpose" => "keep machine lanes fresh",
|
|
136
|
+
"steps" => ["pulse — stale list names overdue entries",
|
|
137
|
+
"drain (lane: #{feeds}) — re-pull stale entries"] },
|
|
138
|
+
},
|
|
142
139
|
"role_resolution" => {
|
|
143
140
|
"summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
|
|
144
141
|
"then a transport default ('human' for CLI, 'agent' for MCP)",
|
|
@@ -148,28 +145,23 @@ module Textus
|
|
|
148
145
|
)
|
|
149
146
|
end
|
|
150
147
|
|
|
151
|
-
def self.
|
|
152
|
-
manifest
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
"
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"orientation" => read_artifact_content(container, "artifacts.config.orientation"),
|
|
162
|
-
"context" => read_boot_context(container),
|
|
163
|
-
"agent_protocol" => agent_protocol(manifest),
|
|
164
|
-
}.compact
|
|
148
|
+
def self.lanes_for(manifest)
|
|
149
|
+
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
150
|
+
verb = manifest.policy.verb_for_lane(name)
|
|
151
|
+
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
152
|
+
kind = manifest.policy.declared_kind(name)
|
|
153
|
+
row["kind"] = kind.to_s if kind
|
|
154
|
+
purpose = manifest.data.lane_descs[name]
|
|
155
|
+
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
156
|
+
row
|
|
157
|
+
end
|
|
165
158
|
end
|
|
166
159
|
|
|
167
160
|
def self.read_artifact_content(container, key)
|
|
168
161
|
res = container.manifest.resolver.resolve(key)
|
|
169
162
|
return nil unless res.path && File.exist?(res.path)
|
|
170
163
|
|
|
171
|
-
|
|
172
|
-
env = Textus::Action::Get.call(container: container, call: call, key: key)
|
|
164
|
+
env = Textus::Store::Entry::Reader.from(container: container).read(key)
|
|
173
165
|
env&.content
|
|
174
166
|
rescue Textus::Error
|
|
175
167
|
nil
|
|
@@ -179,24 +171,11 @@ module Textus
|
|
|
179
171
|
res = container.manifest.resolver.resolve("knowledge.boot")
|
|
180
172
|
return nil unless res.path && File.exist?(res.path)
|
|
181
173
|
|
|
182
|
-
|
|
183
|
-
env = Textus::Action::Get.call(container: container, call: call, key: "knowledge.boot")
|
|
174
|
+
env = Textus::Store::Entry::Reader.from(container: container).read("knowledge.boot")
|
|
184
175
|
body = env&.body&.strip
|
|
185
176
|
body.nil? || body.empty? ? nil : body
|
|
186
177
|
rescue Textus::Error
|
|
187
178
|
nil
|
|
188
179
|
end
|
|
189
|
-
|
|
190
|
-
def self.lanes_for(manifest)
|
|
191
|
-
manifest.data.declared_lane_kinds.keys.map do |name|
|
|
192
|
-
verb = manifest.policy.verb_for_lane(name)
|
|
193
|
-
row = { "name" => name, "writers" => manifest.policy.roles_with_capability(verb) }
|
|
194
|
-
kind = manifest.policy.declared_kind(name)
|
|
195
|
-
row["kind"] = kind.to_s if kind
|
|
196
|
-
purpose = manifest.data.lane_descs[name]
|
|
197
|
-
row["purpose"] = purpose if purpose && !purpose.empty?
|
|
198
|
-
row
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
180
|
end
|
|
202
181
|
end
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
|
|
3
|
-
# Raised when a required arg is absent from the bound input. Surface
|
|
4
|
-
# adapters translate this to their native error (MCP ToolError, CLI
|
|
5
|
-
# UsageError); a direct Ruby call lets it surface as-is.
|
|
2
|
+
module Dispatch
|
|
6
3
|
class MissingArgs < Textus::Error
|
|
7
4
|
attr_reader :spec, :missing
|
|
8
5
|
|
|
@@ -13,21 +10,22 @@ module Textus
|
|
|
13
10
|
end
|
|
14
11
|
end
|
|
15
12
|
|
|
16
|
-
# Validates and resolves a by-name inputs hash against a contract spec.
|
|
17
|
-
# Returns a flat hash with defaults and session_defaults filled in.
|
|
18
|
-
# Every caller receives the same shape — no positional/kwarg split.
|
|
19
13
|
module Binder
|
|
14
|
+
Pending = Data.define(:spec, :inputs)
|
|
15
|
+
|
|
20
16
|
module_function
|
|
21
17
|
|
|
22
|
-
def
|
|
18
|
+
def command(spec, inputs)
|
|
19
|
+
Pending.new(spec: spec, inputs: inputs)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def bind(spec, inputs)
|
|
23
23
|
missing = spec.required_args.reject { |a| inputs.key?(a.name) }
|
|
24
24
|
raise MissingArgs.new(spec, missing) unless missing.empty?
|
|
25
25
|
|
|
26
26
|
spec.args.each_with_object({}) do |a, h|
|
|
27
27
|
if inputs.key?(a.name)
|
|
28
28
|
h[a.name] = inputs[a.name]
|
|
29
|
-
elsif a.session_default && session
|
|
30
|
-
h[a.name] = session.public_send(a.session_default)
|
|
31
29
|
elsif !a.default.nil?
|
|
32
30
|
h[a.name] = a.default
|
|
33
31
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
module Contracts
|
|
4
|
+
GetEntry = Data.define(:key)
|
|
5
|
+
|
|
6
|
+
PutEntry = Data.define(:key, :meta, :body, :content, :if_etag)
|
|
7
|
+
|
|
8
|
+
ListKeys = Data.define(:prefix, :lane, :q, :schema)
|
|
9
|
+
|
|
10
|
+
DeleteKey = Data.define(:key, :if_etag)
|
|
11
|
+
|
|
12
|
+
MoveKey = Data.define(:old_key, :new_key, :if_etag, :dry_run)
|
|
13
|
+
|
|
14
|
+
ProposeEntry = Data.define(:key, :meta, :body, :content)
|
|
15
|
+
|
|
16
|
+
AcceptProposal = Data.define(:pending_key)
|
|
17
|
+
|
|
18
|
+
RejectProposal = Data.define(:pending_key)
|
|
19
|
+
|
|
20
|
+
EnqueueJob = Data.define(:type, :args)
|
|
21
|
+
|
|
22
|
+
AuditEntries = Data.define(:key, :lane, :role, :verb, :since, :seq_since, :correlation_id, :limit)
|
|
23
|
+
|
|
24
|
+
PulseEntries = Data.define(:since)
|
|
25
|
+
|
|
26
|
+
BlameEntry = Data.define(:key, :limit)
|
|
27
|
+
|
|
28
|
+
WhereEntry = Data.define(:key)
|
|
29
|
+
|
|
30
|
+
UidEntry = Data.define(:key)
|
|
31
|
+
|
|
32
|
+
DepsEntry = Data.define(:key)
|
|
33
|
+
|
|
34
|
+
RdepsEntry = Data.define(:key)
|
|
35
|
+
|
|
36
|
+
BootStore = Data.define
|
|
37
|
+
|
|
38
|
+
DoctorStore = Data.define(:checks)
|
|
39
|
+
|
|
40
|
+
PublishedEntries = Data.define
|
|
41
|
+
|
|
42
|
+
RuleExplain = Data.define(:key, :detail)
|
|
43
|
+
|
|
44
|
+
RuleList = Data.define
|
|
45
|
+
|
|
46
|
+
SchemaEnvelope = Data.define(:key)
|
|
47
|
+
|
|
48
|
+
DrainStore = Data.define(:prefix, :lane)
|
|
49
|
+
|
|
50
|
+
IngestEntry = Data.define(:kind, :slug, :url, :path, :lane, :label)
|
|
51
|
+
|
|
52
|
+
JobsAction = Data.define(:state, :action, :job_id)
|
|
53
|
+
|
|
54
|
+
RuleLint = Data.define(:candidate_yaml)
|
|
55
|
+
|
|
56
|
+
DataMv = Data.define(:from, :to, :dry_run)
|
|
57
|
+
|
|
58
|
+
KeyMvPrefix = Data.define(:from_prefix, :to_prefix, :dry_run)
|
|
59
|
+
|
|
60
|
+
KeyDeletePrefix = Data.define(:prefix, :dry_run)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Dispatch
|
|
3
|
+
class HandlerRegistry
|
|
4
|
+
def initialize
|
|
5
|
+
@handlers = {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def register(command_class, handler)
|
|
9
|
+
@handlers[command_class] = handler
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def for(command_class)
|
|
13
|
+
@handlers[command_class] || raise("no handler registered for #{command_class}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def registered?(command_class)
|
|
17
|
+
@handlers.key?(command_class)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|