Module: Textus::Intro

Defined in:
lib/textus/intro.rb

Overview

Read-only “what’s in this store and how do I use it” envelope. A single call gives an agent the working model of a textus-managed project: zones and their write authority, entries and their flags, registered extensions, write flows, and the CLI verb catalog.

Intro is side-effect-free.

Constant Summary collapse

PROTOCOL_ID =
"textus/1".freeze
ZONE_PURPOSES =

Conventional zone purposes. Unknown zones (declared in the manifest but not listed here) get no ‘purpose` field.

{
  "canon" => "slow-changing identity; human-only writes",
  "working" => "active project state; humans, AI, and scripts share this surface",
  "intake" => "declared external inputs; script-refreshed via actions",
  "pending" => "AI proposals awaiting human accept",
  "derived" => "build-computed outputs; never hand-edited",
}.freeze
WRITE_FLOWS =
{
  "human" => "edit files in canon/working zones, then 'textus put KEY --as=human'",
  "ai" => "propose changes by writing 'pending.*' entries with --as=ai and a 'proposal:' frontmatter block; " \
          "a human runs 'textus accept' to apply",
  "script" => "refresh intake entries with 'textus refresh KEY --as=script' (uses the entry's declared action)",
  "build" => "'textus build' computes derived entries from projections; derived files are never hand-edited",
}.freeze
CLI_VERBS =

The CLI verb catalog. Truth lives here; do not derive dynamically. Agents that read intro should see a stable shape regardless of how verb implementations evolve.

[
  { "name" => "intro",    "summary" => "this output — orientation for agents and tools" },
  { "name" => "list",     "summary" => "enumerate keys (optional --prefix)" },
  { "name" => "get",      "summary" => "read an entry; envelope with frontmatter, body, uid, etag" },
  { "name" => "where",    "summary" => "resolve a key to its zone and path without reading" },
  { "name" => "schema",   "summary" => "field shape for a key family" },
  { "name" => "put",      "summary" => "write an entry; --as=<role>, --stdin payload" },
  { "name" => "accept",   "summary" => "apply a pending.* proposal; --as=human only" },
  { "name" => "mv",       "summary" => "rename a key in place; uid preserved, audit row written" },
  { "name" => "delete",   "summary" => "delete an entry; --as=<role>" },
  { "name" => "build",    "summary" => "materialize derived entries; publish_to and publish_each fan out copies" },
  { "name" => "refresh",  "summary" => "run an action for an intake entry" },
  { "name" => "stale",    "summary" => "list derived/intake entries past their freshness check" },
  { "name" => "doctor",   "summary" => "health-check the store (missing schemas, illegal keys, sentinel drift, etc.)" },
  { "name" => "migrate-keys", "summary" => "rename files whose basenames violate the strict key grammar" },
  { "name" => "extensions", "summary" => "list registered actions, reducers, doctor_checks, declared hooks" },
].freeze

Class Method Summary collapse

Class Method Details

.entries_for(store) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/textus/intro.rb', line 72

def self.entries_for(store)
  store.manifest.entries.map do |e|
    derived = store.manifest.zone_writers(e.zone).include?("build")
    {
      "key" => e.key,
      "zone" => e.zone,
      "schema" => e.schema,
      "nested" => e.nested ? true : false,
      "owner" => e.owner,
      "format" => e.format,
      "derived" => derived,
      "intake" => !e.action.nil?,
      "publish_to" => Array(e.publish_to),
      "publish_each" => e.publish_each,
    }
  end
end

.extensions_for(store) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/textus/intro.rb', line 90

def self.extensions_for(store)
  reg = store.registry
  reducers = reg.reducer_names.map(&:to_s).sort
  actions = reg.action_names.map(&:to_s).sort
  doctor_checks = reg.doctor_check_names.map(&:to_s).sort
  hooks = reg.hook_events.flat_map do |evt|
    reg.hooks(evt).map { |h| { "event" => evt.to_s, "name" => h[:name].to_s } }
  end.sort_by { |h| [h["event"], h["name"]] }
  {
    "reducers" => reducers,
    "actions" => actions,
    "doctor_checks" => doctor_checks,
    "hooks" => hooks,
  }
end

.run(store) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/textus/intro.rb', line 50

def self.run(store)
  {
    "protocol" => PROTOCOL_ID,
    "store_root" => store.root,
    "zones" => zones_for(store),
    "entries" => entries_for(store),
    "extensions" => extensions_for(store),
    "write_flows" => WRITE_FLOWS.dup,
    "cli_verbs" => CLI_VERBS.map(&:dup),
    "docs" => { "spec" => "SPEC.md", "example" => "examples/claude-plugin/" },
  }
end

.zones_for(store) ⇒ Object



63
64
65
66
67
68
69
70
# File 'lib/textus/intro.rb', line 63

def self.zones_for(store)
  store.manifest.zones.map do |name, writers|
    row = { "name" => name, "writers" => Array(writers) }
    purpose = ZONE_PURPOSES[name]
    row["purpose"] = purpose if purpose
    row
  end
end