@descryy/runtime-adapter-ruby 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/index.d.ts +14 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +10 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/rails-frame-parsing.d.ts +105 -0
  6. package/dist/rails-frame-parsing.d.ts.map +1 -0
  7. package/dist/rails-frame-parsing.js +183 -0
  8. package/dist/rails-frame-parsing.js.map +1 -0
  9. package/dist/rails-stack-trace-parser.d.ts +31 -0
  10. package/dist/rails-stack-trace-parser.d.ts.map +1 -0
  11. package/dist/rails-stack-trace-parser.js +63 -0
  12. package/dist/rails-stack-trace-parser.js.map +1 -0
  13. package/dist/ruby-frame-parsing.d.ts +78 -0
  14. package/dist/ruby-frame-parsing.d.ts.map +1 -0
  15. package/dist/ruby-frame-parsing.js +108 -0
  16. package/dist/ruby-frame-parsing.js.map +1 -0
  17. package/dist/ruby-log-envelopes.d.ts +101 -0
  18. package/dist/ruby-log-envelopes.d.ts.map +1 -0
  19. package/dist/ruby-log-envelopes.js +214 -0
  20. package/dist/ruby-log-envelopes.js.map +1 -0
  21. package/dist/ruby-runtime-adapter.d.ts +51 -0
  22. package/dist/ruby-runtime-adapter.d.ts.map +1 -0
  23. package/dist/ruby-runtime-adapter.js +56 -0
  24. package/dist/ruby-runtime-adapter.js.map +1 -0
  25. package/dist/ruby-source-location-resolver.d.ts +34 -0
  26. package/dist/ruby-source-location-resolver.d.ts.map +1 -0
  27. package/dist/ruby-source-location-resolver.js +61 -0
  28. package/dist/ruby-source-location-resolver.js.map +1 -0
  29. package/dist/ruby-stack-trace-parser.d.ts +24 -0
  30. package/dist/ruby-stack-trace-parser.d.ts.map +1 -0
  31. package/dist/ruby-stack-trace-parser.js +71 -0
  32. package/dist/ruby-stack-trace-parser.js.map +1 -0
  33. package/dist/sinatra-frame-parsing.d.ts +92 -0
  34. package/dist/sinatra-frame-parsing.d.ts.map +1 -0
  35. package/dist/sinatra-frame-parsing.js +142 -0
  36. package/dist/sinatra-frame-parsing.js.map +1 -0
  37. package/dist/sinatra-stack-trace-parser.d.ts +18 -0
  38. package/dist/sinatra-stack-trace-parser.d.ts.map +1 -0
  39. package/dist/sinatra-stack-trace-parser.js +50 -0
  40. package/dist/sinatra-stack-trace-parser.js.map +1 -0
  41. package/package.json +30 -0
@@ -0,0 +1,14 @@
1
+ export type { RawRailsFrame } from "./rails-frame-parsing.ts";
2
+ export { createRailsLineClassifier, isRailsHeaderLine, isRailsFrameLine, parseRailsHeader, parseRailsFrame, } from "./rails-frame-parsing.ts";
3
+ export { createRailsStackTraceParser } from "./rails-stack-trace-parser.ts";
4
+ export type { RawSinatraFrame } from "./sinatra-frame-parsing.ts";
5
+ export { createSinatraLineClassifier, isSinatraHeaderLine, isSinatraFrameLine, parseSinatraHeader, parseSinatraFrame, } from "./sinatra-frame-parsing.ts";
6
+ export { createSinatraStackTraceParser } from "./sinatra-stack-trace-parser.ts";
7
+ export type { RawRubyFrame } from "./ruby-frame-parsing.ts";
8
+ export { createRubyLineClassifier, isRubyHeaderLine, isRubyFrameLine, parseRubyHeaderFrame, parseRubyFromFrame, } from "./ruby-frame-parsing.ts";
9
+ export { createRubySourceLocationResolver } from "./ruby-source-location-resolver.ts";
10
+ export { createRubyStackTraceParser } from "./ruby-stack-trace-parser.ts";
11
+ export { createRubyLoggerEnvelope, createRailsTaggedLogEnvelope } from "./ruby-log-envelopes.ts";
12
+ export type { RubyRuntimeAdapterOptions } from "./ruby-runtime-adapter.ts";
13
+ export { createRubyRuntimeAdapter } from "./ruby-runtime-adapter.ts";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AACjG,YAAY,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { createRailsLineClassifier, isRailsHeaderLine, isRailsFrameLine, parseRailsHeader, parseRailsFrame, } from "./rails-frame-parsing.js";
2
+ export { createRailsStackTraceParser } from "./rails-stack-trace-parser.js";
3
+ export { createSinatraLineClassifier, isSinatraHeaderLine, isSinatraFrameLine, parseSinatraHeader, parseSinatraFrame, } from "./sinatra-frame-parsing.js";
4
+ export { createSinatraStackTraceParser } from "./sinatra-stack-trace-parser.js";
5
+ export { createRubyLineClassifier, isRubyHeaderLine, isRubyFrameLine, parseRubyHeaderFrame, parseRubyFromFrame, } from "./ruby-frame-parsing.js";
6
+ export { createRubySourceLocationResolver } from "./ruby-source-location-resolver.js";
7
+ export { createRubyStackTraceParser } from "./ruby-stack-trace-parser.js";
8
+ export { createRubyLoggerEnvelope, createRailsTaggedLogEnvelope } from "./ruby-log-envelopes.js";
9
+ export { createRubyRuntimeAdapter } from "./ruby-runtime-adapter.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAE5E,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAEhF,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AAEjG,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Rails' backtrace grammar, which is **not Ruby's** (RT-111).
3
+ *
4
+ * ## The measurement
5
+ *
6
+ * A real Rails 8.0.4 application, spawned through the managed lifecycle,
7
+ * failing through its own request pipeline, logging through
8
+ * `ActiveSupport::TaggedLogging`, produced:
9
+ *
10
+ * ```
11
+ * 77 lines in -> 0 exceptions, 0 stack traces, 77 plain logs
12
+ * ```
13
+ *
14
+ * Zero. With the Rails log envelope applied and without it, identically.
15
+ * RT-089's shape for the third time (RT-089 ASP.NET, RT-104 PEP 654) and the
16
+ * most consequential of the three, because Rails is *the* Ruby framework and
17
+ * this is every exception a Rails application reports.
18
+ *
19
+ * ## Why the Ruby classifier cannot see it
20
+ *
21
+ * `ActionDispatch::DebugExceptions` does not log `Exception#full_message`.
22
+ * It composes its own text, and both halves differ:
23
+ *
24
+ * ```
25
+ * NoMethodError (undefined method 'fetch' for nil): <- Class (message):
26
+ *
27
+ * rails_service.rb:69:in 'ProbeController#read_customer_name'
28
+ * rails_service.rb:73:in 'ProbeController#charge_invoice'
29
+ * ```
30
+ *
31
+ * against Ruby's own printer:
32
+ *
33
+ * ```
34
+ * rails_service.rb:69:in 'ProbeController#read_customer_name': undefined method 'fetch' for nil (NoMethodError)
35
+ * from rails_service.rb:73:in 'ProbeController#charge_invoice'
36
+ * ```
37
+ *
38
+ * - **The header carries no location.** Ruby's header IS the innermost
39
+ * frame; Rails' names only the class and the message, and the raise site
40
+ * moves down to the first backtrace line.
41
+ * - **The frames have no `from` prefix.** `FROM_FRAME_PATTERN` requires one,
42
+ * and `HEADER_PATTERN` requires a trailing `: message` that these lines do
43
+ * not have. So a Rails backtrace line matches neither, and the block never
44
+ * opens.
45
+ *
46
+ * ## The fix is a declared framework, not a looser Ruby pattern
47
+ *
48
+ * Widening Ruby's `isFrame` to accept a bare `file:line:in 'name'` was the
49
+ * cheaper change and is the wrong one. `ExceptionBlockDetector` opens a
50
+ * block the moment `isFrame` returns true from idle, so that pattern would
51
+ * turn **any** log line containing `something.rb:12:in 'foo'` into the start
52
+ * of a stack trace — a wrong edge, which rule 2 ranks as worse than a
53
+ * missing one because it corrupts everything above it.
54
+ *
55
+ * So this is a second classifier, selected by
56
+ * `createRubyRuntimeAdapter({ framework: "rails" })` — **declared, never
57
+ * sniffed**, the same rule `createCSharpRuntimeAdapter`'s `framework` option
58
+ * follows and for the same reason: detecting Rails from a line that looks
59
+ * like its output would apply this grammar to a plain Ruby process that
60
+ * happened to print one.
61
+ *
62
+ * ## The bare-frame pattern is still loose, and is confined rather than tightened
63
+ *
64
+ * `isFrame` here is stateful: a bare `file:line:in 'name'` is only a frame
65
+ * **after a header has been seen**, which is the same argument
66
+ * `createPythonLineClassifier` makes about indented lines. Out of that
67
+ * state the pattern is never consulted from idle, so it cannot open a block
68
+ * on its own.
69
+ *
70
+ * ## The blank line between the header and the first frame is a bridge
71
+ *
72
+ * Rails prints a whitespace-only line after the header (and another before
73
+ * it). `LineClassifier.isBridge` exists for exactly this — PHP's `Stack
74
+ * trace:` line was the first — and `ExceptionBlockDetector` consults it only
75
+ * while a header is already held, so a blank line elsewhere in the log opens
76
+ * nothing.
77
+ */
78
+ export interface RawRailsFrame {
79
+ /** `Class#method`, `Class.method`, `block in Class#method` — kept verbatim, never rewritten. */
80
+ readonly functionName: string | null;
81
+ readonly file: string;
82
+ readonly line: number;
83
+ }
84
+ export declare function isRailsHeaderLine(line: string): boolean;
85
+ export declare function isRailsFrameLine(line: string): boolean;
86
+ /** `null` when the line is not `Class (message):` shaped — never fabricated. */
87
+ export declare function parseRailsHeader(line: string): {
88
+ readonly exceptionClass: string;
89
+ readonly message: string;
90
+ } | null;
91
+ /** `null` for anything not shaped like a bare `file:line:in 'name'` frame — never fabricated. */
92
+ export declare function parseRailsFrame(line: string): RawRailsFrame | null;
93
+ /**
94
+ * A stateful `LineClassifier`, and the statefulness is what makes the loose
95
+ * frame pattern safe. See this file's header.
96
+ *
97
+ * A fresh classifier per collector keeps two processes' output from priming
98
+ * each other — the same construction rule every other adapter here follows.
99
+ */
100
+ export declare function createRailsLineClassifier(): {
101
+ isHeader(line: string): boolean;
102
+ isFrame(line: string): boolean;
103
+ isBridge(line: string): boolean;
104
+ };
105
+ //# sourceMappingURL=rails-frame-parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rails-frame-parsing.d.ts","sourceRoot":"","sources":["../src/rails-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AAEH,MAAM,WAAW,aAAa;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAmCD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAMnH;AAED,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAOlE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,IAAI;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAuCA"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Rails' backtrace grammar, which is **not Ruby's** (RT-111).
3
+ *
4
+ * ## The measurement
5
+ *
6
+ * A real Rails 8.0.4 application, spawned through the managed lifecycle,
7
+ * failing through its own request pipeline, logging through
8
+ * `ActiveSupport::TaggedLogging`, produced:
9
+ *
10
+ * ```
11
+ * 77 lines in -> 0 exceptions, 0 stack traces, 77 plain logs
12
+ * ```
13
+ *
14
+ * Zero. With the Rails log envelope applied and without it, identically.
15
+ * RT-089's shape for the third time (RT-089 ASP.NET, RT-104 PEP 654) and the
16
+ * most consequential of the three, because Rails is *the* Ruby framework and
17
+ * this is every exception a Rails application reports.
18
+ *
19
+ * ## Why the Ruby classifier cannot see it
20
+ *
21
+ * `ActionDispatch::DebugExceptions` does not log `Exception#full_message`.
22
+ * It composes its own text, and both halves differ:
23
+ *
24
+ * ```
25
+ * NoMethodError (undefined method 'fetch' for nil): <- Class (message):
26
+ *
27
+ * rails_service.rb:69:in 'ProbeController#read_customer_name'
28
+ * rails_service.rb:73:in 'ProbeController#charge_invoice'
29
+ * ```
30
+ *
31
+ * against Ruby's own printer:
32
+ *
33
+ * ```
34
+ * rails_service.rb:69:in 'ProbeController#read_customer_name': undefined method 'fetch' for nil (NoMethodError)
35
+ * from rails_service.rb:73:in 'ProbeController#charge_invoice'
36
+ * ```
37
+ *
38
+ * - **The header carries no location.** Ruby's header IS the innermost
39
+ * frame; Rails' names only the class and the message, and the raise site
40
+ * moves down to the first backtrace line.
41
+ * - **The frames have no `from` prefix.** `FROM_FRAME_PATTERN` requires one,
42
+ * and `HEADER_PATTERN` requires a trailing `: message` that these lines do
43
+ * not have. So a Rails backtrace line matches neither, and the block never
44
+ * opens.
45
+ *
46
+ * ## The fix is a declared framework, not a looser Ruby pattern
47
+ *
48
+ * Widening Ruby's `isFrame` to accept a bare `file:line:in 'name'` was the
49
+ * cheaper change and is the wrong one. `ExceptionBlockDetector` opens a
50
+ * block the moment `isFrame` returns true from idle, so that pattern would
51
+ * turn **any** log line containing `something.rb:12:in 'foo'` into the start
52
+ * of a stack trace — a wrong edge, which rule 2 ranks as worse than a
53
+ * missing one because it corrupts everything above it.
54
+ *
55
+ * So this is a second classifier, selected by
56
+ * `createRubyRuntimeAdapter({ framework: "rails" })` — **declared, never
57
+ * sniffed**, the same rule `createCSharpRuntimeAdapter`'s `framework` option
58
+ * follows and for the same reason: detecting Rails from a line that looks
59
+ * like its output would apply this grammar to a plain Ruby process that
60
+ * happened to print one.
61
+ *
62
+ * ## The bare-frame pattern is still loose, and is confined rather than tightened
63
+ *
64
+ * `isFrame` here is stateful: a bare `file:line:in 'name'` is only a frame
65
+ * **after a header has been seen**, which is the same argument
66
+ * `createPythonLineClassifier` makes about indented lines. Out of that
67
+ * state the pattern is never consulted from idle, so it cannot open a block
68
+ * on its own.
69
+ *
70
+ * ## The blank line between the header and the first frame is a bridge
71
+ *
72
+ * Rails prints a whitespace-only line after the header (and another before
73
+ * it). `LineClassifier.isBridge` exists for exactly this — PHP's `Stack
74
+ * trace:` line was the first — and `ExceptionBlockDetector` consults it only
75
+ * while a header is already held, so a blank line elsewhere in the log opens
76
+ * nothing.
77
+ */
78
+ /**
79
+ * `NoMethodError (undefined method 'fetch' for nil):`
80
+ *
81
+ * `ActionDispatch::ExceptionWrapper` composes `"#{class} (#{message}):"`.
82
+ * Anchored at both ends and requiring the trailing `):`, because a partial
83
+ * match is evidence the line is not a header rather than a reason to guess
84
+ * at one. The class group allows `::` so a namespaced
85
+ * `ActiveRecord::RecordNotFound` matches.
86
+ *
87
+ * Not anchored at the *start* of the line beyond leading whitespace: the
88
+ * Rails envelope removes the `[request-id] ` tag run before the classifier
89
+ * sees the line, but an application logging through a different formatter
90
+ * may leave something else there, and Ruby's own header pattern made the
91
+ * same allowance for the same reason.
92
+ */
93
+ const RAILS_HEADER = /(?<class>[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*) \((?<message>.*)\):\s*$/;
94
+ /**
95
+ * `rails_service.rb:69:in 'ProbeController#read_customer_name'`, or a gem
96
+ * frame `actionpack (8.0.4) lib/action_controller/metal.rb:252:in '...'`.
97
+ *
98
+ * The gem form is real and is why `file` is `.+` rather than a path
99
+ * character class: Rails rewrites a gem's absolute path to
100
+ * `name (version) relative/path`, which is not openable but is the
101
+ * interpreter's — strictly Rails' — own answer, and rewriting it here would
102
+ * invent a location. Same decision as the `eval` frames in RT-105.
103
+ *
104
+ * The closing quote is always `'`; only the opening one varies by Ruby
105
+ * version (Feature #16495), so both are accepted exactly as Ruby's own
106
+ * patterns do.
107
+ */
108
+ const RAILS_FRAME = /^\s*(?<file>.+):(?<line>\d+):in\s+[`'](?<fn>[^']*)'\s*$/;
109
+ export function isRailsHeaderLine(line) {
110
+ return RAILS_HEADER.test(line);
111
+ }
112
+ export function isRailsFrameLine(line) {
113
+ return RAILS_FRAME.test(line);
114
+ }
115
+ /** `null` when the line is not `Class (message):` shaped — never fabricated. */
116
+ export function parseRailsHeader(line) {
117
+ const match = RAILS_HEADER.exec(line);
118
+ if (!match?.groups)
119
+ return null;
120
+ const exceptionClass = match.groups["class"];
121
+ if (exceptionClass === undefined)
122
+ return null;
123
+ return { exceptionClass, message: match.groups["message"] ?? "" };
124
+ }
125
+ /** `null` for anything not shaped like a bare `file:line:in 'name'` frame — never fabricated. */
126
+ export function parseRailsFrame(line) {
127
+ const match = RAILS_FRAME.exec(line);
128
+ if (!match?.groups)
129
+ return null;
130
+ const file = match.groups["file"];
131
+ const lineNumber = Number(match.groups["line"]);
132
+ if (file === undefined || !Number.isInteger(lineNumber))
133
+ return null;
134
+ return { functionName: match.groups["fn"] ?? null, file, line: lineNumber };
135
+ }
136
+ /**
137
+ * A stateful `LineClassifier`, and the statefulness is what makes the loose
138
+ * frame pattern safe. See this file's header.
139
+ *
140
+ * A fresh classifier per collector keeps two processes' output from priming
141
+ * each other — the same construction rule every other adapter here follows.
142
+ */
143
+ export function createRailsLineClassifier() {
144
+ let sawHeader = false;
145
+ return {
146
+ isHeader(line) {
147
+ if (isRailsHeaderLine(line)) {
148
+ sawHeader = true;
149
+ return true;
150
+ }
151
+ return false;
152
+ },
153
+ isFrame(line) {
154
+ // Never from idle. A bare `x.rb:12:in 'foo'` is frame-shaped and is
155
+ // also an entirely ordinary thing for an application to log.
156
+ if (!sawHeader)
157
+ return false;
158
+ if (isRailsFrameLine(line))
159
+ return true;
160
+ // **A blank line must not clear the state**, and the first version of
161
+ // this classifier did clear it — which is why the whole grammar
162
+ // recognised every line correctly and still produced zero exceptions.
163
+ // `ExceptionBlockDetector` asks `isFrame` before `isBridge` in the
164
+ // `holding-header` state, so the blank line Rails prints between its
165
+ // header and its first frame arrived here first, reset `sawHeader`,
166
+ // and every real frame after it was refused. Recognising each line in
167
+ // isolation is not the same as recognising the sequence.
168
+ if (line.trim() === "")
169
+ return false;
170
+ sawHeader = false;
171
+ return false;
172
+ },
173
+ /**
174
+ * The whitespace-only line Rails prints between the header and the first
175
+ * frame. Consulted only while a header is held, so a blank line
176
+ * elsewhere in the log opens nothing.
177
+ */
178
+ isBridge(line) {
179
+ return line.trim() === "";
180
+ },
181
+ };
182
+ }
183
+ //# sourceMappingURL=rails-frame-parsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rails-frame-parsing.js","sourceRoot":"","sources":["../src/rails-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AASH;;;;;;;;;;;;;;GAcG;AACH,MAAM,YAAY,GAAG,+EAA+E,CAAC;AAErG;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,GAAG,yDAAyD,CAAC;AAE9E,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,cAAc,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;AACpE,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB;IAKvC,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,OAAO;QACL,QAAQ,CAAC,IAAY;YACnB,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC5B,SAAS,GAAG,IAAI,CAAC;gBACjB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,CAAC,IAAY;YAClB,oEAAoE;YACpE,6DAA6D;YAC7D,IAAI,CAAC,SAAS;gBAAE,OAAO,KAAK,CAAC;YAC7B,IAAI,gBAAgB,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;YACxC,sEAAsE;YACtE,gEAAgE;YAChE,sEAAsE;YACtE,mEAAmE;YACnE,qEAAqE;YACrE,oEAAoE;YACpE,sEAAsE;YACtE,yDAAyD;YACzD,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,OAAO,KAAK,CAAC;YACrC,SAAS,GAAG,KAAK,CAAC;YAClB,OAAO,KAAK,CAAC;QACf,CAAC;QAED;;;;WAIG;QACH,QAAQ,CAAC,IAAY;YACnB,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Composes Rails' line-recognition functions with a `SourceLocationResolver`
3
+ * (RT-111).
4
+ *
5
+ * ## Why this is a second parser and not a branch in the Ruby one
6
+ *
7
+ * The two grammars disagree about **where the raise site is**, which is the
8
+ * one thing `primaryFrameIndex` exists to state:
9
+ *
10
+ * - Ruby's header IS the innermost frame, so index 0 is the header line.
11
+ * - Rails' header carries **no location at all** — it is `Class (message):`
12
+ * — and the raise site is the *first backtrace line beneath it*.
13
+ *
14
+ * A single parser with a flag would have to carry both answers and pick one,
15
+ * which is the shape that produced RT-083 (`primaryFrameIndex` inherited
16
+ * rather than stated). Two parsers each state one answer.
17
+ *
18
+ * Both still report `primaryFrameIndex: 0`, and for Rails that is a
19
+ * different fact rather than the same one: **the header contributes no
20
+ * frame**, so frame 0 is the first line Rails printed, and Rails prints
21
+ * innermost-first. Measured against a live Rails 8.0.4 request pipeline:
22
+ * frame 0 is `ProbeController#read_customer_name`, the line that raised,
23
+ * with the controller action two frames below it and 40 framework frames
24
+ * under that.
25
+ *
26
+ * `fidelity` is `"synchronous"` for the same reason Ruby's is: every frame
27
+ * is a genuine link in the call chain that was executing.
28
+ */
29
+ import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
30
+ export declare function createRailsStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
31
+ //# sourceMappingURL=rails-stack-trace-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rails-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/rails-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAIrG,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAkC9F"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Composes Rails' line-recognition functions with a `SourceLocationResolver`
3
+ * (RT-111).
4
+ *
5
+ * ## Why this is a second parser and not a branch in the Ruby one
6
+ *
7
+ * The two grammars disagree about **where the raise site is**, which is the
8
+ * one thing `primaryFrameIndex` exists to state:
9
+ *
10
+ * - Ruby's header IS the innermost frame, so index 0 is the header line.
11
+ * - Rails' header carries **no location at all** — it is `Class (message):`
12
+ * — and the raise site is the *first backtrace line beneath it*.
13
+ *
14
+ * A single parser with a flag would have to carry both answers and pick one,
15
+ * which is the shape that produced RT-083 (`primaryFrameIndex` inherited
16
+ * rather than stated). Two parsers each state one answer.
17
+ *
18
+ * Both still report `primaryFrameIndex: 0`, and for Rails that is a
19
+ * different fact rather than the same one: **the header contributes no
20
+ * frame**, so frame 0 is the first line Rails printed, and Rails prints
21
+ * innermost-first. Measured against a live Rails 8.0.4 request pipeline:
22
+ * frame 0 is `ProbeController#read_customer_name`, the line that raised,
23
+ * with the controller action two frames below it and 40 framework frames
24
+ * under that.
25
+ *
26
+ * `fidelity` is `"synchronous"` for the same reason Ruby's is: every frame
27
+ * is a genuine link in the call chain that was executing.
28
+ */
29
+ import { parseRailsFrame, parseRailsHeader } from "./rails-frame-parsing.js";
30
+ export function createRailsStackTraceParser(resolver) {
31
+ return {
32
+ async parse(rawText) {
33
+ const lines = rawText.split(/\r\n|\r|\n/);
34
+ const frames = [];
35
+ for (const line of lines) {
36
+ // The header is tried first and produces no frame — it has no
37
+ // location to produce one from. Skipping it explicitly rather than
38
+ // letting it fall through matters because `Class (message):` can
39
+ // contain a colon and a number in the message and would otherwise
40
+ // be worth checking against the frame pattern.
41
+ if (parseRailsHeader(line) !== null)
42
+ continue;
43
+ const raw = parseRailsFrame(line);
44
+ if (raw === null)
45
+ continue;
46
+ const location = await resolver.resolve({
47
+ file: raw.file,
48
+ line: raw.line,
49
+ column: null,
50
+ functionName: raw.functionName,
51
+ });
52
+ frames.push({ location, raw: line });
53
+ }
54
+ // Null, never an empty-but-structured StackTrace standing in for
55
+ // "couldn't parse" — this interface's contract, and the reason a
56
+ // header-only block does not become a zero-frame trace.
57
+ if (frames.length === 0)
58
+ return null;
59
+ return { fidelity: "synchronous", frames, primaryFrameIndex: 0 };
60
+ },
61
+ };
62
+ }
63
+ //# sourceMappingURL=rails-stack-trace-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rails-stack-trace-parser.js","sourceRoot":"","sources":["../src/rails-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE7E,MAAM,UAAU,2BAA2B,CAAC,QAAgC;IAC1E,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAe;YACzB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAE1C,MAAM,MAAM,GAAiB,EAAE,CAAC;YAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,8DAA8D;gBAC9D,mEAAmE;gBACnE,iEAAiE;gBACjE,kEAAkE;gBAClE,+CAA+C;gBAC/C,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,IAAI;oBAAE,SAAS;gBAE9C,MAAM,GAAG,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;gBAClC,IAAI,GAAG,KAAK,IAAI;oBAAE,SAAS;gBAE3B,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC;oBACtC,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,MAAM,EAAE,IAAI;oBACZ,YAAY,EAAE,GAAG,CAAC,YAAY;iBAC/B,CAAC,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,CAAC;YAED,iEAAiE;YACjE,iEAAiE;YACjE,wDAAwD;YACxD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YAErC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACnE,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Ruby backtrace line shapes — the `LineClassifier` for
3
+ * `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
4
+ * feeds `SourceLocationResolver`.
5
+ *
6
+ * Ruby's format is architecturally its own shape, distinct from both V8 and
7
+ * CPython:
8
+ *
9
+ * 1. **The header line IS the innermost frame.** Where V8 prints a
10
+ * header with no location and CPython prints a banner with no location,
11
+ * Ruby's default uncaught-exception printer combines them:
12
+ * `file.rb:2:in 'delete_invoice': invoice 3 is corrupt (RuntimeError)` is
13
+ * simultaneously "here is the exception" and "here is where it happened."
14
+ * Confirmed verbatim against `Exception#full_message`'s own documented
15
+ * example (docs.ruby-lang.org/en/3.4/Exception.html): the array's first
16
+ * entry is `"t.rb:3:in 'Integer#/': divided by 0 (ZeroDivisionError)"`.
17
+ * 2. **Continuation frames are prefixed `from`, not `at`,** and carry no
18
+ * message: `\tfrom t.rb:10:in 'Object#bar'`. Confirmed against the same
19
+ * doc example and against ruby/spec's own
20
+ * `core/exception/top_level_spec.rb`, which asserts the custom-backtrace
21
+ * case renders as:
22
+ * ```
23
+ * /dir/foo.rb:10:in `raising': foo (RuntimeError)
24
+ * \tfrom /dir/bar.rb:20:in `caller'
25
+ * ```
26
+ * 3. **The closing quote is always a single quote; only the opening quote
27
+ * varies by Ruby version.** Ruby 3.4 (Feature #16495) replaced the
28
+ * opening backtick with a single quote and, per Feature #19117, prefixes
29
+ * the owning class where one has a permanent name
30
+ * (`'Object#division'` vs. the pre-3.4 `` `division' ``). Both are real,
31
+ * currently-deployed formats — Ruby 3.4 shipped 2024-12-25, and every
32
+ * LTS-adjacent 3.0–3.3 install in production still prints the backtick
33
+ * form — so both are accepted here via a `[` ']` opening-character class,
34
+ * never just the newer one.
35
+ * 4. **No `isTrailer` is needed.** Because the header already carries the
36
+ * exception's identity, Ruby has nothing analogous to CPython's
37
+ * identity-comes-last problem. Chained exceptions (`Exception#cause`)
38
+ * print as two complete header+frames blocks back to back with **no
39
+ * connective sentence between them** — confirmed against
40
+ * `top_level_spec.rb`'s `"the Exception#cause is printed to STDERR with
41
+ * backtraces"` example — so the second header simply starts a new block
42
+ * once the first one's last `from` line stops matching `isFrame`. No
43
+ * special-casing required, unlike CPython's connective-exclusion.
44
+ *
45
+ * **Known, disclosed gap:** a backtrace whose only frame is the header
46
+ * itself — a bare top-level `raise` with no caller beneath it, e.g.
47
+ * `ruby -e 'raise "boom"'` — has no `from` line to confirm it, and
48
+ * `ExceptionBlockDetector`'s `holding-header` state demotes an unconfirmed
49
+ * header to a plain log line at `flush()`. This is a structural
50
+ * consequence of a header-embeds-frame grammar meeting a classifier
51
+ * interface built around "header, then confirming frame line": there is no
52
+ * lookahead-free way to distinguish "a header about to be followed by
53
+ * frames" from "a header that IS the entire trace" from the header line
54
+ * alone. `parseRubyHeaderFrame` still extracts the correct single frame
55
+ * from such a line if handed it directly — the gap is confined to the
56
+ * streaming block detector's own decision to invoke the parser at all, not
57
+ * to the parser's capability. In practice this only affects genuinely
58
+ * flat top-level scripts; a backend process (the collector's real target)
59
+ * always has at least one caller frame between the raise site and the
60
+ * process entry point.
61
+ */
62
+ export interface RawRubyFrame {
63
+ /** `<main>`, `block in foo`, `rescue in foo` and `Class#method`/`Class.method` are real Ruby frame names and are kept verbatim, never rewritten. */
64
+ readonly functionName: string | null;
65
+ readonly file: string;
66
+ readonly line: number;
67
+ }
68
+ export declare function isRubyHeaderLine(line: string): boolean;
69
+ export declare function isRubyFrameLine(line: string): boolean;
70
+ export declare function createRubyLineClassifier(): {
71
+ isHeader(line: string): boolean;
72
+ isFrame(line: string): boolean;
73
+ };
74
+ /** `null` when the header doesn't match Ruby's `file:line:in 'name': message` shape — never fabricated. */
75
+ export declare function parseRubyHeaderFrame(line: string): RawRubyFrame | null;
76
+ /** `null` for anything not shaped like a `from file:line:in 'name'` continuation — never fabricated. */
77
+ export declare function parseRubyFromFrame(line: string): RawRubyFrame | null;
78
+ //# sourceMappingURL=ruby-frame-parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-frame-parsing.d.ts","sourceRoot":"","sources":["../src/ruby-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAEH,MAAM,WAAW,YAAY;IAC3B,oJAAoJ;IACpJ,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAgBD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,wBAAwB,IAAI;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAChC,CAKA;AAED,2GAA2G;AAC3G,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAOtE;AAED,wGAAwG;AACxG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAOpE"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Ruby backtrace line shapes — the `LineClassifier` for
3
+ * `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
4
+ * feeds `SourceLocationResolver`.
5
+ *
6
+ * Ruby's format is architecturally its own shape, distinct from both V8 and
7
+ * CPython:
8
+ *
9
+ * 1. **The header line IS the innermost frame.** Where V8 prints a
10
+ * header with no location and CPython prints a banner with no location,
11
+ * Ruby's default uncaught-exception printer combines them:
12
+ * `file.rb:2:in 'delete_invoice': invoice 3 is corrupt (RuntimeError)` is
13
+ * simultaneously "here is the exception" and "here is where it happened."
14
+ * Confirmed verbatim against `Exception#full_message`'s own documented
15
+ * example (docs.ruby-lang.org/en/3.4/Exception.html): the array's first
16
+ * entry is `"t.rb:3:in 'Integer#/': divided by 0 (ZeroDivisionError)"`.
17
+ * 2. **Continuation frames are prefixed `from`, not `at`,** and carry no
18
+ * message: `\tfrom t.rb:10:in 'Object#bar'`. Confirmed against the same
19
+ * doc example and against ruby/spec's own
20
+ * `core/exception/top_level_spec.rb`, which asserts the custom-backtrace
21
+ * case renders as:
22
+ * ```
23
+ * /dir/foo.rb:10:in `raising': foo (RuntimeError)
24
+ * \tfrom /dir/bar.rb:20:in `caller'
25
+ * ```
26
+ * 3. **The closing quote is always a single quote; only the opening quote
27
+ * varies by Ruby version.** Ruby 3.4 (Feature #16495) replaced the
28
+ * opening backtick with a single quote and, per Feature #19117, prefixes
29
+ * the owning class where one has a permanent name
30
+ * (`'Object#division'` vs. the pre-3.4 `` `division' ``). Both are real,
31
+ * currently-deployed formats — Ruby 3.4 shipped 2024-12-25, and every
32
+ * LTS-adjacent 3.0–3.3 install in production still prints the backtick
33
+ * form — so both are accepted here via a `[` ']` opening-character class,
34
+ * never just the newer one.
35
+ * 4. **No `isTrailer` is needed.** Because the header already carries the
36
+ * exception's identity, Ruby has nothing analogous to CPython's
37
+ * identity-comes-last problem. Chained exceptions (`Exception#cause`)
38
+ * print as two complete header+frames blocks back to back with **no
39
+ * connective sentence between them** — confirmed against
40
+ * `top_level_spec.rb`'s `"the Exception#cause is printed to STDERR with
41
+ * backtraces"` example — so the second header simply starts a new block
42
+ * once the first one's last `from` line stops matching `isFrame`. No
43
+ * special-casing required, unlike CPython's connective-exclusion.
44
+ *
45
+ * **Known, disclosed gap:** a backtrace whose only frame is the header
46
+ * itself — a bare top-level `raise` with no caller beneath it, e.g.
47
+ * `ruby -e 'raise "boom"'` — has no `from` line to confirm it, and
48
+ * `ExceptionBlockDetector`'s `holding-header` state demotes an unconfirmed
49
+ * header to a plain log line at `flush()`. This is a structural
50
+ * consequence of a header-embeds-frame grammar meeting a classifier
51
+ * interface built around "header, then confirming frame line": there is no
52
+ * lookahead-free way to distinguish "a header about to be followed by
53
+ * frames" from "a header that IS the entire trace" from the header line
54
+ * alone. `parseRubyHeaderFrame` still extracts the correct single frame
55
+ * from such a line if handed it directly — the gap is confined to the
56
+ * streaming block detector's own decision to invoke the parser at all, not
57
+ * to the parser's capability. In practice this only affects genuinely
58
+ * flat top-level scripts; a backend process (the collector's real target)
59
+ * always has at least one caller frame between the raise site and the
60
+ * process entry point.
61
+ */
62
+ /**
63
+ * The combined header+innermost-frame line. Distinguished from a bare
64
+ * `from` line by the trailing `: message` — real `from` lines never carry
65
+ * one (see `FROM_FRAME_PATTERN`). Not anchored to the start of the line,
66
+ * for the same reason V8's header pattern isn't: a backend that logs a
67
+ * caught exception's `full_message` through a leveled logger prefixes a
68
+ * timestamp, and Ruby's own top-level printer (which never adds a prefix)
69
+ * is only one of the two realistic capture paths.
70
+ */
71
+ const HEADER_PATTERN = /(?<file>.+):(?<line>\d+):in\s+[`'](?<fn>[^']*)':\s/;
72
+ /** Same location+name grammar as the header, but `from`-prefixed and with no trailing message. */
73
+ const FROM_FRAME_PATTERN = /^\s*from\s+(?<file>.+):(?<line>\d+):in\s+[`'](?<fn>[^']*)'\s*$/;
74
+ export function isRubyHeaderLine(line) {
75
+ return HEADER_PATTERN.test(line);
76
+ }
77
+ export function isRubyFrameLine(line) {
78
+ return FROM_FRAME_PATTERN.test(line);
79
+ }
80
+ export function createRubyLineClassifier() {
81
+ return {
82
+ isHeader: isRubyHeaderLine,
83
+ isFrame: isRubyFrameLine,
84
+ };
85
+ }
86
+ /** `null` when the header doesn't match Ruby's `file:line:in 'name': message` shape — never fabricated. */
87
+ export function parseRubyHeaderFrame(line) {
88
+ const match = HEADER_PATTERN.exec(line);
89
+ if (!match?.groups)
90
+ return null;
91
+ const file = match.groups["file"];
92
+ const lineNumber = Number(match.groups["line"]);
93
+ if (file === undefined || !Number.isInteger(lineNumber))
94
+ return null;
95
+ return { functionName: match.groups["fn"] ?? null, file, line: lineNumber };
96
+ }
97
+ /** `null` for anything not shaped like a `from file:line:in 'name'` continuation — never fabricated. */
98
+ export function parseRubyFromFrame(line) {
99
+ const match = FROM_FRAME_PATTERN.exec(line);
100
+ if (!match?.groups)
101
+ return null;
102
+ const file = match.groups["file"];
103
+ const lineNumber = Number(match.groups["line"]);
104
+ if (file === undefined || !Number.isInteger(lineNumber))
105
+ return null;
106
+ return { functionName: match.groups["fn"] ?? null, file, line: lineNumber };
107
+ }
108
+ //# sourceMappingURL=ruby-frame-parsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-frame-parsing.js","sourceRoot":"","sources":["../src/ruby-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AASH;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,oDAAoD,CAAC;AAE5E,kGAAkG;AAClG,MAAM,kBAAkB,GAAG,gEAAgE,CAAC;AAE5F,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,wBAAwB;IAItC,OAAO;QACL,QAAQ,EAAE,gBAAgB;QAC1B,OAAO,EAAE,eAAe;KACzB,CAAC;AACJ,CAAC;AAED,2GAA2G;AAC3G,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9E,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9E,CAAC"}