Module: Tina4::Context::Chunker
- Defined in:
- lib/tina4/context/chunker.rb
Overview
Folding + chunking helpers. All methods are module functions so they read
as Chunker.fold(text), matching the Python free-function layout.
Constant Summary collapse
- NUM_COMMA =
Join comma-grouped numbers ("24,601" -> "24601") and split camelCase ("ForeignKeyField" -> "foreign key field") so a query token reaches a code identifier. snake_case already splits for free at the tokenizer.
/(?<=\d),(?=\d)/- CAMEL =
/(?<=[a-z0-9])(?=[A-Z])/- WORD_RE =
/[a-z0-9]+/- SENT =
Sentence boundary = end punctuation FOLLOWED BY whitespace/EOL, or a newline. NOT a bare '.', which would shred embedded code in prose ("db.fetch()" -> "db. fetch()"). Intra-token dots stay intact.
/(?<=[.!?])\s+|\n+/- TOPLEVEL =
Chunk boundaries across languages. Ruby is the native target so def/class/module are accepted at any indent (Ruby nests methods inside classes/modules); the cross-language alternatives (php/js/ts function, ts export, Object Pascal unit/routine headers) mirror the Python port so the SAME index can hold .rb, .py, .php, .js, .ts, .pas … files.
Regexp.new( '\A(?:' \ '\s*(?:async )?def |\s*class |\s*module |@\w' \ '|\s*(?:public |private |protected |static |final |abstract )*function \w' \ '|(?:export )?(?:default )?(?:abstract )?class ' \ '|interface |trait ' \ '|export (?:const|function|interface|type|async) ' \ '|[Uu]nit |[Pp]rocedure |[Ff]unction |[Cc]onstructor |[Dd]estructor ' \ '|[Tt]ype$' \ ')' )
Class Method Summary collapse
-
.chunk_code(text, path: "", max_lines: 60) ⇒ Object
Chunk source on top-level def/class/module boundaries (sentence chunking shreds code).
-
.chunk_text(text, max_words: 350) ⇒ Object
Chunk prose/docs into sentence-packed windows of at most max_words words.
-
.fold(text) ⇒ Object
Lowercase, strip diacritics, join comma-grouped numbers, split camelCase.
-
.light_stem(token) ⇒ Object
Fold simple plurals: strip one trailing 's', never from ss/us/is endings (class, status, axis).
-
.pack(segments, max_lines) ⇒ Object
Pack line-segments into chunks of at most max_lines lines, hard-splitting any single oversized segment.
-
.terms(text) ⇒ Object
Accent-folded lowercase alphanumeric tokens (digits kept).
Class Method Details
.chunk_code(text, path: "", max_lines: 60) ⇒ Object
Chunk source on top-level def/class/module boundaries (sentence chunking
shreds code). Segments are packed up to max_lines, and every chunk starts
with a "# file:
Returns an array of [index, chunk_text] pairs.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/tina4/context/chunker.rb', line 104 def chunk_code(text, path: "", max_lines: 60) lines = text.to_s.split("\n", -1) lines.pop if lines.last == "" # drop trailing empty from a final newline bounds = [] lines.each_with_index { |ln, i| bounds << i if TOPLEVEL.match?(ln) } bounds = [0] + bounds if bounds.empty? || bounds.first != 0 ends = bounds[1..] + [lines.length] segments = bounds.zip(ends).map { |a, b| lines[a...b] } header = path.to_s.empty? ? nil : "# file: #{path}" out = [] pack(segments, max_lines).each_with_index do |ch, i| body = ((header ? [header] : []) + ch).join("\n") out << [i, body] end out end |
.chunk_text(text, max_words: 350) ⇒ Object
Chunk prose/docs into sentence-packed windows of at most max_words words. Returns an array of [index, chunk_text] pairs.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/tina4/context/chunker.rb', line 125 def chunk_text(text, max_words: 350) sents = text.to_s.split(SENT).map(&:strip).reject(&:empty?) chunks = [] cur = [] cur_words = 0 sents.each do |s| w = s.split.length if !cur.empty? && cur_words + w > max_words chunks << cur.join(" ") cur = [] cur_words = 0 end cur << s cur_words += w end chunks << cur.join(" ") unless cur.empty? chunks.each_with_index.map { |c, i| [i, c] } end |
.fold(text) ⇒ Object
Lowercase, strip diacritics, join comma-grouped numbers, split camelCase. Applied symmetrically to the indexed body and to query tokens so matching is consistent: a query for "field" reaches "IntegerField".
51 52 53 54 55 56 |
# File 'lib/tina4/context/chunker.rb', line 51 def fold(text) s = text.to_s.gsub(NUM_COMMA, "") s = s.gsub(CAMEL, " ").downcase s.unicode_normalize(:nfkd) .encode(Encoding::ASCII, invalid: :replace, undef: :replace, replace: "") end |
.light_stem(token) ⇒ Object
Fold simple plurals: strip one trailing 's', never from ss/us/is endings (class, status, axis). QUERY-side only — so "fields" also reaches "Field".
60 61 62 63 64 65 66 |
# File 'lib/tina4/context/chunker.rb', line 60 def light_stem(token) if token.length > 3 && token.end_with?("s") && !token.end_with?("ss", "us", "is") token[0..-2] else token end end |
.pack(segments, max_lines) ⇒ Object
Pack line-segments into chunks of at most max_lines lines, hard-splitting any single oversized segment.
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/context/chunker.rb', line 75 def pack(segments, max_lines) chunks = [] cur = [] segments.each do |seg| seg = seg.dup while seg.length > max_lines # oversized segment: hard split unless cur.empty? chunks << cur cur = [] end chunks << seg[0...max_lines] seg = seg[max_lines..] || [] end if !cur.empty? && cur.length + seg.length > max_lines chunks << cur cur = [] end cur += seg end chunks << cur unless cur.empty? chunks end |
.terms(text) ⇒ Object
Accent-folded lowercase alphanumeric tokens (digits kept).
69 70 71 |
# File 'lib/tina4/context/chunker.rb', line 69 def terms(text) fold(text).scan(WORD_RE) end |