Module: Textus::Protocol::Boot::Context

Defined in:
lib/textus/protocol/boot/context.rb

Defined Under Namespace

Classes: NullEntryIndex

Class Method Summary collapse

Class Method Details

.build(manifest:, event_store:, file_system:, layout:) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/textus/protocol/boot/context.rb', line 5

def self.build(manifest:, event_store:, file_system:, layout:)
  etag       = Textus::Value::Etag.for_contract(layout)
  latest_seq = event_store.latest[:seq]
  artifact   = read_artifact_content(manifest, file_system, layout)
  context    = read_boot_context(manifest, file_system, layout)

  stable = artifact || inline_boot_content(manifest, latest_seq)
  if stable["agent_quickstart"]
    stable = stable.merge(
      "agent_quickstart" => stable["agent_quickstart"].merge("latest_seq" => latest_seq),
    )
  end

  payload = {
    "protocol" => Textus::PROTOCOL,
    "store_root" => layout.root,
    "contract_etag" => etag,
  }.merge(stable)
  payload["context"] = context if context
  payload
end

.inline_boot_content(manifest, _latest_seq) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/textus/protocol/boot/context.rb', line 53

def self.inline_boot_content(manifest, _latest_seq)
  agent_role     = manifest.policy.proposer_role
  writable_lanes = manifest.data.declared_lane_kinds.keys.each_with_object([]) do |ln, acc|
    next unless agent_role

    writers = manifest.policy.writers_for(ln)
    acc << ln if writers.include?(agent_role)
  end

  author_writers = manifest.policy.writers_for("knowledge")
  agent_has_author = agent_role && author_writers.include?(agent_role)
  write_verbs = if agent_role
                  mcp_verbs = Textus::Protocol::VerbRegistry.registered.select { |s| s.write? && s.mcp? }
                  unless agent_has_author
                    forbidden = %i[accept reject].to_set.freeze
                    mcp_verbs = mcp_verbs.reject { |s| forbidden.include?(s.verb) }
                  end
                  mcp_verbs.map { |s| s.verb.to_s }
                else
                  []
                end

  meta_glossary = {
    "knowledge.loop.adoption" => { "node" => "target loop node", "confidence" => "high|medium|low",
                                   "signal" => "observation type", "summary" => "brief description" },
    "scratchpad.proposals" => { "proposal" => "{ target_key, action }" },
    "raw" => { "source" => "{ kind, url, path, label }" },
  }.freeze

  blueprints = manifest.data.entries.map do |entry|
    bp = { "key_prefix" => entry.key, "format" => entry.format.to_s, "naming" => entry.naming.to_s }
    bp["schema"] = entry.schema.to_s if entry.schema
    meta = meta_glossary[entry.key]
    bp["meta"] = meta if meta
    bp
  end

  {
    "lanes" => lanes_for(manifest),
    "entry_blueprints" => blueprints,
    "workflow_tips" => [
      { "when" => "write an entry", "then" => "`data` param: String for body, Hash for content" },
      { "when" => "edit a protected path", "then" => "`textus propose key=my-change data=...` — human reviews and accepts" },
      { "when" => "explore recent changes", "then" => "`textus pulse` then `textus get <changed_key>`" },
      { "when" => "find related entries", "then" => "`textus graph key=<key> depth=2`" },
    ],
    "agent_quickstart" => {
      "read_verbs" => Textus::Protocol::VerbRegistry.registered.select { |s| s.read? && s.mcp? }.map { |s| s.verb.to_s },
      "write_verbs" => write_verbs,
      "writable_lanes" => writable_lanes,
      "propose_lane" => manifest.policy.propose_lane_for(agent_role),
    },
  }
end

.lanes_for(manifest) ⇒ Object



108
109
110
111
112
113
114
115
# File 'lib/textus/protocol/boot/context.rb', line 108

def self.lanes_for(manifest)
  manifest.data.declared_lane_kinds.keys.map do |name|
    row = { "name" => name, "writers" => manifest.policy.writers_for(name) }
    purpose = manifest.data.lane_descs[name]
    row["purpose"] = purpose if purpose && !purpose.empty?
    row
  end
end

.read_artifact_content(manifest, file_system, _layout) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/textus/protocol/boot/context.rb', line 27

def self.read_artifact_content(manifest, file_system, _layout)
  key = "artifacts.boot"
  res = manifest.resolver.resolve(key)
  return nil unless res.path && file_system.exists?(res.path)

  mentry = res.entry
  raw = file_system.read(res.path)
  parsed = Textus::Protocol::Format.for(mentry.format).parse(raw, path: res.path)
  parsed["content"]
rescue Textus::Error
  nil
end

.read_boot_context(manifest, file_system, _layout) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/textus/protocol/boot/context.rb', line 40

def self.read_boot_context(manifest, file_system, _layout)
  res = manifest.resolver.resolve("knowledge.boot")
  return nil unless res.path && file_system.exists?(res.path)

  mentry = res.entry
  raw = file_system.read(res.path)
  parsed = Textus::Protocol::Format.for(mentry.format).parse(raw, path: res.path)
  body = parsed["body"]&.strip
  body.nil? || body.empty? ? nil : body
rescue Textus::Error
  nil
end