textus 0.35.1 → 0.39.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -2
  3. data/README.md +34 -13
  4. data/SPEC.md +10 -4
  5. data/lib/textus/boot.rb +41 -21
  6. data/lib/textus/cli/verb/mcp_serve.rb +8 -3
  7. data/lib/textus/cli/verb/propose.rb +28 -0
  8. data/lib/textus/cli/verb/pulse.rb +12 -3
  9. data/lib/textus/cli/verb/schema.rb +1 -1
  10. data/lib/textus/cli/verb.rb +3 -2
  11. data/lib/textus/contract.rb +106 -0
  12. data/lib/textus/cursor_store.rb +24 -0
  13. data/lib/textus/dispatcher.rb +3 -1
  14. data/lib/textus/doctor/check/audit_log.rb +1 -1
  15. data/lib/textus/doctor/check/fetch_locks.rb +2 -2
  16. data/lib/textus/doctor/check/illegal_keys.rb +10 -4
  17. data/lib/textus/domain/policy/evaluation.rb +3 -6
  18. data/lib/textus/init.rb +4 -0
  19. data/lib/textus/layout.rb +41 -0
  20. data/lib/textus/maintenance/key_delete_prefix.rb +9 -0
  21. data/lib/textus/maintenance/key_mv_prefix.rb +10 -0
  22. data/lib/textus/maintenance/migrate.rb +9 -0
  23. data/lib/textus/maintenance/rule_lint.rb +8 -0
  24. data/lib/textus/maintenance/zone_mv.rb +10 -0
  25. data/lib/textus/manifest/entry/base.rb +5 -0
  26. data/lib/textus/manifest/entry/ignore_matcher.rb +46 -0
  27. data/lib/textus/manifest/entry/nested.rb +9 -2
  28. data/lib/textus/manifest/entry/validators/ignore.rb +28 -0
  29. data/lib/textus/manifest/entry/validators.rb +1 -0
  30. data/lib/textus/manifest/resolver.rb +2 -0
  31. data/lib/textus/manifest/schema.rb +1 -1
  32. data/lib/textus/mcp/catalog.rb +72 -0
  33. data/lib/textus/mcp/server.rb +8 -5
  34. data/lib/textus/mcp/session.rb +3 -20
  35. data/lib/textus/mcp/tool_schemas.rb +6 -62
  36. data/lib/textus/mcp/tools.rb +4 -119
  37. data/lib/textus/ports/audit_log.rb +17 -15
  38. data/lib/textus/ports/build_lock.rb +1 -2
  39. data/lib/textus/ports/fetch/lock.rb +1 -1
  40. data/lib/textus/read/audit.rb +3 -3
  41. data/lib/textus/read/boot.rb +6 -0
  42. data/lib/textus/read/get.rb +8 -0
  43. data/lib/textus/read/list.rb +8 -0
  44. data/lib/textus/read/pulse.rb +7 -0
  45. data/lib/textus/read/rules.rb +24 -0
  46. data/lib/textus/read/schema_envelope.rb +7 -0
  47. data/lib/textus/role.rb +6 -2
  48. data/lib/textus/session.rb +24 -0
  49. data/lib/textus/store.rb +11 -0
  50. data/lib/textus/version.rb +1 -1
  51. data/lib/textus/write/accept.rb +1 -1
  52. data/lib/textus/write/delete.rb +1 -1
  53. data/lib/textus/write/fetch_all.rb +8 -0
  54. data/lib/textus/write/fetch_worker.rb +9 -1
  55. data/lib/textus/write/mv.rb +1 -1
  56. data/lib/textus/write/propose.rb +46 -0
  57. data/lib/textus/write/put.rb +13 -1
  58. data/lib/textus/write/reject.rb +1 -1
  59. data/lib/textus.rb +4 -0
  60. metadata +15 -5
  61. data/docs/conventions.md +0 -148
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.35.1
4
+ version: 0.39.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: Storage convention and JSON wire protocol for agent-readable project
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:
@@ -106,7 +107,6 @@ files:
106
107
  - CHANGELOG.md
107
108
  - README.md
108
109
  - SPEC.md
109
- - docs/conventions.md
110
110
  - exe/textus
111
111
  - lib/textus.rb
112
112
  - lib/textus/boot.rb
@@ -147,6 +147,7 @@ files:
147
147
  - lib/textus/cli/verb/mcp_serve.rb
148
148
  - lib/textus/cli/verb/migrate.rb
149
149
  - lib/textus/cli/verb/mv.rb
150
+ - lib/textus/cli/verb/propose.rb
150
151
  - lib/textus/cli/verb/published.rb
151
152
  - lib/textus/cli/verb/pulse.rb
152
153
  - lib/textus/cli/verb/put.rb
@@ -164,6 +165,8 @@ files:
164
165
  - lib/textus/cli/verb/where.rb
165
166
  - lib/textus/cli/verb/zone_mv.rb
166
167
  - lib/textus/container.rb
168
+ - lib/textus/contract.rb
169
+ - lib/textus/cursor_store.rb
167
170
  - lib/textus/dispatcher.rb
168
171
  - lib/textus/doctor.rb
169
172
  - lib/textus/doctor/check.rb
@@ -234,6 +237,7 @@ files:
234
237
  - lib/textus/key/distance.rb
235
238
  - lib/textus/key/grammar.rb
236
239
  - lib/textus/key/path.rb
240
+ - lib/textus/layout.rb
237
241
  - lib/textus/maintenance.rb
238
242
  - lib/textus/maintenance/key_delete_prefix.rb
239
243
  - lib/textus/maintenance/key_mv_prefix.rb
@@ -246,6 +250,7 @@ files:
246
250
  - lib/textus/manifest/entry.rb
247
251
  - lib/textus/manifest/entry/base.rb
248
252
  - lib/textus/manifest/entry/derived.rb
253
+ - lib/textus/manifest/entry/ignore_matcher.rb
249
254
  - lib/textus/manifest/entry/intake.rb
250
255
  - lib/textus/manifest/entry/leaf.rb
251
256
  - lib/textus/manifest/entry/nested.rb
@@ -253,6 +258,7 @@ files:
253
258
  - lib/textus/manifest/entry/validators.rb
254
259
  - lib/textus/manifest/entry/validators/events.rb
255
260
  - lib/textus/manifest/entry/validators/format_matrix.rb
261
+ - lib/textus/manifest/entry/validators/ignore.rb
256
262
  - lib/textus/manifest/entry/validators/index_filename.rb
257
263
  - lib/textus/manifest/entry/validators/inject_boot.rb
258
264
  - lib/textus/manifest/entry/validators/publish_each.rb
@@ -261,6 +267,7 @@ files:
261
267
  - lib/textus/manifest/rules.rb
262
268
  - lib/textus/manifest/schema.rb
263
269
  - lib/textus/mcp.rb
270
+ - lib/textus/mcp/catalog.rb
264
271
  - lib/textus/mcp/errors.rb
265
272
  - lib/textus/mcp/server.rb
266
273
  - lib/textus/mcp/session.rb
@@ -292,6 +299,7 @@ files:
292
299
  - lib/textus/read/pulse.rb
293
300
  - lib/textus/read/rdeps.rb
294
301
  - lib/textus/read/retainable.rb
302
+ - lib/textus/read/rules.rb
295
303
  - lib/textus/read/schema_envelope.rb
296
304
  - lib/textus/read/stale.rb
297
305
  - lib/textus/read/uid.rb
@@ -303,6 +311,7 @@ files:
303
311
  - lib/textus/schema.rb
304
312
  - lib/textus/schema/tools.rb
305
313
  - lib/textus/schemas.rb
314
+ - lib/textus/session.rb
306
315
  - lib/textus/store.rb
307
316
  - lib/textus/uid.rb
308
317
  - lib/textus/version.rb
@@ -314,6 +323,7 @@ files:
314
323
  - lib/textus/write/intake_fetch.rb
315
324
  - lib/textus/write/materializer.rb
316
325
  - lib/textus/write/mv.rb
326
+ - lib/textus/write/propose.rb
317
327
  - lib/textus/write/publish.rb
318
328
  - lib/textus/write/put.rb
319
329
  - lib/textus/write/reject.rb
@@ -344,5 +354,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
344
354
  requirements: []
345
355
  rubygems_version: 3.6.9
346
356
  specification_version: 4
347
- summary: Reference implementation of the textus/1 protocol.
357
+ summary: Reference implementation of the textus/3 protocol.
348
358
  test_files: []
data/docs/conventions.md DELETED
@@ -1,148 +0,0 @@
1
- # Conventions
2
-
3
- > **Reference** · for integrators · **read when** you're shaping a `.textus/` tree and want the idiomatic choices
4
- > **SSoT for** idiomatic key naming, schema design, and automation integration · **reviewed** 2026-05 (v0.31)
5
-
6
- Guidelines for shaping a `.textus/` tree, naming keys, organising schemas, and integrating with build automation. The spec ([`../SPEC.md`](../SPEC.md)) defines what's enforceable; this document captures what's *idiomatic*.
7
-
8
- ## Key naming
9
-
10
- - **Segments are lowercase, kebab- or snake-case.** The grammar `^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$` is the hard limit. Prefer `acme-dashboard` over `acmedashboard` when there's a natural word break.
11
- - **Lead with the zone in the key path.** `working.projects.acme.dashboard`, not `projects.acme.dashboard`. The zone prefix makes it obvious from the key alone whether a write will be accepted.
12
- - **Mirror the directory structure.** If `working.projects.acme.dashboard` resolves to `working/projects/acme/dashboard.md`, do not invent shortcuts that diverge.
13
- - **Don't pluralise the leaf.** `working.network.org.jane`, not `working.network.org.janes`. Pluralise the container, not the entry.
14
-
15
- ## Zone layout
16
-
17
- Recommended top-level layout — the spec allows alternatives, but this is what tooling will default to:
18
-
19
- ```
20
- .textus/
21
- manifest.yaml
22
- schemas/ # YAML schema definitions
23
- zones/
24
- knowledge/ # authored truth: identity, voice, decisions — author-holders write
25
- notebook/ # agent's own durable lane (workspace) — keep-holders write
26
- feeds/ # automation-fed external inputs (fetch)
27
- proposals/ # AI proposals awaiting accept (propose)
28
- artifacts/ # generated by build automation — never edit by hand
29
- ```
30
-
31
- Inside `knowledge/`, group by **domain** (identity, people, projects, decisions, runbooks), not by file type or date. `knowledge.identity.*` is the convention for slow-changing identity facts. Inside `artifacts/`, group by **producer** (`artifacts/catalogs/`, `artifacts/indexes/`) so it's clear which build job owns what.
32
-
33
- ## Schema design
34
-
35
- - **One schema per entry type, not per directory.** `person.yaml`, `project.yaml`, `decision.yaml` — applied across multiple subtrees if the shape matches.
36
- - **Required = "this entry is meaningless without it."** Everything else is `optional`. Resist the urge to mark organisational metadata (like `tags`) required.
37
- - **Prefer `enum` over free-text** for low-cardinality fields (relationship type, status, severity). Agents are far better at picking from a list than at producing exact strings.
38
- - **Cap string lengths** with `max:` where the field has a natural bound (names, summaries). Skip for prose body — bodies are not schema-validated, only frontmatter is.
39
-
40
- ## Owner strings
41
-
42
- The `owner:` field in the manifest is **advisory metadata**, not an ACL. Use it to label *who's expected to write here*:
43
-
44
- - `textus:network` — humans curate
45
- - `agent:planner` — a specific named agent
46
- - `build:catalog-skills` — a specific build job
47
-
48
- Tooling around `git blame` or audit logs may filter on owner; the gem itself only echoes it back in envelopes.
49
-
50
- ## Derived entries
51
-
52
- A derived entry declares a `compute:` block with a `kind:` discriminator. Two kinds:
53
-
54
- **`compute: { kind: projection }`** — textus computes the entry on `textus build` from other store entries. Declarative; nothing shells out.
55
-
56
- ```yaml
57
- - key: artifacts.catalogs.people
58
- path: artifacts/catalogs/people.md
59
- zone: artifacts
60
- schema: null
61
- owner: build:catalog-people
62
- compute:
63
- kind: projection
64
- select: knowledge.network.org # prefix or list of prefixes
65
- pluck: [name, relationship, org]
66
- sort_by: name
67
- template: people.mustache # under .textus/templates/
68
- publish_to: [docs/people.md] # optional repo-relative byte-copy targets
69
- ```
70
-
71
- **`compute: { kind: external }`** — an external build tool (rake, just, a shell script) produces the file. textus never executes the `command:`; it only tracks `sources:` so `textus freshness` can compare source mtimes against the file's `_meta.generated.at`. The role running the build must hold `build` (default: `automation`).
72
-
73
- ```yaml
74
- - key: artifacts.catalogs.skills
75
- path: artifacts/catalogs/skills.md
76
- zone: artifacts
77
- owner: build:catalog-skills
78
- compute:
79
- kind: external
80
- command: "rake catalog:skills" # informational; the automation invokes it
81
- sources: [knowledge.projects, knowledge.network]
82
- ```
83
-
84
- The build automation is responsible for writing the `generated:` frontmatter block (`by`, `at`, `from`) when it produces the file. `generated.from` SHOULD match `compute.sources` — same list, recorded twice so a diff proves what was consumed.
85
-
86
- Full contract for both shapes is in [`../SPEC.md` §5.2.1 and §5.2.2](../SPEC.md). Transforms (`compute.transform:`) and per-leaf publishing (`publish_each:`) are also covered there.
87
-
88
- ## Intake and freshness
89
-
90
- External inputs land via `:resolve_intake` hooks, not shell commands. Each intake entry names a registered handler; fetch is on demand:
91
-
92
- ```sh
93
- textus fetch feeds.notion.roadmap --as=automation
94
- textus fetch stale --zone=feeds --as=automation # everything past its TTL
95
- ```
96
-
97
- Freshness budgets live in the top-level `rules:` block, matched by glob:
98
-
99
- ```yaml
100
- rules:
101
- - match: feeds.notion.**
102
- fetch: { ttl: 6h, on_stale: warn } # warn | sync | timed_sync
103
- ```
104
-
105
- A typical scheduled-fetch integration shells the `fetch stale` sweep itself:
106
-
107
- ```sh
108
- textus fetch stale --zone=intake --as=automation # in cron / CI
109
- ```
110
-
111
- See [`./zones.md` §6](zones.md) for the full intake contract and [`./events.md`](events.md) for writing custom handlers.
112
-
113
- ### Read vs. fetch
114
-
115
- There are two read operations, and the difference matters in custom code:
116
-
117
- | Operation | Triggers fetch? | Use for |
118
- |-----------|-----------------|---------|
119
- | `ops.get` | No — pure read of on-disk envelope + freshness verdict | build / materialization paths, schema tooling, any context where a side-effecting read would surprise the caller |
120
- | `ops.get_or_fetch` | Yes — composes `get` with the orchestrator per the rule's `on_stale` policy | interactive reads (`textus get`, dashboards) where the caller wants the freshest envelope obtainable |
121
-
122
- Build always uses the pure path; injecting fetch into materialization caused the cascading-staleness incident behind issue #59. Pick `get_or_fetch` only when you genuinely want side effects on read.
123
-
124
- ## Body content
125
-
126
- - **Bodies are Markdown.** Headings, lists, code fences — whatever a human or agent finds useful.
127
- - **The schema does not validate the body.** If a field belongs in structured data, put it in frontmatter, not the body.
128
- - **Keep entries short.** If a project entry hits 500 lines, it probably wants to be split into sub-entries (e.g. `working.projects.acme.dashboard` + `working.projects.acme.api`) rather than one mega-document.
129
-
130
- ## Concurrency
131
-
132
- 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.
133
-
134
- ## Application layering
135
-
136
- 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).
137
-
138
- - **`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)`.
139
- - **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.
140
- - **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(...)`).
141
-
142
- The user-facing CLI surface, the wire envelope shape, and the protocol version (`textus/3`) are unchanged.
143
-
144
- ## Pairing with other tools
145
-
146
- - **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.
147
- - **Vector stores**: index `body` content into a vector store if you want fuzzy retrieval. `frontmatter` stays in textus as the source of truth for deterministic facts.
148
- - **CI**: run `textus freshness` (or `textus list` + schema validation) in CI to catch drift between derived entries and their sources.