Class: Ace::Docs::Models::Document

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/docs/models/document.rb

Overview

Model representing a managed document with frontmatter and content

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path: nil, frontmatter: {}, content: "") ⇒ Document

Returns a new instance of Document.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/ace/docs/models/document.rb', line 15

def initialize(path: nil, frontmatter: {}, content: "")
  @path = path
  @frontmatter = frontmatter || {}
  @content = content || ""

  # Extract key fields from frontmatter
  @doc_type = @frontmatter["doc-type"]
  @purpose = @frontmatter["purpose"]
  @update_config = @frontmatter["update"] || {}
  @context_config = @frontmatter["context"] || {}
  @rules = @frontmatter["rules"] || {}
  @metadata = @frontmatter["metadata"] || {}

  # Extract ace-docs namespace configuration
  @ace_docs_config = @frontmatter["ace-docs"] || {}
end

Instance Attribute Details

#ace_docs_configObject (readonly)

Get the ace-docs configuration namespace



192
193
194
# File 'lib/ace/docs/models/document.rb', line 192

def ace_docs_config
  @ace_docs_config
end

#contentObject

Returns the value of attribute content.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def content
  @content
end

#context_configObject

Returns the value of attribute context_config.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def context_config
  @context_config
end

#doc_typeObject

Returns the value of attribute doc_type.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def doc_type
  @doc_type
end

#frontmatterObject

Returns the value of attribute frontmatter.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def frontmatter
  @frontmatter
end

#metadataObject

Returns the value of attribute metadata.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def 
  @metadata
end

#pathObject

Returns the value of attribute path.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def path
  @path
end

#purposeObject

Returns the value of attribute purpose.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def purpose
  @purpose
end

#rulesObject

Returns the value of attribute rules.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def rules
  @rules
end

#update_configObject

Returns the value of attribute update_config.



12
13
14
# File 'lib/ace/docs/models/document.rb', line 12

def update_config
  @update_config
end

Instance Method Details

#<=>(other) ⇒ Object

Make documents comparable by path



347
348
349
350
# File 'lib/ace/docs/models/document.rb', line 347

def <=>(other)
  return nil unless other.is_a?(Document)
  (@path || "") <=> (other.path || "")
end

#==(other) ⇒ Object

Check equality



336
337
338
339
# File 'lib/ace/docs/models/document.rb', line 336

def ==(other)
  return false unless other.is_a?(Document)
  @path == other.path
end

#auto_generateObject

Get auto-generation rules



272
273
274
# File 'lib/ace/docs/models/document.rb', line 272

def auto_generate
  @rules["auto-generate"] || []
end

#context_includesObject

Get additional context includes



252
253
254
# File 'lib/ace/docs/models/document.rb', line 252

def context_includes
  @context_config["includes"] || []
end

#context_keywordsObject

Get context keywords for LLM analysis



237
238
239
# File 'lib/ace/docs/models/document.rb', line 237

def context_keywords
  @ace_docs_config.dig("context", "keywords") || []
end

#context_presetObject

Get the context preset



242
243
244
245
246
247
248
249
# File 'lib/ace/docs/models/document.rb', line 242

def context_preset
  # Try new ace-docs namespace first
  preset = @ace_docs_config.dig("context", "preset")
  return preset if preset

  # Fall back to legacy format
  @context_config["preset"]
end

#display_nameObject

Get display name for document



284
285
286
# File 'lib/ace/docs/models/document.rb', line 284

def display_name
  @path ? File.basename(@path) : "untitled.md"
end

#focus_hintsObject

Get the focus hints for LLM analysis



187
188
189
# File 'lib/ace/docs/models/document.rb', line 187

def focus_hints
  @update_config["focus"] || {}
end

#freshness_statusObject

Get the freshness status Uses configurable thresholds from .ace-defaults/docs/config.yml Follows ADR-022 pattern for configuration loading



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
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/ace/docs/models/document.rb', line 121

def freshness_status
  return :unknown unless last_updated

  # Convert Time to Date for comparison
  last_updated_date = last_updated.is_a?(Time) ? last_updated.to_date : last_updated
  days_since_update = (Date.today - last_updated_date).to_i

  thresholds = freshness_thresholds

  case update_frequency
  when "daily"
    if days_since_update == 0
      :current
    elsif days_since_update <= thresholds[:daily_stale]
      :stale
    else
      :outdated
    end
  when "weekly"
    if days_since_update <= thresholds[:weekly_current]
      :current
    elsif days_since_update <= thresholds[:weekly_stale]
      :stale
    else
      :outdated
    end
  when "monthly"
    if days_since_update <= thresholds[:monthly_current]
      :current
    elsif days_since_update <= thresholds[:monthly_stale]
      :stale
    else
      :outdated
    end
  when "on-change"
    :current # Always current for on-change documents
  else
    :unknown
  end
end

#freshness_thresholdsHash

Get freshness thresholds from configuration Falls back to historical defaults if config not available Supports frequency-specific thresholds from .ace-defaults/docs/config.yml

Returns:

  • (Hash)

    Threshold values for different update frequencies



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/ace/docs/models/document.rb', line 166

def freshness_thresholds
  config_thresholds = Ace::Docs.config["default_freshness_days"] || {}

  # Extract frequency-specific thresholds with historical defaults
  daily_config = config_thresholds["daily"] || {}
  weekly_config = config_thresholds["weekly"] || {}
  monthly_config = config_thresholds["monthly"] || {}

  {
    # Daily frequency: current=today (0 days), stale within 2 days
    daily_stale: daily_config["stale"] || 2,
    # Weekly frequency: historical defaults 7/14 days
    weekly_current: weekly_config["current"] || 7,
    weekly_stale: weekly_config["stale"] || 14,
    # Monthly frequency: historical defaults 30/45 days
    monthly_current: monthly_config["current"] || 30,
    monthly_stale: monthly_config["stale"] || 45
  }
end

#hashObject

Generate hash code



342
343
344
# File 'lib/ace/docs/models/document.rb', line 342

def hash
  @path.hash
end

#last_checkedDate, ...

Get the last checked date or datetime

Polymorphic Return Type:

- Date object for date-only timestamps (YYYY-MM-DD)
- Time object (UTC) for ISO 8601 timestamps (YYYY-MM-DDTHH:MM:SSZ)

Returns:

  • (Date, Time, nil)

    The last checked timestamp, or nil if not set

See Also:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/ace/docs/models/document.rb', line 79

def last_checked
  date_str = @ace_docs_config["last-checked"] || @update_config["last-checked"]
  return nil unless date_str

  result = case date_str
  when Date, Time
    date_str  # Return as-is
  when String
    Atoms::TimestampParser.parse_timestamp(date_str)
  end

  # Ensure Time objects are in UTC
  result.is_a?(Time) ? result.utc : result
rescue ArgumentError
  nil
end

#last_updatedDate, ...

Get the last updated date or datetime

Polymorphic Return Type:

- Date object for date-only timestamps (YYYY-MM-DD)
- Time object (UTC) for ISO 8601 timestamps (YYYY-MM-DDTHH:MM:SSZ)

This preserves the precision of the original timestamp format. When comparing dates for freshness calculations, Time objects are converted to Date objects.

Returns:

  • (Date, Time, nil)

    The last updated timestamp, or nil if not set



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/ace/docs/models/document.rb', line 52

def last_updated
  # Try ace-docs namespace first
  date_str = @ace_docs_config["last-updated"]

  return nil unless date_str

  result = case date_str
  when Date, Time
    date_str  # Return as-is
  when String
    Atoms::TimestampParser.parse_timestamp(date_str)
  end

  # Ensure Time objects are in UTC
  result.is_a?(Time) ? result.utc : result
rescue ArgumentError
  nil
end

#managed?Boolean

Check if document is managed by ace-docs

Returns:

  • (Boolean)


33
34
35
# File 'lib/ace/docs/models/document.rb', line 33

def managed?
  !@doc_type.nil? && !@purpose.nil?
end

#max_linesObject

Get the maximum line count rule



257
258
259
# File 'lib/ace/docs/models/document.rb', line 257

def max_lines
  @rules["max-lines"]
end

#multi_subject?Boolean

Check if document has multi-subject configuration

Returns:

  • (Boolean)

    True if subject is an array of hashes



206
207
208
209
# File 'lib/ace/docs/models/document.rb', line 206

def multi_subject?
  subject_config = @ace_docs_config["subject"]
  subject_config.is_a?(Array)
end

#needs_update?Boolean

Check if document needs updating based on frequency and last updated date

Returns:

  • (Boolean)


97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/ace/docs/models/document.rb', line 97

def needs_update?
  return true unless last_updated

  # Convert Time to Date for comparison
  last_updated_date = last_updated.is_a?(Time) ? last_updated.to_date : last_updated
  days_since_update = (Date.today - last_updated_date).to_i

  case update_frequency
  when "daily"
    days_since_update >= 1
  when "weekly"
    days_since_update >= 7
  when "monthly"
    days_since_update >= 30
  when "on-change"
    false # Only update when changes detected
  else
    false
  end
end

#no_duplicate_fromObject

Get documents to avoid duplication from



267
268
269
# File 'lib/ace/docs/models/document.rb', line 267

def no_duplicate_from
  @rules["no-duplicate-from"] || []
end

#relative_pathObject

Get relative path from current directory



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/ace/docs/models/document.rb', line 289

def relative_path
  return nil unless @path

  begin
    require "pathname"
    # Try to get a nice relative path from current directory
    pwd_path = Pathname.new(Dir.pwd)
    file_path = Pathname.new(@path)

    # If file is under current directory or we can compute relative path
    relative = file_path.relative_path_from(pwd_path).to_s

    # If relative path goes up too many levels, just use absolute
    if relative.start_with?("../../../")
      @path
    else
      relative
    end
  rescue
    # If we can't compute relative path, use absolute
    @path
  end
end

#required_sectionsObject

Get required sections



262
263
264
# File 'lib/ace/docs/models/document.rb', line 262

def required_sections
  @rules["sections"] || []
end

#subject_configurationsArray<Hash>

Get structured subject configurations for multi-subject support

Returns:

  • (Array<Hash>)

    Array of String, filters: Array<String>



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/ace/docs/models/document.rb', line 213

def subject_configurations
  subject_config = @ace_docs_config["subject"]

  if subject_config.is_a?(Array)
    # Multi-subject format: array of single-key hashes
    # [ { "code" => { "diff" => { "filters" => [...] } } }, { "docs" => {...} } ]
    subject_config.map do |subject_hash|
      # Each item should be a hash with one key (the subject name)
      name = subject_hash.keys.first
      config = subject_hash[name] || {}
      filters = config.dig("diff", "filters") || []

      {
        name: name,
        filters: filters
      }
    end.reject { |s| s[:filters].empty? }
  else
    # No valid subject configuration
    []
  end
end

#subject_diff_filtersArray<String>

Get subject diff filters

Returns:

  • (Array<String>)

    Flat array of path filters for single subject



196
197
198
199
200
201
202
# File 'lib/ace/docs/models/document.rb', line 196

def subject_diff_filters
  # Try new format first
  filters = @ace_docs_config.dig("subject", "diff", "filters")
  return filters if filters && !filters.empty?

  []
end

#titleObject

Get the document title from content



277
278
279
280
281
# File 'lib/ace/docs/models/document.rb', line 277

def title
  # Extract first heading from content
  match = @content.match(/^#\s+(.+)$/m)
  match ? match[1].strip : File.basename(@path || "untitled", ".md")
end

#to_hObject

Convert to hash for serialization



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/ace/docs/models/document.rb', line 314

def to_h
  {
    path: @path,
    doc_type: @doc_type,
    purpose: @purpose,
    title: title,
    last_updated: last_updated&.to_s,
    update_frequency: update_frequency,
    needs_update: needs_update?,
    freshness: freshness_status,
    rules: @rules,
    context: @context_config,
    metadata: @metadata
  }
end

#to_sObject

Format for display



331
332
333
# File 'lib/ace/docs/models/document.rb', line 331

def to_s
  "Document: #{display_name} (#{@doc_type || "untyped"})"
end

#update_frequencyObject

Get the update frequency configuration



38
39
40
# File 'lib/ace/docs/models/document.rb', line 38

def update_frequency
  @update_config["frequency"] || "on-change"
end