textus 0.35.1 → 0.38.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 +109 -1
- data/README.md +7 -8
- data/SPEC.md +8 -2
- data/docs/conventions.md +1 -1
- data/lib/textus/boot.rb +41 -21
- data/lib/textus/cli/verb/mcp_serve.rb +8 -3
- data/lib/textus/cli/verb/propose.rb +28 -0
- data/lib/textus/cli/verb/pulse.rb +12 -3
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb.rb +3 -2
- data/lib/textus/contract.rb +106 -0
- data/lib/textus/cursor_store.rb +24 -0
- data/lib/textus/dispatcher.rb +3 -1
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/fetch_locks.rb +2 -2
- data/lib/textus/domain/policy/evaluation.rb +3 -6
- data/lib/textus/init.rb +4 -0
- data/lib/textus/layout.rb +41 -0
- data/lib/textus/maintenance/key_delete_prefix.rb +9 -0
- data/lib/textus/maintenance/key_mv_prefix.rb +10 -0
- data/lib/textus/maintenance/migrate.rb +9 -0
- data/lib/textus/maintenance/rule_lint.rb +8 -0
- data/lib/textus/maintenance/zone_mv.rb +10 -0
- data/lib/textus/mcp/catalog.rb +72 -0
- data/lib/textus/mcp/server.rb +8 -5
- data/lib/textus/mcp/session.rb +3 -20
- data/lib/textus/mcp/tool_schemas.rb +6 -62
- data/lib/textus/mcp/tools.rb +4 -119
- data/lib/textus/ports/audit_log.rb +17 -15
- data/lib/textus/ports/build_lock.rb +1 -2
- data/lib/textus/ports/fetch/lock.rb +1 -1
- data/lib/textus/read/audit.rb +3 -3
- data/lib/textus/read/boot.rb +6 -0
- data/lib/textus/read/get.rb +8 -0
- data/lib/textus/read/list.rb +8 -0
- data/lib/textus/read/pulse.rb +7 -0
- data/lib/textus/read/rules.rb +24 -0
- data/lib/textus/read/schema_envelope.rb +7 -0
- data/lib/textus/role.rb +6 -2
- data/lib/textus/session.rb +24 -0
- data/lib/textus/store.rb +11 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +1 -1
- data/lib/textus/write/delete.rb +1 -1
- data/lib/textus/write/fetch_all.rb +8 -0
- data/lib/textus/write/fetch_worker.rb +9 -1
- data/lib/textus/write/mv.rb +1 -1
- data/lib/textus/write/propose.rb +46 -0
- data/lib/textus/write/put.rb +13 -1
- data/lib/textus/write/reject.rb +1 -1
- data/lib/textus.rb +4 -0
- metadata +13 -4
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Read
|
|
3
|
+
# Effective rule set (fetch + guard) for a key. Was the inlined MCP
|
|
4
|
+
# `rules` tool; promoted to a first-class verb so MCP is a pure projection
|
|
5
|
+
# (ADR 0039).
|
|
6
|
+
class Rules
|
|
7
|
+
extend Textus::Contract::DSL
|
|
8
|
+
|
|
9
|
+
verb :rules
|
|
10
|
+
summary "Return effective rules for a key (fetch, guard, ...)."
|
|
11
|
+
surfaces :ruby, :mcp
|
|
12
|
+
arg :key, String, required: true, positional: true
|
|
13
|
+
|
|
14
|
+
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
+
@manifest = container.manifest
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(key)
|
|
19
|
+
set = @manifest.rules.for(key)
|
|
20
|
+
{ "fetch" => set.fetch&.to_h, "guard" => set.guard }.compact
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Read
|
|
3
3
|
class SchemaEnvelope
|
|
4
|
+
extend Textus::Contract::DSL
|
|
5
|
+
|
|
6
|
+
verb :schema
|
|
7
|
+
summary "Return the schema (field shape) for an entry's family, by key."
|
|
8
|
+
surfaces :ruby, :mcp
|
|
9
|
+
arg :key, String, required: true, positional: true
|
|
10
|
+
|
|
4
11
|
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
5
12
|
@manifest = container.manifest
|
|
6
13
|
@schemas = container.schemas
|
data/lib/textus/role.rb
CHANGED
|
@@ -2,9 +2,13 @@ module Textus
|
|
|
2
2
|
module Role
|
|
3
3
|
PATTERN = /\A[a-z][a-z0-9_-]*\z/
|
|
4
4
|
DEFAULT = "human".freeze
|
|
5
|
+
# The default acting identity for the MCP transport (ADR 0040): an agent
|
|
6
|
+
# over stdio proposes; it does not inherit the human's authority. CLI
|
|
7
|
+
# callers keep the `human` DEFAULT.
|
|
8
|
+
AGENT = "agent".freeze
|
|
5
9
|
|
|
6
|
-
def self.resolve(root:, flag: nil, env: ENV)
|
|
7
|
-
candidate = flag || env["TEXTUS_ROLE"] || read_file(root) ||
|
|
10
|
+
def self.resolve(root:, flag: nil, env: ENV, default: DEFAULT)
|
|
11
|
+
candidate = flag || env["TEXTUS_ROLE"] || read_file(root) || default
|
|
8
12
|
raise InvalidRole.new(candidate) unless candidate.match?(PATTERN)
|
|
9
13
|
|
|
10
14
|
candidate
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
# The agent session: per-connection (MCP), per-process (CLI), or per-loop
|
|
3
|
+
# (Ruby) orientation state — the audit cursor plus the manifest etag and
|
|
4
|
+
# propose_zone captured at boot. Immutable Data value; advance_cursor
|
|
5
|
+
# returns a new instance. ADR 0036.
|
|
6
|
+
Session = Data.define(:role, :cursor, :propose_zone, :manifest_etag) do
|
|
7
|
+
def advance_cursor(new_cursor) = with(cursor: new_cursor)
|
|
8
|
+
|
|
9
|
+
def check_etag!(observed_etag)
|
|
10
|
+
return if observed_etag == manifest_etag
|
|
11
|
+
|
|
12
|
+
raise Textus::MCP::ContractDrift.new(
|
|
13
|
+
"manifest changed (was #{short_etag(manifest_etag)}, now #{short_etag(observed_etag)}); re-run boot",
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# First 8 hex chars after the "sha256:" prefix — a stable short id for
|
|
20
|
+
# the drift diagnostic. Tolerates non-prefixed values (delete_prefix is
|
|
21
|
+
# a no-op when the prefix is absent).
|
|
22
|
+
def short_etag(etag) = etag.to_s.delete_prefix("sha256:")[0, 8]
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/textus/store.rb
CHANGED
|
@@ -50,6 +50,17 @@ module Textus
|
|
|
50
50
|
@container ||= Textus::Container.from_store(self)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# Build an agent Session oriented at the current cursor/manifest — the
|
|
54
|
+
# Ruby equivalent of an MCP `initialize`. ADR 0036.
|
|
55
|
+
def session(role:)
|
|
56
|
+
Textus::Session.new(
|
|
57
|
+
role: role,
|
|
58
|
+
cursor: audit_log.latest_seq,
|
|
59
|
+
propose_zone: manifest.policy.propose_zone_for(role),
|
|
60
|
+
manifest_etag: file_store.etag(File.join(root, "manifest.yaml")),
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
53
64
|
def as(role, dry_run: false, correlation_id: nil)
|
|
54
65
|
RoleScope.new(container: container, role: role, dry_run: dry_run, correlation_id: correlation_id)
|
|
55
66
|
end
|
data/lib/textus/version.rb
CHANGED
data/lib/textus/write/accept.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Textus
|
|
|
18
18
|
guard.for(:accept, target).check!(
|
|
19
19
|
Textus::Domain::Policy::Evaluation.new(
|
|
20
20
|
actor: @call.role, transition: :accept, origin: pending_key,
|
|
21
|
-
target: target, envelope: env,
|
|
21
|
+
target: target, envelope: env, manifest: @manifest
|
|
22
22
|
),
|
|
23
23
|
)
|
|
24
24
|
|
data/lib/textus/write/delete.rb
CHANGED
|
@@ -36,7 +36,7 @@ module Textus
|
|
|
36
36
|
def eval_for(transition, target_key:, envelope: nil)
|
|
37
37
|
Textus::Domain::Policy::Evaluation.new(
|
|
38
38
|
actor: @call.role, transition: transition, origin: nil,
|
|
39
|
-
target: target_key, envelope: envelope,
|
|
39
|
+
target: target_key, envelope: envelope, manifest: @manifest
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Write
|
|
3
3
|
class FetchAll
|
|
4
|
+
extend Textus::Contract::DSL
|
|
5
|
+
|
|
6
|
+
verb :fetch_all
|
|
7
|
+
summary "Fetch all stale quarantine entries, optionally scoped by zone/prefix."
|
|
8
|
+
surfaces :cli, :ruby, :mcp
|
|
9
|
+
arg :prefix, String
|
|
10
|
+
arg :zone, String
|
|
11
|
+
|
|
4
12
|
def initialize(container:, call:)
|
|
5
13
|
@container = container
|
|
6
14
|
@call = call
|
|
@@ -3,6 +3,14 @@ require "timeout"
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Write
|
|
5
5
|
class FetchWorker
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :fetch
|
|
9
|
+
summary "Run a fetch action for one quarantine entry."
|
|
10
|
+
surfaces :cli, :ruby, :mcp
|
|
11
|
+
arg :key, String, required: true, positional: true
|
|
12
|
+
response { |outcome| { "outcome" => outcome.class.name.split("::").last.downcase } }
|
|
13
|
+
|
|
6
14
|
FETCH_TIMEOUT_SECONDS = IntakeFetch::FETCH_TIMEOUT_SECONDS
|
|
7
15
|
|
|
8
16
|
def initialize(container:, call:)
|
|
@@ -101,7 +109,7 @@ module Textus
|
|
|
101
109
|
).for(:fetch, key).check!(
|
|
102
110
|
Textus::Domain::Policy::Evaluation.new(
|
|
103
111
|
actor: @call.role, transition: :fetch, origin: nil,
|
|
104
|
-
target: key, envelope: nil,
|
|
112
|
+
target: key, envelope: nil, manifest: @manifest
|
|
105
113
|
),
|
|
106
114
|
)
|
|
107
115
|
envelope = writer.put(
|
data/lib/textus/write/mv.rb
CHANGED
|
@@ -109,7 +109,7 @@ module Textus
|
|
|
109
109
|
def eval_for(transition, target_key:, envelope: nil)
|
|
110
110
|
Textus::Domain::Policy::Evaluation.new(
|
|
111
111
|
actor: @call.role, transition: transition, origin: nil,
|
|
112
|
-
target: target_key, envelope: envelope,
|
|
112
|
+
target: target_key, envelope: envelope, manifest: @manifest
|
|
113
113
|
)
|
|
114
114
|
end
|
|
115
115
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Write
|
|
3
|
+
# Queue a proposal: resolve the acting role's propose_zone, prefix the key,
|
|
4
|
+
# and write there via the Put verb. Was inlined in the MCP `propose` tool
|
|
5
|
+
# and the CLI propose verb; promoted to a first-class verb so all three
|
|
6
|
+
# transports share one implementation (ADR 0036, ADR 0039).
|
|
7
|
+
class Propose
|
|
8
|
+
extend Textus::Contract::DSL
|
|
9
|
+
|
|
10
|
+
verb :propose
|
|
11
|
+
summary "Write a proposal to the role's propose_zone. Auto-prefixes the key."
|
|
12
|
+
surfaces :cli, :ruby, :mcp
|
|
13
|
+
arg :key, String, required: true, positional: true,
|
|
14
|
+
description: "key relative to propose_zone, e.g. 'decisions.feature-x'"
|
|
15
|
+
arg :meta, Hash, required: true
|
|
16
|
+
arg :body, String
|
|
17
|
+
arg :content, Hash
|
|
18
|
+
response { |env| { "uid" => env.uid, "etag" => env.etag, "key" => env.key } }
|
|
19
|
+
|
|
20
|
+
def initialize(container:, call:)
|
|
21
|
+
@container = container
|
|
22
|
+
@call = call
|
|
23
|
+
@manifest = container.manifest
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# if_etag is intentionally absent: a proposal is always a fresh queue write.
|
|
27
|
+
def call(key, meta: nil, body: nil, content: nil)
|
|
28
|
+
zone = @manifest.policy.propose_zone_for(@call.role)
|
|
29
|
+
unless zone
|
|
30
|
+
raise Textus::Error.new(
|
|
31
|
+
"propose_forbidden",
|
|
32
|
+
"role '#{@call.role}' has no writable propose_zone",
|
|
33
|
+
details: { "role" => @call.role },
|
|
34
|
+
hint: "the manifest must define a queue zone and '#{@call.role}' must hold the 'propose' capability",
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Textus::Dispatcher.invoke(
|
|
39
|
+
:put, container: @container, call: @call,
|
|
40
|
+
args: ["#{zone}.#{key}"],
|
|
41
|
+
kwargs: { meta: meta || {}, body: body, content: content }
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/textus/write/put.rb
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Write
|
|
3
3
|
class Put
|
|
4
|
+
extend Textus::Contract::DSL
|
|
5
|
+
|
|
6
|
+
verb :put
|
|
7
|
+
summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
|
|
8
|
+
surfaces :cli, :ruby, :mcp
|
|
9
|
+
arg :key, String, required: true, positional: true
|
|
10
|
+
arg :meta, Hash, required: true
|
|
11
|
+
arg :body, String
|
|
12
|
+
arg :content, Hash
|
|
13
|
+
arg :if_etag, String
|
|
14
|
+
response { |env| { "uid" => env.uid, "etag" => env.etag } }
|
|
15
|
+
|
|
4
16
|
def initialize(container:, call:)
|
|
5
17
|
@container = container
|
|
6
18
|
@call = call
|
|
@@ -41,7 +53,7 @@ module Textus
|
|
|
41
53
|
def eval_for(transition, target_key:, envelope: nil)
|
|
42
54
|
Textus::Domain::Policy::Evaluation.new(
|
|
43
55
|
actor: @call.role, transition: transition, origin: nil,
|
|
44
|
-
target: target_key, envelope: envelope,
|
|
56
|
+
target: target_key, envelope: envelope, manifest: @manifest
|
|
45
57
|
)
|
|
46
58
|
end
|
|
47
59
|
|
data/lib/textus/write/reject.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Textus
|
|
|
13
13
|
guard.for(:reject, pending_key).check!(
|
|
14
14
|
Textus::Domain::Policy::Evaluation.new(
|
|
15
15
|
actor: @call.role, transition: :reject, origin: pending_key,
|
|
16
|
-
target: pending_key, envelope: nil,
|
|
16
|
+
target: pending_key, envelope: nil, manifest: @manifest
|
|
17
17
|
),
|
|
18
18
|
)
|
|
19
19
|
|
data/lib/textus.rb
CHANGED
|
@@ -20,6 +20,10 @@ loader.ignore(File.expand_path("textus/mcp/errors.rb", __dir__))
|
|
|
20
20
|
loader.setup
|
|
21
21
|
loader.eager_load
|
|
22
22
|
|
|
23
|
+
# Derive CLI_VERBS after eager_load so all contract-declaring files are present
|
|
24
|
+
# (boot.rb loads first alphabetically; Dispatcher contracts are declared later).
|
|
25
|
+
Textus::Boot::CLI_VERBS = Textus::Boot.build_cli_verbs.freeze
|
|
26
|
+
|
|
23
27
|
module Textus
|
|
24
28
|
@hook_mutex = Mutex.new
|
|
25
29
|
@hook_blocks = []
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.38.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -93,8 +93,9 @@ dependencies:
|
|
|
93
93
|
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '3.13'
|
|
96
|
-
description:
|
|
97
|
-
memory
|
|
96
|
+
description: A coordination space for humans, AI, and automation. Durable, multi-writer
|
|
97
|
+
project memory where each actor writes into its own lane, proposals cross a review
|
|
98
|
+
queue, and every change is audited.
|
|
98
99
|
email:
|
|
99
100
|
- patrick204nqh@gmail.com
|
|
100
101
|
executables:
|
|
@@ -147,6 +148,7 @@ files:
|
|
|
147
148
|
- lib/textus/cli/verb/mcp_serve.rb
|
|
148
149
|
- lib/textus/cli/verb/migrate.rb
|
|
149
150
|
- lib/textus/cli/verb/mv.rb
|
|
151
|
+
- lib/textus/cli/verb/propose.rb
|
|
150
152
|
- lib/textus/cli/verb/published.rb
|
|
151
153
|
- lib/textus/cli/verb/pulse.rb
|
|
152
154
|
- lib/textus/cli/verb/put.rb
|
|
@@ -164,6 +166,8 @@ files:
|
|
|
164
166
|
- lib/textus/cli/verb/where.rb
|
|
165
167
|
- lib/textus/cli/verb/zone_mv.rb
|
|
166
168
|
- lib/textus/container.rb
|
|
169
|
+
- lib/textus/contract.rb
|
|
170
|
+
- lib/textus/cursor_store.rb
|
|
167
171
|
- lib/textus/dispatcher.rb
|
|
168
172
|
- lib/textus/doctor.rb
|
|
169
173
|
- lib/textus/doctor/check.rb
|
|
@@ -234,6 +238,7 @@ files:
|
|
|
234
238
|
- lib/textus/key/distance.rb
|
|
235
239
|
- lib/textus/key/grammar.rb
|
|
236
240
|
- lib/textus/key/path.rb
|
|
241
|
+
- lib/textus/layout.rb
|
|
237
242
|
- lib/textus/maintenance.rb
|
|
238
243
|
- lib/textus/maintenance/key_delete_prefix.rb
|
|
239
244
|
- lib/textus/maintenance/key_mv_prefix.rb
|
|
@@ -261,6 +266,7 @@ files:
|
|
|
261
266
|
- lib/textus/manifest/rules.rb
|
|
262
267
|
- lib/textus/manifest/schema.rb
|
|
263
268
|
- lib/textus/mcp.rb
|
|
269
|
+
- lib/textus/mcp/catalog.rb
|
|
264
270
|
- lib/textus/mcp/errors.rb
|
|
265
271
|
- lib/textus/mcp/server.rb
|
|
266
272
|
- lib/textus/mcp/session.rb
|
|
@@ -292,6 +298,7 @@ files:
|
|
|
292
298
|
- lib/textus/read/pulse.rb
|
|
293
299
|
- lib/textus/read/rdeps.rb
|
|
294
300
|
- lib/textus/read/retainable.rb
|
|
301
|
+
- lib/textus/read/rules.rb
|
|
295
302
|
- lib/textus/read/schema_envelope.rb
|
|
296
303
|
- lib/textus/read/stale.rb
|
|
297
304
|
- lib/textus/read/uid.rb
|
|
@@ -303,6 +310,7 @@ files:
|
|
|
303
310
|
- lib/textus/schema.rb
|
|
304
311
|
- lib/textus/schema/tools.rb
|
|
305
312
|
- lib/textus/schemas.rb
|
|
313
|
+
- lib/textus/session.rb
|
|
306
314
|
- lib/textus/store.rb
|
|
307
315
|
- lib/textus/uid.rb
|
|
308
316
|
- lib/textus/version.rb
|
|
@@ -314,6 +322,7 @@ files:
|
|
|
314
322
|
- lib/textus/write/intake_fetch.rb
|
|
315
323
|
- lib/textus/write/materializer.rb
|
|
316
324
|
- lib/textus/write/mv.rb
|
|
325
|
+
- lib/textus/write/propose.rb
|
|
317
326
|
- lib/textus/write/publish.rb
|
|
318
327
|
- lib/textus/write/put.rb
|
|
319
328
|
- lib/textus/write/reject.rb
|
|
@@ -344,5 +353,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
344
353
|
requirements: []
|
|
345
354
|
rubygems_version: 3.6.9
|
|
346
355
|
specification_version: 4
|
|
347
|
-
summary: Reference implementation of the textus/
|
|
356
|
+
summary: Reference implementation of the textus/3 protocol.
|
|
348
357
|
test_files: []
|