Class: Tina4::Docs
- Inherits:
-
Object
- Object
- Tina4::Docs
- Defined in:
- lib/tina4/docs.rb
Constant Summary collapse
- USER_DIRS =
── Constants ────────────────────────────────────────────────────
%w[orm routes app services].freeze
- STDLIB_ALLOWLIST =
Method names commonly referenced in markdown that must NOT be flagged as drift even when not present in the live index.
%w[ puts print p pp inspect to_s to_a to_h to_i to_f to_sym keys values each map select reject reduce inject filter length size count first last empty? include? has_key? push pop shift unshift slice splice get set put patch post delete head options json html xml render redirect text file stream call strip lstrip rstrip chomp chop split join replace gsub sub upcase downcase capitalize to_str new initialize freeze dup clone tap then yield_self raise rescue retry begin ensure end require require_relative load assert assert_equal assert_nil assert_not_nil expect should ].freeze
Instance Attribute Summary collapse
-
#project_root ⇒ Object
readonly
Returns the value of attribute project_root.
Class Method Summary collapse
-
.cached(project_root) ⇒ Object
── Internal: cached MCP instance ────────────────────────────────.
-
.check_docs(md_file_path, project_root: nil) ⇒ Object
Scan a markdown file for method-call references that don’t exist in the live index.
- .mcp_class(fqn, project_root: nil) ⇒ Object
- .mcp_method(class_fqn, name, project_root: nil) ⇒ Object
-
.mcp_search(query, k: 5, project_root: nil, source: "all", include_private: false) ⇒ Object
── MCP-style mirrors ────────────────────────────────────────────.
- .reset_cache! ⇒ Object
-
.sync_docs(md_file_path, project_root: nil) ⇒ Object
Overwrite the ‘<!– BEGIN GENERATED API –>` block in the markdown file.
Instance Method Summary collapse
-
#class_spec(fqn) ⇒ Object
Full reflection of a single class — ‘nil` for unknown.
-
#index ⇒ Object
Flat list of every entity (classes + methods).
-
#initialize(project_root) ⇒ Docs
constructor
── Construction ─────────────────────────────────────────────────.
-
#method_spec(class_fqn, method_name) ⇒ Object
Single method spec — ‘nil` for unknown.
-
#search(query, k: 5, source: "all", include_private: false) ⇒ Array<Hash>
Search the merged framework + user index for ranked hits.
Constructor Details
#initialize(project_root) ⇒ Docs
── Construction ─────────────────────────────────────────────────
48 49 50 51 52 53 54 55 56 |
# File 'lib/tina4/docs.rb', line 48 def initialize(project_root) @project_root = File.(project_root.to_s) @framework_root = File.(File.dirname(__FILE__)) @gem_root = File.(File.join(@framework_root, "..")) @version = detect_version @framework_entries = nil @user_entries = nil @user_mtime = 0 end |
Instance Attribute Details
#project_root ⇒ Object (readonly)
Returns the value of attribute project_root.
58 59 60 |
# File 'lib/tina4/docs.rb', line 58 def project_root @project_root end |
Class Method Details
.cached(project_root) ⇒ Object
── Internal: cached MCP instance ────────────────────────────────
236 237 238 239 240 241 242 |
# File 'lib/tina4/docs.rb', line 236 def self.cached(project_root) key = File.((project_root || Dir.pwd).to_s) @_mcp_mutex.synchronize do @_mcp_instances ||= {} @_mcp_instances[key] ||= new(key) end end |
.check_docs(md_file_path, project_root: nil) ⇒ Object
Scan a markdown file for method-call references that don’t exist in the live index. Returns ‘[…]`.
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 204 205 206 207 |
# File 'lib/tina4/docs.rb', line 161 def self.check_docs(md_file_path, project_root: nil) return { drift: [], error: "file not found: #{md_file_path}" } unless File.file?(md_file_path) project_root ||= File.dirname(File.(md_file_path)) docs = cached(project_root) idx = docs.index known = idx.each_with_object({}) do |e, acc| acc[e[:name].to_s.downcase] = true if e[:kind] == "method" end allow = STDLIB_ALLOWLIST.map(&:downcase).to_set text = File.read(md_file_path, encoding: "utf-8", invalid: :replace, undef: :replace) drift = [] # Patterns: var.method(, Class::method(, Class#method(, var->method( patterns = [ /(?:\b[a-z_][\w]*|\$\w+)\s*[.](\w+)\s*\(/, /\b[A-Z]\w*::(\w+)\s*\(/, /\b[A-Z]\w*#(\w+)\s*\(/, /\$\w+->(\w+)\s*\(/, ] in_block = false text.lines.each_with_index do |line, i| if line.lstrip.start_with?("```") in_block = !in_block next end next unless in_block patterns.each do |pat| line.scan(pat) do |captures| name = captures.first name_lc = name.to_s.downcase next if allow.include?(name_lc) next if known[name_lc] drift << { method: name, line: i + 1, block: line.strip, } end end end { drift: drift } end |
.mcp_class(fqn, project_root: nil) ⇒ Object
153 154 155 |
# File 'lib/tina4/docs.rb', line 153 def self.mcp_class(fqn, project_root: nil) cached(project_root).class_spec(fqn) end |
.mcp_method(class_fqn, name, project_root: nil) ⇒ Object
149 150 151 |
# File 'lib/tina4/docs.rb', line 149 def self.mcp_method(class_fqn, name, project_root: nil) cached(project_root).method_spec(class_fqn, name) end |
.mcp_search(query, k: 5, project_root: nil, source: "all", include_private: false) ⇒ Object
── MCP-style mirrors ────────────────────────────────────────────
145 146 147 |
# File 'lib/tina4/docs.rb', line 145 def self.mcp_search(query, k: 5, project_root: nil, source: "all", include_private: false) cached(project_root).search(query, k: k, source: source, include_private: include_private) end |
.reset_cache! ⇒ Object
244 245 246 247 248 249 |
# File 'lib/tina4/docs.rb', line 244 def self.reset_cache! @_mcp_mutex ||= Mutex.new @_mcp_mutex.synchronize do @_mcp_instances = {} end end |
.sync_docs(md_file_path, project_root: nil) ⇒ Object
Overwrite the ‘<!– BEGIN GENERATED API –>` block in the markdown file. Append a fresh block if the markers are absent.
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/tina4/docs.rb', line 211 def self.sync_docs(md_file_path, project_root: nil) project_root ||= (File.file?(md_file_path) ? File.dirname(File.(md_file_path)) : Dir.pwd) docs = cached(project_root) generated = docs.send(:render_generated_block) begin_marker = "<!-- BEGIN GENERATED API -->" end_marker = "<!-- END GENERATED API -->" existing = File.file?(md_file_path) ? File.read(md_file_path, encoding: "utf-8") : "" if existing.include?(begin_marker) && existing.include?(end_marker) b = existing.index(begin_marker) e = existing.index(end_marker) if b && e && e > b before = existing[0, b + begin_marker.length] after = existing[e..-1] File.write(md_file_path, "#{before}\n#{generated}\n#{after}") return end end block = "\n\n#{begin_marker}\n#{generated}\n#{end_marker}\n" File.write(md_file_path, existing.rstrip + block) end |
Instance Method Details
#class_spec(fqn) ⇒ Object
Full reflection of a single class — ‘nil` for unknown.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/tina4/docs.rb', line 99 def class_spec(fqn) ensure_index key = normalise_fqn(fqn) class_entry = all_entries.find { |e| e[:kind] == "class" && e[:fqn] == key } return nil if class_entry.nil? methods = all_entries.select do |m| m[:kind] == "method" && m[:class_fqn] == class_entry[:fqn] && m[:visibility] == "public" end.map { |m| method_payload(m) } { fqn: class_entry[:fqn], kind: "class", name: class_entry[:name], file: class_entry[:file], line: class_entry[:line], summary: class_entry[:summary], source: class_entry[:source], version: class_entry[:version], methods: methods, properties: [], } end |
#index ⇒ Object
Flat list of every entity (classes + methods).
134 135 136 137 138 139 140 141 |
# File 'lib/tina4/docs.rb', line 134 def index ensure_index all_entries.map do |e| clean = e.dup clean.delete(:_private) clean end end |
#method_spec(class_fqn, method_name) ⇒ Object
Single method spec — ‘nil` for unknown.
124 125 126 127 128 129 130 131 |
# File 'lib/tina4/docs.rb', line 124 def method_spec(class_fqn, method_name) ensure_index key = normalise_fqn(class_fqn) entry = all_entries.find do |e| e[:kind] == "method" && e[:class_fqn] == key && e[:name] == method_name.to_s end entry && method_payload(entry) end |
#search(query, k: 5, source: "all", include_private: false) ⇒ Array<Hash>
Search the merged framework + user index for ranked hits.
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 |
# File 'lib/tina4/docs.rb', line 69 def search(query, k: 5, source: "all", include_private: false) ensure_index tokens = tokenise(query.to_s) return [] if tokens.empty? joined = query.to_s.downcase.gsub(/\s+/, "") results = [] all_entries.each do |entry| src = entry[:source] next if source == "all" && src == "vendor" next if source != "all" && src != source next if !include_private && entry[:_private] score = score_entry(entry, tokens, joined) next if score <= 0 score *= 1.2 if src == "user" hit = entry.dup hit.delete(:_private) hit.delete(:docstring) hit[:score] = score.round(4) results << hit end results.sort! do |a, b| cmp = b[:score] <=> a[:score] cmp.nonzero? || (a[:fqn] <=> b[:fqn]) end results.first([k, 1].max) end |