@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,101 @@
1
+ /**
2
+ * The two `LogEnvelope`s Ruby actually ships with: the stdlib `Logger`'s
3
+ * default formatter, and Rails' `ActiveSupport::TaggedLogging`.
4
+ *
5
+ * ## Why Ruby needs these when Python does not
6
+ *
7
+ * RT-090's leading measurement. A real Ruby backtrace logged through a real
8
+ * `Logger` detects **correctly** — right frame count, right function, right
9
+ * line — and reports
10
+ *
11
+ * ```
12
+ * file: "E, [2026-08-19T04:55:54.126478 #763304] ERROR -- : logenv.rb"
13
+ * ```
14
+ *
15
+ * Confident evidence, every number a reviewer would check correct, and the
16
+ * one field that has to resolve to a graph node resolving to nothing.
17
+ * Ruby's header pattern is deliberately unanchored — its own comment
18
+ * anticipates a leveled logger prefixing a timestamp — so the prefix does
19
+ * not stop it matching; the greedy `(?<file>.+)` before `:(\d+):in` simply
20
+ * absorbs the prefix into the path. There is no shape that separates a log
21
+ * prefix from a path containing spaces and a colon, so the wrapper has to
22
+ * come off first. That is this file.
23
+ *
24
+ * By contrast Python needs no **unwrapping**: measured across real Flask
25
+ * 3.1.3, Django 6.1 and FastAPI 0.141.1, all three print
26
+ * `Traceback (most recent call last):` at column 0 with CPython's own
27
+ * two-space frame indent, because `logging` appends `exc_info` unindented.
28
+ * PHP escapes for a different reason again — its header anchors the location
29
+ * at the *end* of the line, so a prefix falls outside the capture.
30
+ *
31
+ * **This paragraph used to end "and never will", and that was wrong**
32
+ * (corrected in RT-106). It is right about unwrapping and wrong about the
33
+ * other half of what this interface does: `startsRecord` is the only way a
34
+ * collector can learn that a line *ended the previous record*, and Python
35
+ * is where that matters most, because a traceback is written one `write()`
36
+ * per line and any concurrent logger interleaves between them.
37
+ * `createUvicornLogEnvelope` in `packages/adapter-python` is the result —
38
+ * an envelope whose `unwrap` is the identity function for every line
39
+ * carrying evidence, and whose entire contribution is the boundary. The
40
+ * general lesson is that "does this framework decorate its lines" and "does
41
+ * this framework tell us where a record starts" are separate questions, and
42
+ * the first one's answer does not settle the second.
43
+ *
44
+ * ## Both formats were measured, and the guess was wrong
45
+ *
46
+ * Written against real output from ruby 3.4.2 and activesupport 8.1.3.1, not
47
+ * from documentation. That mattered: the first version of this work assumed
48
+ * Rails tags looked like `[req-abc] [ERROR] message`, by analogy with every
49
+ * other framework surveyed. Real `TaggedLogging` emits **no level token at
50
+ * all** — it installs its own formatter deriving from `SimpleFormatter`,
51
+ * which discards the severity, pid and timestamp the stdlib formatter would
52
+ * have written. The two formats are therefore disjoint rather than nested,
53
+ * which is why they are two envelopes and not one with a branch.
54
+ *
55
+ * ## What Rails does NOT give, stated because ASP.NET does give it
56
+ *
57
+ * RT-089 found Kestrel writing `Request id "0HNNTFUBAD72K:00000001"` — §16's
58
+ * framework-correlation row arriving for free. **Rails has no equivalent.**
59
+ * Its tags are positional and untyped: `[req-ABC123] [user-42]` is two
60
+ * strings in the order `config.log_tags` listed them, with nothing in the
61
+ * stream saying which is a request id. The near-universal default is
62
+ * `[:request_id]` first, and treating the first tag as `requestId` on that
63
+ * basis would be inferring from a convention rather than reading a
64
+ * declaration — the exact distinction `fields()` exists to keep. So tags go
65
+ * to `attrs` verbatim and `requestId` stays null.
66
+ *
67
+ * ## The disclosed ambiguity in the tagged format
68
+ *
69
+ * `[anything] rest` is not a format Rails owns. An application logging
70
+ * `[fixture] GET /api/invoices/3 failed` matches it, and this envelope will
71
+ * unwrap that line to `GET /api/invoices/3 failed`. Bounded, and stated
72
+ * rather than discovered:
73
+ *
74
+ * - The line is a plain log either way, so no exception is created or lost.
75
+ * - `payload.envelopedRaw` keeps the printed bytes, so nothing is destroyed.
76
+ * - `startsRecord` firing on it is *correct* for the case that matters — a
77
+ * foreign line interleaved into a traceback genuinely does end the record,
78
+ * which is the defect `startsRecord` exists to close.
79
+ *
80
+ * **That third bullet was wrong, and booting a real Rails application is
81
+ * what showed it** (RT-111). `TaggedLogging` tags every line of a multi-line
82
+ * message, so a backtrace arrives as forty lines each looking like a record
83
+ * header; a boundary on all of them makes a multi-line block impossible to
84
+ * assemble. `startsRecord` is `false` for this envelope now — see the
85
+ * comment at the return. The stdlib `Logger` envelope above keeps `true`,
86
+ * because its prefix genuinely appears once per record.
87
+ *
88
+ * The one genuine false positive is a Ruby source line beginning with `[` at
89
+ * column zero, since `error_highlight` prints the offending source line
90
+ * unindented — `[1, 2].each do |x|` would be unwrapped. The anchor requires
91
+ * the bracket at column zero with no leading whitespace, which excludes
92
+ * every indented source line and every `\tfrom` frame; a top-level array
93
+ * literal on the raising line is what remains, and it costs a prefix on a
94
+ * snippet line rather than any part of a frame.
95
+ */
96
+ import type { LogEnvelope } from "@descryy/runtime-backend-observation";
97
+ /** The stdlib `Logger` default formatter. */
98
+ export declare function createRubyLoggerEnvelope(): LogEnvelope;
99
+ /** Rails' `ActiveSupport::TaggedLogging`. */
100
+ export declare function createRailsTaggedLogEnvelope(): LogEnvelope;
101
+ //# sourceMappingURL=ruby-log-envelopes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-log-envelopes.d.ts","sourceRoot":"","sources":["../src/ruby-log-envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAiC,MAAM,sCAAsC,CAAC;AA0CvG,6CAA6C;AAC7C,wBAAgB,wBAAwB,IAAI,WAAW,CAgCtD;AAED,6CAA6C;AAC7C,wBAAgB,4BAA4B,IAAI,WAAW,CAoD1D"}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * The two `LogEnvelope`s Ruby actually ships with: the stdlib `Logger`'s
3
+ * default formatter, and Rails' `ActiveSupport::TaggedLogging`.
4
+ *
5
+ * ## Why Ruby needs these when Python does not
6
+ *
7
+ * RT-090's leading measurement. A real Ruby backtrace logged through a real
8
+ * `Logger` detects **correctly** — right frame count, right function, right
9
+ * line — and reports
10
+ *
11
+ * ```
12
+ * file: "E, [2026-08-19T04:55:54.126478 #763304] ERROR -- : logenv.rb"
13
+ * ```
14
+ *
15
+ * Confident evidence, every number a reviewer would check correct, and the
16
+ * one field that has to resolve to a graph node resolving to nothing.
17
+ * Ruby's header pattern is deliberately unanchored — its own comment
18
+ * anticipates a leveled logger prefixing a timestamp — so the prefix does
19
+ * not stop it matching; the greedy `(?<file>.+)` before `:(\d+):in` simply
20
+ * absorbs the prefix into the path. There is no shape that separates a log
21
+ * prefix from a path containing spaces and a colon, so the wrapper has to
22
+ * come off first. That is this file.
23
+ *
24
+ * By contrast Python needs no **unwrapping**: measured across real Flask
25
+ * 3.1.3, Django 6.1 and FastAPI 0.141.1, all three print
26
+ * `Traceback (most recent call last):` at column 0 with CPython's own
27
+ * two-space frame indent, because `logging` appends `exc_info` unindented.
28
+ * PHP escapes for a different reason again — its header anchors the location
29
+ * at the *end* of the line, so a prefix falls outside the capture.
30
+ *
31
+ * **This paragraph used to end "and never will", and that was wrong**
32
+ * (corrected in RT-106). It is right about unwrapping and wrong about the
33
+ * other half of what this interface does: `startsRecord` is the only way a
34
+ * collector can learn that a line *ended the previous record*, and Python
35
+ * is where that matters most, because a traceback is written one `write()`
36
+ * per line and any concurrent logger interleaves between them.
37
+ * `createUvicornLogEnvelope` in `packages/adapter-python` is the result —
38
+ * an envelope whose `unwrap` is the identity function for every line
39
+ * carrying evidence, and whose entire contribution is the boundary. The
40
+ * general lesson is that "does this framework decorate its lines" and "does
41
+ * this framework tell us where a record starts" are separate questions, and
42
+ * the first one's answer does not settle the second.
43
+ *
44
+ * ## Both formats were measured, and the guess was wrong
45
+ *
46
+ * Written against real output from ruby 3.4.2 and activesupport 8.1.3.1, not
47
+ * from documentation. That mattered: the first version of this work assumed
48
+ * Rails tags looked like `[req-abc] [ERROR] message`, by analogy with every
49
+ * other framework surveyed. Real `TaggedLogging` emits **no level token at
50
+ * all** — it installs its own formatter deriving from `SimpleFormatter`,
51
+ * which discards the severity, pid and timestamp the stdlib formatter would
52
+ * have written. The two formats are therefore disjoint rather than nested,
53
+ * which is why they are two envelopes and not one with a branch.
54
+ *
55
+ * ## What Rails does NOT give, stated because ASP.NET does give it
56
+ *
57
+ * RT-089 found Kestrel writing `Request id "0HNNTFUBAD72K:00000001"` — §16's
58
+ * framework-correlation row arriving for free. **Rails has no equivalent.**
59
+ * Its tags are positional and untyped: `[req-ABC123] [user-42]` is two
60
+ * strings in the order `config.log_tags` listed them, with nothing in the
61
+ * stream saying which is a request id. The near-universal default is
62
+ * `[:request_id]` first, and treating the first tag as `requestId` on that
63
+ * basis would be inferring from a convention rather than reading a
64
+ * declaration — the exact distinction `fields()` exists to keep. So tags go
65
+ * to `attrs` verbatim and `requestId` stays null.
66
+ *
67
+ * ## The disclosed ambiguity in the tagged format
68
+ *
69
+ * `[anything] rest` is not a format Rails owns. An application logging
70
+ * `[fixture] GET /api/invoices/3 failed` matches it, and this envelope will
71
+ * unwrap that line to `GET /api/invoices/3 failed`. Bounded, and stated
72
+ * rather than discovered:
73
+ *
74
+ * - The line is a plain log either way, so no exception is created or lost.
75
+ * - `payload.envelopedRaw` keeps the printed bytes, so nothing is destroyed.
76
+ * - `startsRecord` firing on it is *correct* for the case that matters — a
77
+ * foreign line interleaved into a traceback genuinely does end the record,
78
+ * which is the defect `startsRecord` exists to close.
79
+ *
80
+ * **That third bullet was wrong, and booting a real Rails application is
81
+ * what showed it** (RT-111). `TaggedLogging` tags every line of a multi-line
82
+ * message, so a backtrace arrives as forty lines each looking like a record
83
+ * header; a boundary on all of them makes a multi-line block impossible to
84
+ * assemble. `startsRecord` is `false` for this envelope now — see the
85
+ * comment at the return. The stdlib `Logger` envelope above keeps `true`,
86
+ * because its prefix genuinely appears once per record.
87
+ *
88
+ * The one genuine false positive is a Ruby source line beginning with `[` at
89
+ * column zero, since `error_highlight` prints the offending source line
90
+ * unindented — `[1, 2].each do |x|` would be unwrapped. The anchor requires
91
+ * the bracket at column zero with no leading whitespace, which excludes
92
+ * every indented source line and every `\tfrom` frame; a top-level array
93
+ * literal on the raising line is what remains, and it costs a prefix on a
94
+ * snippet line rather than any part of a frame.
95
+ */
96
+ /**
97
+ * `E, [2026-08-19T04:55:36.409952 #762887] ERROR -- : message`
98
+ *
99
+ * `Logger::Formatter::Format` is `"%s, [%s #%d] %5s -- %s: %s\n"` — severity
100
+ * letter, timestamp, pid, right-padded severity label, progname, message.
101
+ * Anchored at the start and requiring every field, because a partial match
102
+ * is evidence the line is not a header rather than a reason to guess at one.
103
+ * `progname` is `[^:]*` and routinely empty, which is what the bare `-- : `
104
+ * in the measured output is.
105
+ */
106
+ const STDLIB_HEADER = /^([A-Z]), \[([^\]]*) #(\d+)\] +(\w+) -- ([^:]*): /;
107
+ /**
108
+ * One or more `[tag] ` groups at column zero.
109
+ *
110
+ * `ActiveSupport::TaggedLogging::Formatter#tagged` writes `"[#{tag}] "` per
111
+ * tag and then the message, with no other decoration — verified against
112
+ * activesupport 8.1.3.1 for both a single tag and two.
113
+ */
114
+ const TAGGED_HEADER = /^((?:\[[^\]]*\] )+)/;
115
+ /** Every `[tag]` inside a matched tag run. */
116
+ const TAG = /\[([^\]]*)\]/g;
117
+ /**
118
+ * A continuation line, for both envelopes.
119
+ *
120
+ * Neither Ruby formatter wraps anything after the record's first line — the
121
+ * `full_message` body (blank line, `error_highlight` snippet, caret line,
122
+ * `\tfrom` frames) is written through untouched. Measured for both. So
123
+ * "unwrap a continuation" is the identity function here, and the only thing
124
+ * an envelope has to get right is not mistaking a continuation for a header.
125
+ *
126
+ * This is the property that makes Ruby cheap and .NET expensive: .NET
127
+ * indents every line of the record and so must strip on every line.
128
+ */
129
+ function continuation(line) {
130
+ return { content: line, startsRecord: false };
131
+ }
132
+ /** The stdlib `Logger` default formatter. */
133
+ export function createRubyLoggerEnvelope() {
134
+ let fields = null;
135
+ return {
136
+ frameworkName: "ruby-stdlib-logger",
137
+ unwrap(line) {
138
+ const match = STDLIB_HEADER.exec(line);
139
+ if (match === null)
140
+ return continuation(line);
141
+ const [, severityChar, timestamp, pid, level, progname] = match;
142
+ fields = {
143
+ level: level ?? null,
144
+ // `progname` is Logger's own word for it and is usually empty. Mapped
145
+ // to `category` because that is the field's meaning, not because
146
+ // Ruby uses the word.
147
+ category: progname !== undefined && progname !== "" ? progname : null,
148
+ // Ruby's stdlib logger has no concept of a request. Null rather than
149
+ // scavenged from the message text — that is `extractCorrelationIds`'
150
+ // job and it still runs on the unwrapped content.
151
+ requestId: null,
152
+ attrs: {
153
+ severityChar: severityChar ?? "",
154
+ timestamp: timestamp ?? "",
155
+ pid: pid ?? "",
156
+ },
157
+ };
158
+ return { content: line.slice(match[0].length), startsRecord: true };
159
+ },
160
+ fields: () => fields,
161
+ };
162
+ }
163
+ /** Rails' `ActiveSupport::TaggedLogging`. */
164
+ export function createRailsTaggedLogEnvelope() {
165
+ let fields = null;
166
+ return {
167
+ frameworkName: "rails-tagged-logging",
168
+ unwrap(line) {
169
+ const match = TAGGED_HEADER.exec(line);
170
+ if (match === null)
171
+ return continuation(line);
172
+ const run = match[1] ?? "";
173
+ const tags = [...run.matchAll(TAG)].map((m) => m[1] ?? "");
174
+ fields = {
175
+ // TaggedLogging's formatter derives from SimpleFormatter, which
176
+ // writes no severity. Measured: the level is genuinely absent from
177
+ // the stream, not merely unparsed.
178
+ level: null,
179
+ category: null,
180
+ // Positional and untyped — see this file's header. Rails does not
181
+ // say which tag is a request id, so neither does this.
182
+ requestId: null,
183
+ attrs: Object.fromEntries(tags.map((t, i) => [`tag${i}`, t])),
184
+ };
185
+ // **`false`, and this is the one place in this repository where a
186
+ // recognised header does NOT declare a record boundary** (RT-111).
187
+ //
188
+ // `ActiveSupport::TaggedLogging::Formatter` tags **every line** of a
189
+ // multi-line message, not the first. So a Rails backtrace arrives as
190
+ // forty consecutive lines each carrying the same `[request-id] ` run,
191
+ // and each of them looks exactly like a record header. Claiming a
192
+ // boundary on all of them makes a multi-line exception block
193
+ // impossible to assemble: the collector flushes the detector before
194
+ // every line, so every frame becomes its own one-line STACK_TRACE.
195
+ //
196
+ // Measured against a live Rails 8.0.4 failure: **80 evidence records,
197
+ // 60 of them single-frame STACK_TRACEs, zero EXCEPTIONs**, from a
198
+ // grammar that recognised every line correctly.
199
+ //
200
+ // RT-103 wrote `true` here with the argument that a foreign line
201
+ // interleaved into a backtrace genuinely does end the record. That
202
+ // argument is sound for a format where only the record's first line
203
+ // carries the prefix — Kestrel's, and Ruby's own stdlib `Logger`
204
+ // above, both of which keep `true`. It is false for a format where
205
+ // the prefix is per *line*, and Rails' is. The distinction is not
206
+ // "does this envelope recognise the line" but "does this framework's
207
+ // marker mean a record began", and only the second one may be
208
+ // reported here.
209
+ return { content: line.slice(run.length), startsRecord: false };
210
+ },
211
+ fields: () => fields,
212
+ };
213
+ }
214
+ //# sourceMappingURL=ruby-log-envelopes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-log-envelopes.js","sourceRoot":"","sources":["../src/ruby-log-envelopes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The concrete `RuntimeAdapter` for Ruby.
3
+ *
4
+ * Ruby's grammar needed no seam changes: unlike CPython (which forced
5
+ * `isTrailer` and the most-recent-first contract into existence — RT-035),
6
+ * Ruby's header-embeds-the-innermost-frame shape and native
7
+ * most-recent-first order both fit the existing `LineClassifier` /
8
+ * `StackTrace.frames` contracts without modification. See
9
+ * `ruby-frame-parsing.ts` for why, and for the one disclosed gap (a
10
+ * top-level raise with zero caller frames) that shape leaves behind.
11
+ */
12
+ import type { RuntimeAdapter } from "@descryy/runtime-backend-observation";
13
+ export interface RubyRuntimeAdapterOptions {
14
+ /** Attributed on every emitted Evidence's `service` field (plan §16.7). Omit when not applicable. */
15
+ readonly service?: string;
16
+ /**
17
+ * The framework whose exception reporting this process uses (§16, RT-111).
18
+ *
19
+ * **Declared, never sniffed**, the same rule ASP.NET's option follows.
20
+ * Rails does not log `Exception#full_message` — `ActionDispatch::DebugExceptions`
21
+ * composes `Class (message):` followed by bare `file:line:in 'name'` lines
22
+ * with no `from` prefix, which matches neither of Ruby's two patterns.
23
+ * Measured against a live Rails 8.0.4 request pipeline: **77 lines in,
24
+ * zero exceptions out.**
25
+ *
26
+ * Selecting this swaps in a different classifier *and* a different stack
27
+ * trace parser, because the two grammars disagree about where the raise
28
+ * site is: Ruby's header is the innermost frame, Rails' header has no
29
+ * location at all.
30
+ *
31
+ * Detecting it from a line that looks like Rails output would apply this
32
+ * grammar to a plain Ruby process that happened to print one, and Rails'
33
+ * frame shape is loose enough that the cost would be silent.
34
+ *
35
+ * `"sinatra"` is the same declared-not-sniffed answer for Sinatra
36
+ * **running in production mode** (`set :environment, :production`) —
37
+ * measured (plan 1C) against a real Sinatra 4.1.1 process:
38
+ * `Sinatra::Base#dump_errors!` composes `TIMESTAMP - Class - message:`
39
+ * followed by raw, `from`-less `Exception#backtrace` lines, matching
40
+ * neither of Ruby's own two patterns. See `sinatra-frame-parsing.ts`.
41
+ *
42
+ * Bare Rack — no framework at all — needs **no** option here: its own
43
+ * dispatch frames (`Rackup::Handler::WEBrick#service`, …) print in
44
+ * Ruby's ordinary `file:line:in 'name'` / `from …` shape and the plain
45
+ * classifier below reads them unchanged. Measured, not assumed — see
46
+ * `rack-framework-lifecycle.test.ts`.
47
+ */
48
+ readonly framework?: "rails" | "sinatra";
49
+ }
50
+ export declare function createRubyRuntimeAdapter(options?: RubyRuntimeAdapterOptions): RuntimeAdapter;
51
+ //# sourceMappingURL=ruby-runtime-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-runtime-adapter.d.ts","sourceRoot":"","sources":["../src/ruby-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,sCAAsC,CAAC;AAahG,MAAM,WAAW,yBAAyB;IACxC,qGAAqG;IACrG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,yBAA8B,GAAG,cAAc,CAsChG"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The concrete `RuntimeAdapter` for Ruby.
3
+ *
4
+ * Ruby's grammar needed no seam changes: unlike CPython (which forced
5
+ * `isTrailer` and the most-recent-first contract into existence — RT-035),
6
+ * Ruby's header-embeds-the-innermost-frame shape and native
7
+ * most-recent-first order both fit the existing `LineClassifier` /
8
+ * `StackTrace.frames` contracts without modification. See
9
+ * `ruby-frame-parsing.ts` for why, and for the one disclosed gap (a
10
+ * top-level raise with zero caller frames) that shape leaves behind.
11
+ */
12
+ import { createLogCollector } from "@descryy/runtime-backend-observation";
13
+ import { createRailsLineClassifier } from "./rails-frame-parsing.js";
14
+ import { createRailsStackTraceParser } from "./rails-stack-trace-parser.js";
15
+ import { createRubyLineClassifier } from "./ruby-frame-parsing.js";
16
+ import { createRailsTaggedLogEnvelope } from "./ruby-log-envelopes.js";
17
+ import { createRubySourceLocationResolver } from "./ruby-source-location-resolver.js";
18
+ import { createRubyStackTraceParser } from "./ruby-stack-trace-parser.js";
19
+ import { createSinatraLineClassifier } from "./sinatra-frame-parsing.js";
20
+ import { createSinatraStackTraceParser } from "./sinatra-stack-trace-parser.js";
21
+ export function createRubyRuntimeAdapter(options = {}) {
22
+ const sourceLocationResolver = createRubySourceLocationResolver();
23
+ const isRails = options.framework === "rails";
24
+ const isSinatra = options.framework === "sinatra";
25
+ const stackTraceParser = isRails
26
+ ? createRailsStackTraceParser(sourceLocationResolver)
27
+ : isSinatra
28
+ ? createSinatraStackTraceParser(sourceLocationResolver)
29
+ : createRubyStackTraceParser(sourceLocationResolver);
30
+ function createBackendCollectors(sources) {
31
+ return sources.map((source) => createLogCollector({
32
+ source,
33
+ // Ruby's classifier is stateless (its markers — the "from " prefix
34
+ // and the trailing ": message" — are unambiguous per-line), but a
35
+ // fresh instance per collector costs nothing and keeps the
36
+ // construction pattern identical across every language adapter.
37
+ classifier: isRails ? createRailsLineClassifier() : isSinatra ? createSinatraLineClassifier() : createRubyLineClassifier(),
38
+ stackTraceParser,
39
+ // Rails logs through `ActiveSupport::TaggedLogging` by default, and
40
+ // the tag run corrupts the file path of every frame (RT-103). The
41
+ // envelope comes with the framework rather than as a separate
42
+ // option, because a caller that knows it launched Rails knows both.
43
+ // Sinatra's `dump_errors!` writes straight to `rack.errors` with no
44
+ // tag run, so it needs no envelope.
45
+ ...(isRails ? { envelope: createRailsTaggedLogEnvelope() } : {}),
46
+ ...(options.service !== undefined ? { service: options.service } : {}),
47
+ }));
48
+ }
49
+ return {
50
+ language: "ruby",
51
+ stackTraceParser,
52
+ sourceLocationResolver,
53
+ createBackendCollectors,
54
+ };
55
+ }
56
+ //# sourceMappingURL=ruby-runtime-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-runtime-adapter.js","sourceRoot":"","sources":["../src/ruby-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAE1E,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAwChF,MAAM,UAAU,wBAAwB,CAAC,UAAqC,EAAE;IAC9E,MAAM,sBAAsB,GAAG,gCAAgC,EAAE,CAAC;IAClE,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,KAAK,OAAO,CAAC;IAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;IAClD,MAAM,gBAAgB,GAAG,OAAO;QAC9B,CAAC,CAAC,2BAA2B,CAAC,sBAAsB,CAAC;QACrD,CAAC,CAAC,SAAS;YACT,CAAC,CAAC,6BAA6B,CAAC,sBAAsB,CAAC;YACvD,CAAC,CAAC,0BAA0B,CAAC,sBAAsB,CAAC,CAAC;IAEzD,SAAS,uBAAuB,CAAC,OAAuC;QACtE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC5B,kBAAkB,CAAC;YACjB,MAAM;YACN,mEAAmE;YACnE,kEAAkE;YAClE,2DAA2D;YAC3D,gEAAgE;YAChE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,yBAAyB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC,wBAAwB,EAAE;YAC1H,gBAAgB;YAChB,oEAAoE;YACpE,kEAAkE;YAClE,8DAA8D;YAC9D,oEAAoE;YACpE,oEAAoE;YACpE,oCAAoC;YACpC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,4BAA4B,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE,CAAC,CACH,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,gBAAgB;QAChB,sBAAsB;QACtB,uBAAuB;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Ruby `SourceLocationResolver`.
3
+ *
4
+ * **Almost every location is `self-contained`, and that is a positive claim
5
+ * rather than a fallback** — the same category RT-003 already assigns Ruby
6
+ * backtraces alongside Python tracebacks and PHP traces. A Ruby backtrace
7
+ * line names the `.rb` file and line the interpreter was actually
8
+ * executing; there is no compilation step and no separate map that can
9
+ * drift or go missing. The frame IS the answer.
10
+ *
11
+ * **What this deliberately does not do:** check that the file exists on
12
+ * disk, or that its contents still match what ran. `self-contained` is a
13
+ * statement about the *mechanism* (nothing was lost in translation), not
14
+ * about whether the working tree has since been edited.
15
+ *
16
+ * **The one exception: `instance_eval`/`eval` frames.** Ruby names their
17
+ * location `(eval at file.rb:12):1` — `file.rb:12` is where `eval` was
18
+ * called from, but `1` counts lines *within the evaluated string*, not
19
+ * within `file.rb`. Opening `file.rb` at line 1 would show unrelated code,
20
+ * so calling this `self-contained` would be false: something *was* lost —
21
+ * the line no longer indexes the named file. `isEvalSyntheticPath` detects
22
+ * Ruby's own synthetic-path shape (verbatim, not reparsed here — the
23
+ * parser already hands it through unchanged; see `ruby-frame-parsing.ts`)
24
+ * and this resolver reports `self-contained-eval-line` instead. The path
25
+ * itself is still reported verbatim, never rewritten to a guessed
26
+ * `file.rb:12` — a guess could be wrong and nothing downstream could tell.
27
+ *
28
+ * `column` is always null. Ruby backtraces never print one — there is
29
+ * nothing to derive it from, unlike CPython's 3.11+ caret line, so this
30
+ * isn't even a "could infer but won't" case.
31
+ */
32
+ import type { SourceLocationResolver } from "@descryy/runtime-backend-observation";
33
+ export declare function createRubySourceLocationResolver(): SourceLocationResolver;
34
+ //# sourceMappingURL=ruby-source-location-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-source-location-resolver.d.ts","sourceRoot":"","sources":["../src/ruby-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAiBrG,wBAAgB,gCAAgC,IAAI,sBAAsB,CAezE"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Ruby `SourceLocationResolver`.
3
+ *
4
+ * **Almost every location is `self-contained`, and that is a positive claim
5
+ * rather than a fallback** — the same category RT-003 already assigns Ruby
6
+ * backtraces alongside Python tracebacks and PHP traces. A Ruby backtrace
7
+ * line names the `.rb` file and line the interpreter was actually
8
+ * executing; there is no compilation step and no separate map that can
9
+ * drift or go missing. The frame IS the answer.
10
+ *
11
+ * **What this deliberately does not do:** check that the file exists on
12
+ * disk, or that its contents still match what ran. `self-contained` is a
13
+ * statement about the *mechanism* (nothing was lost in translation), not
14
+ * about whether the working tree has since been edited.
15
+ *
16
+ * **The one exception: `instance_eval`/`eval` frames.** Ruby names their
17
+ * location `(eval at file.rb:12):1` — `file.rb:12` is where `eval` was
18
+ * called from, but `1` counts lines *within the evaluated string*, not
19
+ * within `file.rb`. Opening `file.rb` at line 1 would show unrelated code,
20
+ * so calling this `self-contained` would be false: something *was* lost —
21
+ * the line no longer indexes the named file. `isEvalSyntheticPath` detects
22
+ * Ruby's own synthetic-path shape (verbatim, not reparsed here — the
23
+ * parser already hands it through unchanged; see `ruby-frame-parsing.ts`)
24
+ * and this resolver reports `self-contained-eval-line` instead. The path
25
+ * itself is still reported verbatim, never rewritten to a guessed
26
+ * `file.rb:12` — a guess could be wrong and nothing downstream could tell.
27
+ *
28
+ * `column` is always null. Ruby backtraces never print one — there is
29
+ * nothing to derive it from, unlike CPython's 3.11+ caret line, so this
30
+ * isn't even a "could infer but won't" case.
31
+ */
32
+ /**
33
+ * Ruby's synthetic path for a frame inside `eval`/`instance_eval`:
34
+ * `(eval)` on <3.4, `(eval at file.rb:12)` on 3.4+ (Feature #19117), and
35
+ * `(eval at (eval at file.rb:12):3)` when an eval nests inside another
36
+ * eval — all confirmed against a real `ruby` binary (see
37
+ * `ruby-call-shapes.test.ts`). `.+` is greedy so a nested case still
38
+ * matches as a whole; there is nothing to extract from inside it, since
39
+ * the value is reported verbatim either way.
40
+ */
41
+ const EVAL_SYNTHETIC_PATH_PATTERN = /^\(eval(?: at .+)?\)$/;
42
+ function isEvalSyntheticPath(file) {
43
+ return file !== null && EVAL_SYNTHETIC_PATH_PATTERN.test(file);
44
+ }
45
+ export function createRubySourceLocationResolver() {
46
+ return {
47
+ // Async to satisfy the interface; Ruby needs no I/O to answer, so this
48
+ // never awaits — same as the Python resolver, for the same reason.
49
+ resolve(frame) {
50
+ return Promise.resolve({
51
+ file: frame.file,
52
+ line: frame.line,
53
+ column: frame.column,
54
+ functionName: frame.functionName,
55
+ reliability: isEvalSyntheticPath(frame.file) ? "self-contained-eval-line" : "self-contained",
56
+ resolvedVia: null,
57
+ });
58
+ },
59
+ };
60
+ }
61
+ //# sourceMappingURL=ruby-source-location-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-source-location-resolver.js","sourceRoot":"","sources":["../src/ruby-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH;;;;;;;;GAQG;AACH,MAAM,2BAA2B,GAAG,uBAAuB,CAAC;AAE5D,SAAS,mBAAmB,CAAC,IAAmB;IAC9C,OAAO,IAAI,KAAK,IAAI,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,gCAAgC;IAC9C,OAAO;QACL,uEAAuE;QACvE,mEAAmE;QACnE,OAAO,CAAC,KAAuB;YAC7B,OAAO,OAAO,CAAC,OAAO,CAAC;gBACrB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,WAAW,EAAE,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,gBAAgB;gBAC5F,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Composes the pure Ruby line-recognition functions
3
+ * (`ruby-frame-parsing.ts`) with a `SourceLocationResolver` to implement the
4
+ * full async `StackTraceParser` contract.
5
+ *
6
+ * **No reversal.** Unlike CPython, Ruby prints most-recent-call first
7
+ * already — the header line IS the innermost frame, and each `from` line
8
+ * walks outward toward `<main>`. `StackTrace.frames` is contractually
9
+ * most-recent-first, so this parser emits frames in the order it reads
10
+ * them. (Of the languages implemented against this seam so far, CPython is
11
+ * the outlier that reverses; V8, and now Ruby, both print native
12
+ * most-recent-first order.)
13
+ *
14
+ * `fidelity` is always reported `"synchronous"`. A Ruby backtrace is built
15
+ * by walking the actual call frames of the raising thread/fiber — every
16
+ * frame shown is a genuine, unbroken link in the call chain that was
17
+ * executing. What it cannot show is a logical caller across a `Thread.new`
18
+ * hand-off or a Fiber resumption boundary, the same class of limit V8 and
19
+ * CPython stacks share; detecting that would need scheduler-level task
20
+ * correlation, not text parsing, so it is not claimed here.
21
+ */
22
+ import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
23
+ export declare function createRubyStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
24
+ //# sourceMappingURL=ruby-stack-trace-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruby-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/ruby-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAIrG,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAkD7F"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Composes the pure Ruby line-recognition functions
3
+ * (`ruby-frame-parsing.ts`) with a `SourceLocationResolver` to implement the
4
+ * full async `StackTraceParser` contract.
5
+ *
6
+ * **No reversal.** Unlike CPython, Ruby prints most-recent-call first
7
+ * already — the header line IS the innermost frame, and each `from` line
8
+ * walks outward toward `<main>`. `StackTrace.frames` is contractually
9
+ * most-recent-first, so this parser emits frames in the order it reads
10
+ * them. (Of the languages implemented against this seam so far, CPython is
11
+ * the outlier that reverses; V8, and now Ruby, both print native
12
+ * most-recent-first order.)
13
+ *
14
+ * `fidelity` is always reported `"synchronous"`. A Ruby backtrace is built
15
+ * by walking the actual call frames of the raising thread/fiber — every
16
+ * frame shown is a genuine, unbroken link in the call chain that was
17
+ * executing. What it cannot show is a logical caller across a `Thread.new`
18
+ * hand-off or a Fiber resumption boundary, the same class of limit V8 and
19
+ * CPython stacks share; detecting that would need scheduler-level task
20
+ * correlation, not text parsing, so it is not claimed here.
21
+ */
22
+ import { parseRubyHeaderFrame, parseRubyFromFrame } from "./ruby-frame-parsing.js";
23
+ export function createRubyStackTraceParser(resolver) {
24
+ return {
25
+ async parse(rawText) {
26
+ const lines = rawText.split(/\r\n|\r|\n/);
27
+ const frames = [];
28
+ for (const line of lines) {
29
+ // A header line's trailing `: message` makes it more specific than
30
+ // a bare `from` line, so it is tried first; a genuine `from` line
31
+ // never matches the header pattern (see FROM_FRAME_PATTERN's
32
+ // required prefix), so trying both in either order is safe.
33
+ const raw = parseRubyHeaderFrame(line) ?? parseRubyFromFrame(line);
34
+ if (raw === null)
35
+ continue;
36
+ const location = await resolver.resolve({
37
+ file: raw.file,
38
+ line: raw.line,
39
+ column: null,
40
+ functionName: raw.functionName,
41
+ });
42
+ frames.push({ location, raw: line });
43
+ }
44
+ if (frames.length === 0) {
45
+ // Nothing frame-shaped at all — not recognisable as a Ruby
46
+ // backtrace, per this interface's contract: null, never an
47
+ // empty-but-structured StackTrace standing in for "couldn't parse".
48
+ return null;
49
+ }
50
+ // `primaryFrameIndex` is required so that every adapter states an
51
+ // answer rather than inheriting one (RT-083). Ruby's is 0, and the
52
+ // reason is structural rather than incidental: **Ruby's header line
53
+ // IS the raise site** (`file:line:in 'method': message (Class)`), it
54
+ // is parsed as a frame, and it is always the first line of the block.
55
+ // Ruby prints no unwinding frames above it — the `from` lines walk
56
+ // strictly outward toward `<main>`.
57
+ //
58
+ // **It is not necessarily application code, and that is fine.**
59
+ // Measured: `JSON.parse("{not json}")` from a two-deep call chain
60
+ // gives a header of
61
+ // `.../3.4.0/json/common.rb:221:in 'JSON::Ext::Parser.parse': ...`,
62
+ // a stdlib frame, with the application's own frames below it. Index 0
63
+ // still names it, because this field marks the failure site rather
64
+ // than the nearest owned code — see the contract's own note that
65
+ // nothing here denylists a stdlib path, since that "would break on
66
+ // any application that legitimately fails inside a library frame."
67
+ return { fidelity: "synchronous", frames, primaryFrameIndex: 0 };
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=ruby-stack-trace-parser.js.map