Class: Woods::SessionTracer::SessionFlowDocument

Inherits:
Object
  • Object
show all
Defined in:
lib/woods/session_tracer/session_flow_document.rb

Overview

Value object representing an assembled session flow trace.

Contains a two-level structure:

  • Timeline — ordered steps with unit_refs and side_effects (lightweight)

  • **Context pool** — deduplicated ExtractedUnit data (heavy, included once each)

Follows the FlowDocument pattern for serialization and rendering.

rubocop:disable Metrics/ClassLength

Examples:

doc = SessionFlowDocument.new(
  session_id: "abc123",
  steps: [...],
  context_pool: { "OrdersController" => { ... } },
  generated_at: Time.now.utc.iso8601
)
doc.to_h         # => JSON-serializable Hash
doc.to_markdown   # => human-readable document
doc.to_context    # => LLM XML format

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(session_id:, steps: [], context_pool: {}, side_effects: [], dependency_map: {}, token_count: 0, generated_at: nil) ⇒ SessionFlowDocument

rubocop:disable Metrics/ParameterLists

Parameters:

  • session_id (String)

    The session identifier

  • steps (Array<Hash>) (defaults to: [])

    Ordered timeline steps

  • context_pool (Hash<String, Hash>) (defaults to: {})

    Deduplicated unit data keyed by identifier

  • side_effects (Array<Hash>) (defaults to: [])

    Async side effects (jobs, mailers)

  • dependency_map (Hash<String, Array<String>>) (defaults to: {})

    Unit -> dependency identifiers

  • token_count (Integer) (defaults to: 0)

    Estimated total tokens

  • generated_at (String, nil) (defaults to: nil)

    ISO8601 timestamp (defaults to now)



39
40
41
42
43
44
45
46
47
48
# File 'lib/woods/session_tracer/session_flow_document.rb', line 39

def initialize(session_id:, steps: [], context_pool: {}, side_effects: [],
               dependency_map: {}, token_count: 0, generated_at: nil)
  @session_id = session_id
  @steps = steps
  @context_pool = context_pool
  @side_effects = side_effects
  @dependency_map = dependency_map
  @token_count = token_count
  @generated_at = generated_at || Time.now.utc.iso8601
end

Instance Attribute Details

#context_poolObject (readonly)

Returns the value of attribute context_pool.



28
29
30
# File 'lib/woods/session_tracer/session_flow_document.rb', line 28

def context_pool
  @context_pool
end

#dependency_mapObject (readonly)

Returns the value of attribute dependency_map.



28
29
30
# File 'lib/woods/session_tracer/session_flow_document.rb', line 28

def dependency_map
  @dependency_map
end

#generated_atObject (readonly)

Returns the value of attribute generated_at.



28
29
30
# File 'lib/woods/session_tracer/session_flow_document.rb', line 28

def generated_at
  @generated_at
end

#session_idObject (readonly)

Returns the value of attribute session_id.



28
29
30
# File 'lib/woods/session_tracer/session_flow_document.rb', line 28

def session_id
  @session_id
end

#side_effectsObject (readonly)

Returns the value of attribute side_effects.



28
29
30
# File 'lib/woods/session_tracer/session_flow_document.rb', line 28

def side_effects
  @side_effects
end

#stepsObject (readonly)

Returns the value of attribute steps.



28
29
30
# File 'lib/woods/session_tracer/session_flow_document.rb', line 28

def steps
  @steps
end

#token_countObject (readonly)

Returns the value of attribute token_count.



28
29
30
# File 'lib/woods/session_tracer/session_flow_document.rb', line 28

def token_count
  @token_count
end

Class Method Details

.from_h(data) ⇒ SessionFlowDocument

Reconstruct from a serialized Hash.

Handles both symbol and string keys for JSON round-trip compatibility.

Parameters:

  • data (Hash)

    Previously serialized document data

Returns:



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/woods/session_tracer/session_flow_document.rb', line 72

def self.from_h(data)
  data = deep_symbolize_keys(data)

  new(
    session_id: data[:session_id],
    steps: data[:steps] || [],
    context_pool: data[:context_pool] || {},
    side_effects: data[:side_effects] || [],
    dependency_map: data[:dependency_map] || {},
    token_count: data[:token_count] || 0,
    generated_at: data[:generated_at]
  )
end

Instance Method Details

#to_contextString

Render as LLM-consumable XML context.

Follows the format from docs/CONTEXT_AND_CHUNKING.md.

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

Returns:

  • (String)


157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/woods/session_tracer/session_flow_document.rb', line 157

def to_context
  lines = []
  header = "<session_context session_id=\"#{@session_id}\" requests=\"#{@steps.size}\" " \
           "tokens=\"#{@token_count}\" units=\"#{@context_pool.size}\">"
  lines << header

  # Timeline
  lines << '<session_timeline>'
  @steps.each_with_index do |step, idx|
    status = step[:status] || '?'
    duration = step[:duration_ms] ? ", #{step[:duration_ms]}ms" : ''
    entry = "#{idx + 1}. #{step[:method]} #{step[:path]}" \
            "#{step[:controller]}##{step[:action]} (#{status}#{duration})"
    lines << entry
  end
  lines << '</session_timeline>'

  # Units
  @context_pool.each do |identifier, unit|
    type = unit[:type] || 'unknown'
    file_path = unit[:file_path] || 'unknown'
    lines << %(<unit identifier="#{identifier}" type="#{type}" file="#{file_path}">)
    lines << (unit[:source_code] || '# source not available')
    lines << '</unit>'
  end

  # Side effects
  if @side_effects.any?
    lines << '<side_effects>'
    @side_effects.each do |effect|
      lines << "#{effect[:identifier]} (triggered by #{effect[:trigger_step]}, #{effect[:type]})"
    end
    lines << '</side_effects>'
  end

  # Dependencies
  if @dependency_map.any?
    lines << '<dependencies>'
    @dependency_map.each do |unit_id, deps|
      lines << "#{unit_id}#{deps.join(', ')}"
    end
    lines << '</dependencies>'
  end

  lines << '</session_context>'
  lines.join("\n")
end

#to_hHash

Serialize to a JSON-compatible Hash.

Returns:

  • (Hash)


54
55
56
57
58
59
60
61
62
63
64
# File 'lib/woods/session_tracer/session_flow_document.rb', line 54

def to_h
  {
    session_id: @session_id,
    generated_at: @generated_at,
    token_count: @token_count,
    steps: @steps,
    context_pool: @context_pool,
    side_effects: @side_effects,
    dependency_map: @dependency_map
  }
end

#to_markdownString

Render as human-readable Markdown.

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

Returns:

  • (String)


90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/woods/session_tracer/session_flow_document.rb', line 90

def to_markdown
  lines = []
  lines << "## Session: #{@session_id}"
  lines << "_Generated at #{@generated_at} | #{@steps.size} requests | ~#{@token_count} tokens_"
  lines << ''

  # Timeline
  lines << '### Timeline'
  lines << ''
  @steps.each_with_index do |step, idx|
    status = step[:status] || '?'
    duration = step[:duration_ms] ? " (#{step[:duration_ms]}ms)" : ''
    entry = "#{idx + 1}. #{step[:method]} #{step[:path]}" \
            "#{step[:controller]}##{step[:action]} [#{status}]#{duration}"
    lines << entry
  end
  lines << ''

  # Side effects
  if @side_effects.any?
    lines << '### Side Effects'
    lines << ''
    @side_effects.each do |effect|
      lines << "- #{effect[:type]}: #{effect[:identifier]} (triggered by #{effect[:trigger_step]})"
    end
    lines << ''
  end

  # Context pool
  if @context_pool.any?
    lines << '### Code Units'
    lines << ''
    @context_pool.each do |identifier, unit|
      type = unit[:type] || 'unknown'
      file_path = unit[:file_path]
      lines << "#### #{identifier} (#{type})"
      lines << "_#{file_path}_" if file_path
      lines << ''
      next unless unit[:source_code]

      lines << '```ruby'
      lines << unit[:source_code]
      lines << '```'
      lines << ''
    end
  end

  # Dependencies
  if @dependency_map.any?
    lines << '### Dependencies'
    lines << ''
    @dependency_map.each do |unit_id, deps|
      lines << "- #{unit_id}#{deps.join(', ')}"
    end
    lines << ''
  end

  lines.join("\n")
end