@descryy/runtime-adapter-python 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.
@@ -0,0 +1,8 @@
1
+ export type { RawPythonFrame } from "./python-frame-parsing.ts";
2
+ export { CHAINED_EXCEPTION_CONNECTIVES, createPythonLineClassifier, isPythonBannerLine, isPythonLocationLine, isPythonTrailerLine, looksLikeContinuation, parsePythonFrame, } from "./python-frame-parsing.ts";
3
+ export { createUvicornLogEnvelope } from "./python-log-envelopes.ts";
4
+ export { createPythonSourceLocationResolver } from "./python-source-location-resolver.ts";
5
+ export { createPythonStackTraceParser } from "./python-stack-trace-parser.ts";
6
+ export type { PythonRuntimeAdapterOptions } from "./python-runtime-adapter.ts";
7
+ export { createPythonRuntimeAdapter } from "./python-runtime-adapter.ts";
8
+ //# 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,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EACL,6BAA6B,EAC7B,0BAA0B,EAC1B,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,sCAAsC,CAAC;AAC1F,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,YAAY,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { CHAINED_EXCEPTION_CONNECTIVES, createPythonLineClassifier, isPythonBannerLine, isPythonLocationLine, isPythonTrailerLine, looksLikeContinuation, parsePythonFrame, } from "./python-frame-parsing.js";
2
+ export { createUvicornLogEnvelope } from "./python-log-envelopes.js";
3
+ export { createPythonSourceLocationResolver } from "./python-source-location-resolver.js";
4
+ export { createPythonStackTraceParser } from "./python-stack-trace-parser.js";
5
+ export { createPythonRuntimeAdapter } from "./python-runtime-adapter.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,6BAA6B,EAC7B,0BAA0B,EAC1B,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,sCAAsC,CAAC;AAC1F,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAE9E,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * CPython traceback line shapes — the `LineClassifier` for
3
+ * `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
4
+ * feeds `SourceLocationResolver`.
5
+ *
6
+ * CPython's format differs from V8's in three ways that matter, and each one
7
+ * is a place a naive port of the V8 classifier would fail quietly rather
8
+ * than loudly:
9
+ *
10
+ * 1. **The opening line names no exception.** `Traceback (most recent call
11
+ * last):` is a fixed banner; the type and message arrive *after* every
12
+ * frame. That is what `isPythonTrailerLine` exists for — see
13
+ * `LineClassifier.isTrailer`.
14
+ * 2. **Frames print oldest-call-first.** The failing frame is last, the
15
+ * opposite of V8. The reversal is done in the parser, not here, because
16
+ * `StackTrace.frames` is contractually most-recent-first.
17
+ * 3. **A frame is more than one line.** The `File "...", line N, in f`
18
+ * line is followed by the source text, and on 3.11+ by a caret line
19
+ * (` ^^^^^^`) marking the failing expression. Those continuation
20
+ * lines carry no location of their own but belong to the block, so
21
+ * `isPythonFrameLine` accepts them — otherwise the first source line
22
+ * would terminate the block and truncate every traceback to one frame.
23
+ *
24
+ * The banner test is deliberately not anchored to the start of the line, for
25
+ * the same reason the V8 header test isn't: real applications log through
26
+ * formatters that prefix timestamps and levels. The block detector's own
27
+ * "banner immediately followed by a frame" rule is what keeps that safe — a
28
+ * log line merely mentioning the word with nothing frame-shaped after it
29
+ * never becomes an exception.
30
+ */
31
+ export interface RawPythonFrame {
32
+ /** `<module>`, `<lambda>` and `<listcomp>` are real CPython function names and are kept verbatim, not nulled. */
33
+ readonly functionName: string | null;
34
+ /** Verbatim, including CPython's pseudo-files: `<string>`, `<stdin>`, `<frozen importlib._bootstrap>`. */
35
+ readonly file: string;
36
+ readonly line: number;
37
+ }
38
+ /** Removes PEP 654's tree gutter, if present. Identity for an ordinary traceback line. */
39
+ export declare function stripExceptionGroupGutter(line: string): string;
40
+ /**
41
+ * Two connective lines CPython prints *between* chained tracebacks. They are
42
+ * deliberately NOT trailers: each chained traceback is its own complete
43
+ * exception block, and swallowing the connective line into the first block
44
+ * would attach the wrong text to it. They fall through as plain log lines,
45
+ * and the second `Traceback` banner opens a second block — which reports
46
+ * chained exceptions as two honest blocks plus the sentence linking them,
47
+ * rather than one block silently missing half its content.
48
+ */
49
+ export declare const CHAINED_EXCEPTION_CONNECTIVES: readonly ["During handling of the above exception, another exception occurred:", "The above exception was the direct cause of the following exception:"];
50
+ export declare function isPythonBannerLine(line: string): boolean;
51
+ export declare function isPythonLocationLine(line: string): boolean;
52
+ /**
53
+ * Indentation alone — **not** sufficient on its own to call a line a frame.
54
+ * See `createPythonLineClassifier`, which is what the adapter actually uses.
55
+ */
56
+ export declare function looksLikeContinuation(line: string): boolean;
57
+ export declare function isPythonTrailerLine(line: string): boolean;
58
+ /**
59
+ * A stateful `LineClassifier`, and the statefulness is the point.
60
+ *
61
+ * The obvious stateless rule — "a frame is a `File` line **or** anything
62
+ * indented" — is wrong, and the cross-language control in this package's
63
+ * tests is what caught it. `ExceptionBlockDetector` opens a block the
64
+ * moment `isFrame` returns true from idle, so a stateless indent rule turns
65
+ * **every indented log line in the application's output** into the start of
66
+ * a spurious stack-trace block: a pretty-printed JSON payload, a wrapped
67
+ * message, an indented SQL statement. It also accepts another runtime's
68
+ * frames verbatim (` at deleteInvoice (file:///app/db.js:17:22)` is just
69
+ * an indented line), so a mis-wired adapter would half-work rather than
70
+ * fail.
71
+ *
72
+ * A continuation is therefore only recognised **after a `File` line has
73
+ * been seen** — which is the actual rule CPython's format obeys, since
74
+ * source text and caret markers only ever appear beneath a location line.
75
+ * Any line that is neither a location nor a continuation clears that state,
76
+ * so a block that ends without a trailer cannot leave the classifier primed.
77
+ *
78
+ * This depends on lines arriving in stream order, which `ExceptionBlockDetector`
79
+ * provides by construction — it is a one-line-at-a-time streaming detector
80
+ * and `isFrame` is consulted for every line in every state. A fresh
81
+ * classifier per collector keeps two processes' output from sharing state.
82
+ */
83
+ export declare function createPythonLineClassifier(): {
84
+ isHeader(line: string): boolean;
85
+ isFrame(line: string): boolean;
86
+ isTrailer(line: string): boolean;
87
+ };
88
+ /** `null` for a continuation line, or a `File` line with no parseable line number — never fabricated. */
89
+ export declare function parsePythonFrame(line: string): RawPythonFrame | null;
90
+ //# sourceMappingURL=python-frame-parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-frame-parsing.d.ts","sourceRoot":"","sources":["../src/python-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,WAAW,cAAc;IAC7B,iHAAiH;IACjH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,0GAA0G;IAC1G,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAyDD,0FAA0F;AAC1F,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9D;AA6ED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,0JAGhC,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,0BAA0B,IAAI;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC,CA+BA;AAED,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAOpE"}
@@ -0,0 +1,259 @@
1
+ /**
2
+ * CPython traceback line shapes — the `LineClassifier` for
3
+ * `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
4
+ * feeds `SourceLocationResolver`.
5
+ *
6
+ * CPython's format differs from V8's in three ways that matter, and each one
7
+ * is a place a naive port of the V8 classifier would fail quietly rather
8
+ * than loudly:
9
+ *
10
+ * 1. **The opening line names no exception.** `Traceback (most recent call
11
+ * last):` is a fixed banner; the type and message arrive *after* every
12
+ * frame. That is what `isPythonTrailerLine` exists for — see
13
+ * `LineClassifier.isTrailer`.
14
+ * 2. **Frames print oldest-call-first.** The failing frame is last, the
15
+ * opposite of V8. The reversal is done in the parser, not here, because
16
+ * `StackTrace.frames` is contractually most-recent-first.
17
+ * 3. **A frame is more than one line.** The `File "...", line N, in f`
18
+ * line is followed by the source text, and on 3.11+ by a caret line
19
+ * (` ^^^^^^`) marking the failing expression. Those continuation
20
+ * lines carry no location of their own but belong to the block, so
21
+ * `isPythonFrameLine` accepts them — otherwise the first source line
22
+ * would terminate the block and truncate every traceback to one frame.
23
+ *
24
+ * The banner test is deliberately not anchored to the start of the line, for
25
+ * the same reason the V8 header test isn't: real applications log through
26
+ * formatters that prefix timestamps and levels. The block detector's own
27
+ * "banner immediately followed by a frame" rule is what keeps that safe — a
28
+ * log line merely mentioning the word with nothing frame-shaped after it
29
+ * never becomes an exception.
30
+ */
31
+ const BANNER_PATTERN = /Traceback \(most recent call last\):/;
32
+ /**
33
+ * PEP 654's tree gutter, stripped before every other pattern in this file
34
+ * sees a line.
35
+ *
36
+ * ## The measurement
37
+ *
38
+ * `asyncio.TaskGroup` (stdlib since 3.11) and every `ExceptionGroup` print
39
+ * their tracebacks inside a drawn tree:
40
+ *
41
+ * ```
42
+ * + Exception Group Traceback (most recent call last):
43
+ * | File "/app/x.py", line 13, in <module>
44
+ * | asyncio.run(main())
45
+ * | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
46
+ * +-+---------------- 1 ----------------
47
+ * | Traceback (most recent call last):
48
+ * | File "/app/x.py", line 4, in read_customer_name
49
+ * | TypeError: 'NoneType' object is not subscriptable
50
+ * +------------------------------------
51
+ * ```
52
+ *
53
+ * Every location line is ` | File "..."`, and `FILE_LINE_LOOSE_PATTERN`
54
+ * anchors `^\s*File`. So **not one frame line matched**. Measured through
55
+ * the real detector on real output: 28 lines in, **28 plain logs out** —
56
+ * no EXCEPTION, no STACK_TRACE, no frames, and no degradation reported.
57
+ *
58
+ * Reachable through the front door, not a corner: a real FastAPI service
59
+ * with an async endpoint failing inside a `TaskGroup` produced two genuine
60
+ * `TypeError`s and the pipeline reported **one**. Half the failures silently
61
+ * gone, from a healthy-looking collector.
62
+ *
63
+ * ## Why it was invisible
64
+ *
65
+ * Every Python fixture in this repository raises from a plain synchronous
66
+ * call chain, so nothing was ever grouped. Lane A hit the identical shape on
67
+ * the JVM (RT-094): `isFrame` rejected JPMS module-qualified frames, every
68
+ * stack truncated at its first reflective call, and it was invisible because
69
+ * every JVM fixture threw directly from `main`. **A fixture that exercises
70
+ * only the simple call shape certifies only the simple call shape** — that
71
+ * is a property of the fixture, not of the language, and it has now cost two
72
+ * languages the same way.
73
+ *
74
+ * ## Why stripping is safe here
75
+ *
76
+ * The pattern is deliberately tighter than "any leading pipe": exactly one
77
+ * `|` or `+` followed by exactly one space, repeated for nesting. The
78
+ * separator rules (`+-+------- 1 -------`, `+---------`) have `-` after the
79
+ * `+` and so are never stripped — they stay unrecognised, which is correct,
80
+ * because they are decoration and belong to no frame. A traceback that is
81
+ * not in a group has no gutter and is untouched.
82
+ */
83
+ const GROUP_GUTTER = /^(?:[ \t]*[|+] )+/;
84
+ /** Removes PEP 654's tree gutter, if present. Identity for an ordinary traceback line. */
85
+ export function stripExceptionGroupGutter(line) {
86
+ return line.replace(GROUP_GUTTER, "");
87
+ }
88
+ /**
89
+ * The two-space-indented location line. `, in <name>` is optional: it is
90
+ * absent for a frame compiled from a bare string, and `line N` is absent in
91
+ * a handful of exotic cases, which is why the line number is required by
92
+ * `parsePythonFrame` but not by the classifier — recognising a line as
93
+ * belonging to the block is a weaker claim than extracting a location from
94
+ * it, and conflating the two loses frames rather than merely their detail.
95
+ */
96
+ const FILE_LINE_PATTERN = /^\s*File "(?<file>.*)", line (?<line>\d+)(?:, in (?<fn>.*))?\s*$/;
97
+ const FILE_LINE_LOOSE_PATTERN = /^\s*File "(?<file>.*)"/;
98
+ /**
99
+ * A frame's continuation: source text, or a 3.11+ caret marker. Indented
100
+ * strictly deeper than the `File` line's two spaces, which is what keeps a
101
+ * zero-indent trailer (`ValueError: ...`) from being read as one.
102
+ */
103
+ const CONTINUATION_PATTERN = /^\s{3,}\S/;
104
+ /**
105
+ * The closing line: `ValueError: bad input`, a dotted
106
+ * `myapp.errors.ConfigError: ...`, or a bare `SystemExit` with no message
107
+ * at all. Anchored at zero indent — every frame line in a CPython traceback
108
+ * is indented, so the indent is what separates "this closes the block" from
109
+ * "this is more of the block".
110
+ *
111
+ * ## The lookahead, and the fabrication it removes (RT-106)
112
+ *
113
+ * Without `(?=[\w.]*[a-z])` this pattern was `^[A-Za-z_][\w.]*(?::[ \t].*)?$`
114
+ * — **any** `WORD: text` line at zero indent, which is `logging`'s default
115
+ * format and uvicorn's entire access-log format. A CPython traceback is
116
+ * written one `write()` per line, so a second thread logging to the same fd
117
+ * interleaves *between* them. Measured, on real threaded CPython 3.12
118
+ * output, three consecutive runs of 25 failures each:
119
+ *
120
+ * | | run 1 | run 2 | run 3 |
121
+ * | --- | --- | --- | --- |
122
+ * | failures reported with an access-log line as their identity | 7 | 8 | 7 |
123
+ *
124
+ * And the damage is symmetric, which the first reading of this defect
125
+ * missed. An `INFO:` line landing after the `Traceback` banner closed the
126
+ * block **immediately**, so the emitted EXCEPTION was two lines — the
127
+ * banner and `INFO: 127.0.0.1:50002 - "GET /health HTTP/1.1" 200 OK`, zero
128
+ * frames. *A successful request reported as the failure.* The real frames
129
+ * and the real `TypeError:` then arrived with no header and were emitted as
130
+ * a bare `stack-trace` event, so the genuine failure produced **no
131
+ * EXCEPTION at all.** Roughly 30% of failures, both halves wrong.
132
+ *
133
+ * ## Why a lowercase letter, and what it costs
134
+ *
135
+ * A Python exception class is CapWords by PEP 8 and a log level is
136
+ * upper-case by universal convention, so "the class token contains at least
137
+ * one lowercase letter" separates them. That is a naming convention rather
138
+ * than a grammar, so the cost was **measured before it was accepted**: 199
139
+ * importable stdlib modules were walked and every `BaseException` subclass
140
+ * in them collected — **228 distinct exception class names, 0 of which
141
+ * contain no lowercase letter.** A dotted `myapp.errors.ConfigError`
142
+ * passes on its module path alone.
143
+ *
144
+ * **The disclosed recall cost:** an exception class named entirely in
145
+ * upper-case (`FOO`) no longer closes a block. Its frames still become a
146
+ * `stack-trace` event; what is lost is the exception identity, not the
147
+ * evidence. That is the deliberate direction of the trade — this defect was
148
+ * shipping *wrong* evidence, and a gap is disclosable in a way a
149
+ * confidently-wrong identity is not.
150
+ *
151
+ * **What it does not fix:** a log line whose first token happens to be
152
+ * CapWords — `Error: connection refused` from an application's own `print`
153
+ * — is still trailer-shaped and still interleaves. The structural answer to
154
+ * that is a declared `LogEnvelope`, whose `startsRecord` closes a block at
155
+ * a boundary the framework *states* rather than one this pattern guesses;
156
+ * `createUvicornLogEnvelope` is the first of those. This lookahead is what
157
+ * an **un-enveloped** stream gets, and it is a heuristic, said plainly.
158
+ */
159
+ const TRAILER_PATTERN = /^(?=[\w.]*[a-z])[A-Za-z_][\w.]*(?::[ \t].*)?$/;
160
+ /**
161
+ * Two connective lines CPython prints *between* chained tracebacks. They are
162
+ * deliberately NOT trailers: each chained traceback is its own complete
163
+ * exception block, and swallowing the connective line into the first block
164
+ * would attach the wrong text to it. They fall through as plain log lines,
165
+ * and the second `Traceback` banner opens a second block — which reports
166
+ * chained exceptions as two honest blocks plus the sentence linking them,
167
+ * rather than one block silently missing half its content.
168
+ */
169
+ export const CHAINED_EXCEPTION_CONNECTIVES = [
170
+ "During handling of the above exception, another exception occurred:",
171
+ "The above exception was the direct cause of the following exception:",
172
+ ];
173
+ export function isPythonBannerLine(line) {
174
+ return BANNER_PATTERN.test(stripExceptionGroupGutter(line));
175
+ }
176
+ export function isPythonLocationLine(line) {
177
+ return FILE_LINE_LOOSE_PATTERN.test(stripExceptionGroupGutter(line));
178
+ }
179
+ /**
180
+ * Indentation alone — **not** sufficient on its own to call a line a frame.
181
+ * See `createPythonLineClassifier`, which is what the adapter actually uses.
182
+ */
183
+ export function looksLikeContinuation(line) {
184
+ return CONTINUATION_PATTERN.test(stripExceptionGroupGutter(line));
185
+ }
186
+ export function isPythonTrailerLine(line) {
187
+ if (CHAINED_EXCEPTION_CONNECTIVES.some((c) => line.trim() === c))
188
+ return false;
189
+ return TRAILER_PATTERN.test(stripExceptionGroupGutter(line));
190
+ }
191
+ /**
192
+ * A stateful `LineClassifier`, and the statefulness is the point.
193
+ *
194
+ * The obvious stateless rule — "a frame is a `File` line **or** anything
195
+ * indented" — is wrong, and the cross-language control in this package's
196
+ * tests is what caught it. `ExceptionBlockDetector` opens a block the
197
+ * moment `isFrame` returns true from idle, so a stateless indent rule turns
198
+ * **every indented log line in the application's output** into the start of
199
+ * a spurious stack-trace block: a pretty-printed JSON payload, a wrapped
200
+ * message, an indented SQL statement. It also accepts another runtime's
201
+ * frames verbatim (` at deleteInvoice (file:///app/db.js:17:22)` is just
202
+ * an indented line), so a mis-wired adapter would half-work rather than
203
+ * fail.
204
+ *
205
+ * A continuation is therefore only recognised **after a `File` line has
206
+ * been seen** — which is the actual rule CPython's format obeys, since
207
+ * source text and caret markers only ever appear beneath a location line.
208
+ * Any line that is neither a location nor a continuation clears that state,
209
+ * so a block that ends without a trailer cannot leave the classifier primed.
210
+ *
211
+ * This depends on lines arriving in stream order, which `ExceptionBlockDetector`
212
+ * provides by construction — it is a one-line-at-a-time streaming detector
213
+ * and `isFrame` is consulted for every line in every state. A fresh
214
+ * classifier per collector keeps two processes' output from sharing state.
215
+ */
216
+ export function createPythonLineClassifier() {
217
+ let sawLocationLine = false;
218
+ return {
219
+ isHeader(line) {
220
+ if (isPythonBannerLine(line)) {
221
+ sawLocationLine = false;
222
+ return true;
223
+ }
224
+ return false;
225
+ },
226
+ isFrame(line) {
227
+ if (isPythonLocationLine(line)) {
228
+ sawLocationLine = true;
229
+ return true;
230
+ }
231
+ if (sawLocationLine && looksLikeContinuation(line))
232
+ return true;
233
+ sawLocationLine = false;
234
+ return false;
235
+ },
236
+ isTrailer(line) {
237
+ // Only closes a block that actually had frames in it — a bare
238
+ // identifier-shaped log line cannot close a block that never opened.
239
+ if (!sawLocationLine && !isPythonTrailerLine(line))
240
+ return false;
241
+ if (!isPythonTrailerLine(line))
242
+ return false;
243
+ sawLocationLine = false;
244
+ return true;
245
+ },
246
+ };
247
+ }
248
+ /** `null` for a continuation line, or a `File` line with no parseable line number — never fabricated. */
249
+ export function parsePythonFrame(line) {
250
+ const match = FILE_LINE_PATTERN.exec(stripExceptionGroupGutter(line));
251
+ if (!match?.groups)
252
+ return null;
253
+ const file = match.groups["file"];
254
+ const lineNumber = Number(match.groups["line"]);
255
+ if (file === undefined || !Number.isInteger(lineNumber))
256
+ return null;
257
+ return { functionName: match.groups["fn"] ?? null, file, line: lineNumber };
258
+ }
259
+ //# sourceMappingURL=python-frame-parsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-frame-parsing.js","sourceRoot":"","sources":["../src/python-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAUH,MAAM,cAAc,GAAG,sCAAsC,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAEzC,0FAA0F;AAC1F,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,iBAAiB,GAAG,kEAAkE,CAAC;AAC7F,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,eAAe,GAAG,+CAA+C,CAAC;AAExE;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,qEAAqE;IACrE,sEAAsE;CAC9D,CAAC;AAEX,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,cAAc,CAAC,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,OAAO,uBAAuB,CAAC,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;AACvE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,oBAAoB,CAAC,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,6BAA6B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/E,OAAO,eAAe,CAAC,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,0BAA0B;IAKxC,IAAI,eAAe,GAAG,KAAK,CAAC;IAE5B,OAAO;QACL,QAAQ,CAAC,IAAY;YACnB,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,eAAe,GAAG,KAAK,CAAC;gBACxB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,CAAC,IAAY;YAClB,IAAI,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/B,eAAe,GAAG,IAAI,CAAC;gBACvB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,IAAI,eAAe,IAAI,qBAAqB,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;YAChE,eAAe,GAAG,KAAK,CAAC;YACxB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,SAAS,CAAC,IAAY;YACpB,8DAA8D;YAC9D,qEAAqE;YACrE,IAAI,CAAC,eAAe,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC;gBAAE,OAAO,KAAK,CAAC;YACjE,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC7C,eAAe,GAAG,KAAK,CAAC;YACxB,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC;AACJ,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;IACtE,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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Python's `LogEnvelope`s. Currently one: **uvicorn** (RT-106).
3
+ *
4
+ * ## This file exists because an earlier claim of mine was too broad
5
+ *
6
+ * RT-103 measured Flask, Django and FastAPI all printing
7
+ * `Traceback (most recent call last):` at column 0 with CPython's own
8
+ * two-space frame indent, because `logging` appends `exc_info` unindented,
9
+ * and concluded **"Python needs no envelope at all and never will."**
10
+ *
11
+ * That is right about *unwrapping* and wrong about *record boundaries*, and
12
+ * the two are different jobs behind one interface. Nothing needs stripping
13
+ * from a Python traceback line. But `UnwrappedLine.startsRecord` is the
14
+ * only mechanism by which a collector can know that a line **ends the
15
+ * previous record**, and Python is the language where that matters most:
16
+ *
17
+ * ```
18
+ * Traceback (most recent call last):
19
+ * INFO: 127.0.0.1:50002 - "GET /health HTTP/1.1" 200 OK
20
+ * File "/app/service.py", line 18, in charge_invoice
21
+ * ```
22
+ *
23
+ * A traceback is written one `write()` per line, so any other thread
24
+ * logging to the same stream interleaves between them. Measured on real
25
+ * threaded CPython 3.12, three runs of 25 failures: 7, 8 and 7 of them were
26
+ * reported as an EXCEPTION whose entire identity was an access-log line for
27
+ * a **successful** request — see `TRAILER_PATTERN` in
28
+ * `python-frame-parsing.ts` for the full measurement and for the heuristic
29
+ * that an un-enveloped stream falls back on.
30
+ *
31
+ * This envelope makes that case structural rather than guessed. uvicorn
32
+ * *states* where a record begins; the trailer pattern can only infer it.
33
+ *
34
+ * ## Nothing is stripped from a traceback, and that is the whole design
35
+ *
36
+ * uvicorn formats with `"%(levelprefix)s %(message)s"`, where `levelprefix`
37
+ * is `f"{levelname}:".ljust(9)`. That prefix goes on the record's **first**
38
+ * line only. The `exc_info` traceback `logging` appends afterwards is
39
+ * written through untouched, at column 0, with CPython's own indentation.
40
+ *
41
+ * Measured against real `uvicorn 0.41.0` / `fastapi 0.141.1` output
42
+ * (`test/fixtures/uvicorn-asgi-failure.txt`): `ERROR: Exception in ASGI
43
+ * application` is the header, and all 28 traceback lines beneath it are
44
+ * continuations returned byte-identical. So `unwrap` is the identity
45
+ * function for every line that carries evidence, and the envelope's entire
46
+ * contribution is `startsRecord` — the opposite balance to Ruby's, where
47
+ * unwrapping is the load-bearing half.
48
+ *
49
+ * ## Why the level set is closed rather than `\w+`
50
+ *
51
+ * `^(\w+): +` would match `TypeError: ...` — a real trailer, wrongly
52
+ * read as a record boundary, closing a block one line before its identity
53
+ * arrives. That is the same class of over-matching this envelope exists to
54
+ * fix, introduced from the other side. So the set is exactly the six level
55
+ * names `logging` and uvicorn can produce, and a seventh would be a
56
+ * measured addition rather than a widened pattern.
57
+ *
58
+ * ## Two disclosed limits
59
+ *
60
+ * **Colour.** uvicorn wraps `levelprefix` in ANSI when its output is a TTY.
61
+ * Descry pipes, so the measured stream has none, and this pattern would not
62
+ * match a colourised one. Stated rather than handled: adding an optional
63
+ * escape-sequence prefix on an unmeasured shape is the guess this file's
64
+ * own argument rejects.
65
+ *
66
+ * **Application loggers are not covered.** uvicorn configures only its own
67
+ * `uvicorn`/`uvicorn.error`/`uvicorn.access` loggers. An application's own
68
+ * `logging.getLogger(__name__).info(...)` goes through the root config and
69
+ * usually carries no prefix at all, so it comes back from `unwrap`
70
+ * untouched with `startsRecord: false` — correct, because this envelope has
71
+ * no idea whether that line began a record, and saying so would be the
72
+ * inference `fields()` exists to refuse.
73
+ */
74
+ import type { LogEnvelope } from "@descryy/runtime-backend-observation";
75
+ export declare function createUvicornLogEnvelope(): LogEnvelope;
76
+ //# sourceMappingURL=python-log-envelopes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-log-envelopes.d.ts","sourceRoot":"","sources":["../src/python-log-envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAiC,MAAM,sCAAsC,CAAC;AAqBvG,wBAAgB,wBAAwB,IAAI,WAAW,CAyCtD"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Python's `LogEnvelope`s. Currently one: **uvicorn** (RT-106).
3
+ *
4
+ * ## This file exists because an earlier claim of mine was too broad
5
+ *
6
+ * RT-103 measured Flask, Django and FastAPI all printing
7
+ * `Traceback (most recent call last):` at column 0 with CPython's own
8
+ * two-space frame indent, because `logging` appends `exc_info` unindented,
9
+ * and concluded **"Python needs no envelope at all and never will."**
10
+ *
11
+ * That is right about *unwrapping* and wrong about *record boundaries*, and
12
+ * the two are different jobs behind one interface. Nothing needs stripping
13
+ * from a Python traceback line. But `UnwrappedLine.startsRecord` is the
14
+ * only mechanism by which a collector can know that a line **ends the
15
+ * previous record**, and Python is the language where that matters most:
16
+ *
17
+ * ```
18
+ * Traceback (most recent call last):
19
+ * INFO: 127.0.0.1:50002 - "GET /health HTTP/1.1" 200 OK
20
+ * File "/app/service.py", line 18, in charge_invoice
21
+ * ```
22
+ *
23
+ * A traceback is written one `write()` per line, so any other thread
24
+ * logging to the same stream interleaves between them. Measured on real
25
+ * threaded CPython 3.12, three runs of 25 failures: 7, 8 and 7 of them were
26
+ * reported as an EXCEPTION whose entire identity was an access-log line for
27
+ * a **successful** request — see `TRAILER_PATTERN` in
28
+ * `python-frame-parsing.ts` for the full measurement and for the heuristic
29
+ * that an un-enveloped stream falls back on.
30
+ *
31
+ * This envelope makes that case structural rather than guessed. uvicorn
32
+ * *states* where a record begins; the trailer pattern can only infer it.
33
+ *
34
+ * ## Nothing is stripped from a traceback, and that is the whole design
35
+ *
36
+ * uvicorn formats with `"%(levelprefix)s %(message)s"`, where `levelprefix`
37
+ * is `f"{levelname}:".ljust(9)`. That prefix goes on the record's **first**
38
+ * line only. The `exc_info` traceback `logging` appends afterwards is
39
+ * written through untouched, at column 0, with CPython's own indentation.
40
+ *
41
+ * Measured against real `uvicorn 0.41.0` / `fastapi 0.141.1` output
42
+ * (`test/fixtures/uvicorn-asgi-failure.txt`): `ERROR: Exception in ASGI
43
+ * application` is the header, and all 28 traceback lines beneath it are
44
+ * continuations returned byte-identical. So `unwrap` is the identity
45
+ * function for every line that carries evidence, and the envelope's entire
46
+ * contribution is `startsRecord` — the opposite balance to Ruby's, where
47
+ * unwrapping is the load-bearing half.
48
+ *
49
+ * ## Why the level set is closed rather than `\w+`
50
+ *
51
+ * `^(\w+): +` would match `TypeError: ...` — a real trailer, wrongly
52
+ * read as a record boundary, closing a block one line before its identity
53
+ * arrives. That is the same class of over-matching this envelope exists to
54
+ * fix, introduced from the other side. So the set is exactly the six level
55
+ * names `logging` and uvicorn can produce, and a seventh would be a
56
+ * measured addition rather than a widened pattern.
57
+ *
58
+ * ## Two disclosed limits
59
+ *
60
+ * **Colour.** uvicorn wraps `levelprefix` in ANSI when its output is a TTY.
61
+ * Descry pipes, so the measured stream has none, and this pattern would not
62
+ * match a colourised one. Stated rather than handled: adding an optional
63
+ * escape-sequence prefix on an unmeasured shape is the guess this file's
64
+ * own argument rejects.
65
+ *
66
+ * **Application loggers are not covered.** uvicorn configures only its own
67
+ * `uvicorn`/`uvicorn.error`/`uvicorn.access` loggers. An application's own
68
+ * `logging.getLogger(__name__).info(...)` goes through the root config and
69
+ * usually carries no prefix at all, so it comes back from `unwrap`
70
+ * untouched with `startsRecord: false` — correct, because this envelope has
71
+ * no idea whether that line began a record, and saying so would be the
72
+ * inference `fields()` exists to refuse.
73
+ */
74
+ /**
75
+ * `INFO: 127.0.0.1:39334 - "GET /health HTTP/1.1" 200 OK`
76
+ *
77
+ * uvicorn's `DefaultFormatter`/`AccessFormatter` both use
78
+ * `"%(levelprefix)s %(message)s"` with `levelprefix = f"{levelname}:".ljust(9)`,
79
+ * so the separator is between 1 and 5 spaces depending on the level's own
80
+ * length — `CRITICAL:` needs no padding, `INFO:` needs four. Anchored at
81
+ * column zero: a traceback's frame lines are indented and its trailer is
82
+ * not level-shaped, so neither can reach this.
83
+ */
84
+ const UVICORN_HEADER = /^(CRITICAL|ERROR|WARNING|INFO|DEBUG|TRACE): +/;
85
+ /**
86
+ * uvicorn's access line: `127.0.0.1:39334 - "GET /health HTTP/1.1" 200 OK`.
87
+ * Parsed only to populate `attrs` — nothing above the envelope reads it, the
88
+ * same contract `attrs` has in the IR.
89
+ */
90
+ const ACCESS_MESSAGE = /^(?<client>\S+) - "(?<method>[A-Z]+) (?<path>\S+) (?<protocol>\S+)" (?<status>\d{3})\b/;
91
+ export function createUvicornLogEnvelope() {
92
+ let fields = null;
93
+ return {
94
+ frameworkName: "uvicorn",
95
+ unwrap(line) {
96
+ const match = UVICORN_HEADER.exec(line);
97
+ // Not a header: a traceback line, an application logger's own output,
98
+ // or anything else. Returned byte-identical, and explicitly NOT
99
+ // claiming a boundary — an unrecognised line is not evidence of one.
100
+ if (match === null)
101
+ return { content: line, startsRecord: false };
102
+ const content = line.slice(match[0].length);
103
+ const access = ACCESS_MESSAGE.exec(content);
104
+ fields = {
105
+ // uvicorn's own word, never normalised to a shared vocabulary.
106
+ level: match[1] ?? null,
107
+ // uvicorn's formatter discards the logger name — `uvicorn.access`
108
+ // and `uvicorn.error` are indistinguishable in the printed stream.
109
+ // Null because it is genuinely absent, not merely unparsed.
110
+ category: null,
111
+ // uvicorn has no request-id concept and prints none. Unlike
112
+ // Kestrel's `Request id "..."`, there is nothing here to read, and
113
+ // deriving one from the client port would be an invention.
114
+ requestId: null,
115
+ attrs: access?.groups === undefined
116
+ ? {}
117
+ : {
118
+ client: access.groups["client"] ?? "",
119
+ method: access.groups["method"] ?? "",
120
+ path: access.groups["path"] ?? "",
121
+ status: access.groups["status"] ?? "",
122
+ },
123
+ };
124
+ return { content, startsRecord: true };
125
+ },
126
+ fields: () => fields,
127
+ };
128
+ }
129
+ //# sourceMappingURL=python-log-envelopes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-log-envelopes.js","sourceRoot":"","sources":["../src/python-log-envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AAIH;;;;;;;;;GASG;AACH,MAAM,cAAc,GAAG,+CAA+C,CAAC;AAEvE;;;;GAIG;AACH,MAAM,cAAc,GAAG,wFAAwF,CAAC;AAEhH,MAAM,UAAU,wBAAwB;IACtC,IAAI,MAAM,GAA0B,IAAI,CAAC;IAEzC,OAAO;QACL,aAAa,EAAE,SAAS;QAExB,MAAM,CAAC,IAAY;YACjB,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxC,sEAAsE;YACtE,gEAAgE;YAChE,qEAAqE;YACrE,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;YAElE,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC5C,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC5C,MAAM,GAAG;gBACP,+DAA+D;gBAC/D,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI;gBACvB,kEAAkE;gBAClE,mEAAmE;gBACnE,4DAA4D;gBAC5D,QAAQ,EAAE,IAAI;gBACd,4DAA4D;gBAC5D,mEAAmE;gBACnE,2DAA2D;gBAC3D,SAAS,EAAE,IAAI;gBACf,KAAK,EACH,MAAM,EAAE,MAAM,KAAK,SAAS;oBAC1B,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC;wBACE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;wBACrC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;wBACrC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE;wBACjC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;qBACtC;aACR,CAAC;YACF,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QACzC,CAAC;QAED,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM;KACrB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The concrete `RuntimeAdapter` for Python — the second language, and the
3
+ * first one to test whether the seam `@descryy/runtime-backend-observation`
4
+ * defines is genuinely language-neutral or merely V8-shaped.
5
+ *
6
+ * It was not, in two places, and both were fixed in the seam rather than
7
+ * worked around here:
8
+ *
9
+ * - `LineClassifier` had no notion of a line that ends a block and belongs
10
+ * to it, because a V8 header names its exception up front. A CPython
11
+ * traceback names it last. `isTrailer` is that concept, added
12
+ * language-neutrally.
13
+ * - `StackTrace.frames` had no documented order, because with one language
14
+ * nothing ever had to convert. Python prints the opposite of V8. The
15
+ * contract now states most-recent-first and this adapter reverses.
16
+ *
17
+ * Both are recorded because they are the general lesson of adding a second
18
+ * anything: a seam with one implementation is a seam in name only, and the
19
+ * defects it hides are silent ones — a truncated traceback and a
20
+ * backwards frame list both look like well-formed output.
21
+ */
22
+ import type { RuntimeAdapter } from "@descryy/runtime-backend-observation";
23
+ export interface PythonRuntimeAdapterOptions {
24
+ /** Attributed on every emitted Evidence's `service` field (plan §16.7). Omit when not applicable. */
25
+ readonly service?: string;
26
+ /**
27
+ * The server whose logging layer wraps this process's output (§16).
28
+ *
29
+ * **Declared, never sniffed** — the same rule ASP.NET's option follows,
30
+ * and for a sharper reason here. uvicorn's format is `LEVEL:` plus
31
+ * padding, which is `logging`'s own default shape; detecting it from a
32
+ * line that looks like one would enable record-boundary flushing for any
33
+ * Python process that ever printed `INFO: ...`, and the cost of a wrong
34
+ * boundary is a truncated exception block. A caller that knows it
35
+ * launched uvicorn says so.
36
+ *
37
+ * Only uvicorn, and only because it was measured (RT-106). Gunicorn's
38
+ * default `[%(asctime)s] [%(process)d] [%(levelname)s]` and Django's
39
+ * `runserver` output are different formats and get no envelope until one
40
+ * of them is run and read.
41
+ */
42
+ readonly server?: "uvicorn";
43
+ }
44
+ export declare function createPythonRuntimeAdapter(options?: PythonRuntimeAdapterOptions): RuntimeAdapter;
45
+ //# sourceMappingURL=python-runtime-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-runtime-adapter.d.ts","sourceRoot":"","sources":["../src/python-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,sCAAsC,CAAC;AAShG,MAAM,WAAW,2BAA2B;IAC1C,qGAAqG;IACrG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;CAC7B;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,2BAAgC,GAAG,cAAc,CA4BpG"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The concrete `RuntimeAdapter` for Python — the second language, and the
3
+ * first one to test whether the seam `@descryy/runtime-backend-observation`
4
+ * defines is genuinely language-neutral or merely V8-shaped.
5
+ *
6
+ * It was not, in two places, and both were fixed in the seam rather than
7
+ * worked around here:
8
+ *
9
+ * - `LineClassifier` had no notion of a line that ends a block and belongs
10
+ * to it, because a V8 header names its exception up front. A CPython
11
+ * traceback names it last. `isTrailer` is that concept, added
12
+ * language-neutrally.
13
+ * - `StackTrace.frames` had no documented order, because with one language
14
+ * nothing ever had to convert. Python prints the opposite of V8. The
15
+ * contract now states most-recent-first and this adapter reverses.
16
+ *
17
+ * Both are recorded because they are the general lesson of adding a second
18
+ * anything: a seam with one implementation is a seam in name only, and the
19
+ * defects it hides are silent ones — a truncated traceback and a
20
+ * backwards frame list both look like well-formed output.
21
+ */
22
+ import { createLogCollector } from "@descryy/runtime-backend-observation";
23
+ import { createPythonLineClassifier } from "./python-frame-parsing.js";
24
+ import { createUvicornLogEnvelope } from "./python-log-envelopes.js";
25
+ import { createPythonSourceLocationResolver } from "./python-source-location-resolver.js";
26
+ import { createPythonStackTraceParser } from "./python-stack-trace-parser.js";
27
+ export function createPythonRuntimeAdapter(options = {}) {
28
+ const sourceLocationResolver = createPythonSourceLocationResolver();
29
+ const stackTraceParser = createPythonStackTraceParser(sourceLocationResolver);
30
+ function createBackendCollectors(sources) {
31
+ return sources.map((source) => createLogCollector({
32
+ source,
33
+ // One classifier per collector, never shared: it carries per-stream
34
+ // state, and two processes' output must not prime each other.
35
+ classifier: createPythonLineClassifier(),
36
+ stackTraceParser,
37
+ // One envelope per collector, never shared: it holds the current
38
+ // record's fields, and two processes' output must not attribute
39
+ // each other's. Its whole contribution here is `startsRecord` —
40
+ // nothing is stripped from a Python traceback (RT-106).
41
+ ...(options.server === "uvicorn" ? { envelope: createUvicornLogEnvelope() } : {}),
42
+ ...(options.service !== undefined ? { service: options.service } : {}),
43
+ }));
44
+ }
45
+ return {
46
+ language: "python",
47
+ stackTraceParser,
48
+ sourceLocationResolver,
49
+ createBackendCollectors,
50
+ };
51
+ }
52
+ //# sourceMappingURL=python-runtime-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-runtime-adapter.js","sourceRoot":"","sources":["../src/python-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAE1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,sCAAsC,CAAC;AAC1F,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAwB9E,MAAM,UAAU,0BAA0B,CAAC,UAAuC,EAAE;IAClF,MAAM,sBAAsB,GAAG,kCAAkC,EAAE,CAAC;IACpE,MAAM,gBAAgB,GAAG,4BAA4B,CAAC,sBAAsB,CAAC,CAAC;IAE9E,SAAS,uBAAuB,CAAC,OAAuC;QACtE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC5B,kBAAkB,CAAC;YACjB,MAAM;YACN,oEAAoE;YACpE,8DAA8D;YAC9D,UAAU,EAAE,0BAA0B,EAAE;YACxC,gBAAgB;YAChB,iEAAiE;YACjE,gEAAgE;YAChE,gEAAgE;YAChE,wDAAwD;YACxD,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,wBAAwB,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjF,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,QAAQ;QAClB,gBAAgB;QAChB,sBAAsB;QACtB,uBAAuB;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Python `SourceLocationResolver`.
3
+ *
4
+ * **Every location is `self-contained`, and that is a positive claim rather
5
+ * than a fallback.** CPython captures the path and line of the `.py` file it
6
+ * is actually executing; there is no compilation step, no separate map, and
7
+ * therefore nothing that can drift or go missing. The frame IS the answer.
8
+ * Contrast the Node resolver, which must decide between four values because
9
+ * a source map is a second file that can be absent, stale, or wrong.
10
+ *
11
+ * This is the first resolver in the codebase for which the reliability
12
+ * decision is genuinely trivial, and the RT-003 enum already anticipates it:
13
+ * `self-contained` means "nothing was lost", not "we didn't look".
14
+ *
15
+ * **What this deliberately does not do:** check that the file exists on
16
+ * disk, or that its current contents still match what ran. Neither would
17
+ * change the reliability value — `self-contained` is a statement about the
18
+ * *mechanism*, not about whether the working tree has since been edited —
19
+ * and a stat call per frame would buy nothing but latency. CPython's
20
+ * pseudo-files (`<string>`, `<stdin>`, `<frozen importlib._bootstrap>`) are
21
+ * passed through verbatim for the same reason: they are the honest location,
22
+ * and rewriting them to null would discard real information.
23
+ *
24
+ * `column` is always null. CPython does not print columns; 3.11+ prints a
25
+ * caret line under the failing expression, from which a column *could* be
26
+ * inferred by counting characters. That inference is not made here — it
27
+ * would be a derived guess presented in a field callers read as captured
28
+ * fact, and the caret line is preserved verbatim in `StackFrame.raw`
29
+ * regardless.
30
+ */
31
+ import type { SourceLocationResolver } from "@descryy/runtime-backend-observation";
32
+ export declare function createPythonSourceLocationResolver(): SourceLocationResolver;
33
+ //# sourceMappingURL=python-source-location-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-source-location-resolver.d.ts","sourceRoot":"","sources":["../src/python-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAErG,wBAAgB,kCAAkC,IAAI,sBAAsB,CAiB3E"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Python `SourceLocationResolver`.
3
+ *
4
+ * **Every location is `self-contained`, and that is a positive claim rather
5
+ * than a fallback.** CPython captures the path and line of the `.py` file it
6
+ * is actually executing; there is no compilation step, no separate map, and
7
+ * therefore nothing that can drift or go missing. The frame IS the answer.
8
+ * Contrast the Node resolver, which must decide between four values because
9
+ * a source map is a second file that can be absent, stale, or wrong.
10
+ *
11
+ * This is the first resolver in the codebase for which the reliability
12
+ * decision is genuinely trivial, and the RT-003 enum already anticipates it:
13
+ * `self-contained` means "nothing was lost", not "we didn't look".
14
+ *
15
+ * **What this deliberately does not do:** check that the file exists on
16
+ * disk, or that its current contents still match what ran. Neither would
17
+ * change the reliability value — `self-contained` is a statement about the
18
+ * *mechanism*, not about whether the working tree has since been edited —
19
+ * and a stat call per frame would buy nothing but latency. CPython's
20
+ * pseudo-files (`<string>`, `<stdin>`, `<frozen importlib._bootstrap>`) are
21
+ * passed through verbatim for the same reason: they are the honest location,
22
+ * and rewriting them to null would discard real information.
23
+ *
24
+ * `column` is always null. CPython does not print columns; 3.11+ prints a
25
+ * caret line under the failing expression, from which a column *could* be
26
+ * inferred by counting characters. That inference is not made here — it
27
+ * would be a derived guess presented in a field callers read as captured
28
+ * fact, and the caret line is preserved verbatim in `StackFrame.raw`
29
+ * regardless.
30
+ */
31
+ export function createPythonSourceLocationResolver() {
32
+ return {
33
+ // The interface is async because other languages must do I/O to answer
34
+ // (Node reads a source map, a JVM reader would read a LineNumberTable).
35
+ // Python genuinely does not, so this body never awaits — returning a
36
+ // resolved promise rather than inventing work to look symmetrical.
37
+ resolve(frame) {
38
+ return Promise.resolve({
39
+ file: frame.file,
40
+ line: frame.line,
41
+ column: frame.column,
42
+ functionName: frame.functionName,
43
+ reliability: "self-contained",
44
+ resolvedVia: null,
45
+ });
46
+ },
47
+ };
48
+ }
49
+ //# sourceMappingURL=python-source-location-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-source-location-resolver.js","sourceRoot":"","sources":["../src/python-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,MAAM,UAAU,kCAAkC;IAChD,OAAO;QACL,uEAAuE;QACvE,wEAAwE;QACxE,qEAAqE;QACrE,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,gBAAgB;gBAC7B,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Composes the pure CPython line-recognition functions
3
+ * (`python-frame-parsing.ts`) with a `SourceLocationResolver` to implement
4
+ * the full async `StackTraceParser` contract.
5
+ *
6
+ * **The reversal is the load-bearing line in this file.** CPython prints
7
+ * oldest call first — the banner says so out loud, "most recent call last" —
8
+ * and `StackTrace.frames` is contractually most-recent-first. Without the
9
+ * reversal every consumer that reads `frames[0]` as the failing frame would
10
+ * instead get the process entry point: a wrong answer that looks entirely
11
+ * well-formed, with the right frames, the right count, and no error
12
+ * anywhere. Root-cause traversal would point at `main` for every Python
13
+ * failure ever captured.
14
+ *
15
+ * `fidelity` is always reported `"synchronous"`. A CPython traceback is
16
+ * built by walking the actual frame objects of the executing thread, so
17
+ * every frame shown is a genuine, unbroken link. What such a traceback
18
+ * cannot show is the logical caller across an `await` boundary or a thread
19
+ * hand-off, where the chain truthfully ends — the same hard limit V8 stacks
20
+ * have, and the same reason this is not reported as
21
+ * `"concurrency-fragmented"`. Detecting a genuinely lost logical caller
22
+ * would need interpreter-level task correlation, not text parsing; claiming
23
+ * fragmentation without that evidence would be a guess.
24
+ */
25
+ import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
26
+ export declare function createPythonStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
27
+ //# sourceMappingURL=python-stack-trace-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/python-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAIrG,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAmD/F"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Composes the pure CPython line-recognition functions
3
+ * (`python-frame-parsing.ts`) with a `SourceLocationResolver` to implement
4
+ * the full async `StackTraceParser` contract.
5
+ *
6
+ * **The reversal is the load-bearing line in this file.** CPython prints
7
+ * oldest call first — the banner says so out loud, "most recent call last" —
8
+ * and `StackTrace.frames` is contractually most-recent-first. Without the
9
+ * reversal every consumer that reads `frames[0]` as the failing frame would
10
+ * instead get the process entry point: a wrong answer that looks entirely
11
+ * well-formed, with the right frames, the right count, and no error
12
+ * anywhere. Root-cause traversal would point at `main` for every Python
13
+ * failure ever captured.
14
+ *
15
+ * `fidelity` is always reported `"synchronous"`. A CPython traceback is
16
+ * built by walking the actual frame objects of the executing thread, so
17
+ * every frame shown is a genuine, unbroken link. What such a traceback
18
+ * cannot show is the logical caller across an `await` boundary or a thread
19
+ * hand-off, where the chain truthfully ends — the same hard limit V8 stacks
20
+ * have, and the same reason this is not reported as
21
+ * `"concurrency-fragmented"`. Detecting a genuinely lost logical caller
22
+ * would need interpreter-level task correlation, not text parsing; claiming
23
+ * fragmentation without that evidence would be a guess.
24
+ */
25
+ import { parsePythonFrame } from "./python-frame-parsing.js";
26
+ export function createPythonStackTraceParser(resolver) {
27
+ return {
28
+ async parse(rawText) {
29
+ const lines = rawText.split(/\r\n|\r|\n/);
30
+ const frames = [];
31
+ for (const line of lines) {
32
+ const raw = parsePythonFrame(line);
33
+ // Continuation lines (source text, 3.11+ caret markers) and the
34
+ // banner and trailer parse to null here. They are real parts of the
35
+ // block and are preserved in the raw text the collector keeps; they
36
+ // are simply not frames and none is fabricated into one.
37
+ if (raw === null)
38
+ continue;
39
+ const location = await resolver.resolve({
40
+ file: raw.file,
41
+ line: raw.line,
42
+ column: null,
43
+ functionName: raw.functionName,
44
+ });
45
+ frames.push({ location, raw: line });
46
+ }
47
+ if (frames.length === 0) {
48
+ // Nothing frame-shaped at all — not recognisable as a CPython
49
+ // traceback, per this interface's contract: null, never an
50
+ // empty-but-structured StackTrace standing in for "couldn't parse".
51
+ return null;
52
+ }
53
+ // CPython prints oldest-first; the contract is most-recent-first.
54
+ //
55
+ // `primaryFrameIndex` is required so that every adapter states an
56
+ // answer rather than inheriting one (RT-083). Python's is 0 **after
57
+ // the reversal on this line** — the last frame CPython prints is the
58
+ // one that raised, and reversing puts it first.
59
+ //
60
+ // **Corrected from the value's first draft**, which justified index 0
61
+ // with "CPython prints no frames of its own above the application's".
62
+ // That is measurably false: `json.loads("{not json}")` called from a
63
+ // two-deep application chain prints
64
+ // `File "/usr/lib/python3.12/json/decoder.py", line 353, in raw_decode`
65
+ // as the LAST frame, i.e. the innermost — three stdlib frames sit
66
+ // above the application's own. Index 0 is still correct, but for the
67
+ // other reason: it marks the failure site, not the nearest owned
68
+ // code, and the contract says outright that a path-shaped stdlib
69
+ // filter "would break on any application that legitimately fails
70
+ // inside a library frame."
71
+ return { fidelity: "synchronous", frames: frames.reverse(), primaryFrameIndex: 0 };
72
+ },
73
+ };
74
+ }
75
+ //# sourceMappingURL=python-stack-trace-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-stack-trace-parser.js","sourceRoot":"","sources":["../src/python-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE7D,MAAM,UAAU,4BAA4B,CAAC,QAAgC;IAC3E,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,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;gBACnC,gEAAgE;gBAChE,oEAAoE;gBACpE,oEAAoE;gBACpE,yDAAyD;gBACzD,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,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,8DAA8D;gBAC9D,2DAA2D;gBAC3D,oEAAoE;gBACpE,OAAO,IAAI,CAAC;YACd,CAAC;YAED,kEAAkE;YAClE,EAAE;YACF,kEAAkE;YAClE,oEAAoE;YACpE,qEAAqE;YACrE,gDAAgD;YAChD,EAAE;YACF,sEAAsE;YACtE,sEAAsE;YACtE,qEAAqE;YACrE,oCAAoC;YACpC,wEAAwE;YACxE,kEAAkE;YAClE,qEAAqE;YACrE,iEAAiE;YACjE,iEAAiE;YACjE,iEAAiE;YACjE,2BAA2B;YAC3B,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACrF,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@descryy/runtime-adapter-python",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "Python runtime adapter: CPython traceback parsing, self-contained source locations, the concrete RuntimeAdapter for Python.",
6
+ "license": "UNLICENSED",
7
+ "engines": {
8
+ "node": ">=22.5"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "registry": "https://registry.npmjs.org",
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc -b"
25
+ },
26
+ "dependencies": {
27
+ "@descryy/runtime-contracts": "*",
28
+ "@descryy/runtime-backend-observation": "*"
29
+ }
30
+ }