@descryy/runtime-adapter-php 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,9 @@
1
+ export { createLaravelLineClassifier, decodeLaravelCallee, isLaravelHeaderLine, parseLaravelHeader, } from "./laravel-frame-parsing.ts";
2
+ export { createLaravelStackTraceParser } from "./laravel-stack-trace-parser.ts";
3
+ export type { RawPhpFrame } from "./php-frame-parsing.ts";
4
+ export { createPhpLineClassifier, isPhpHeaderLine, isPhpFrameLine, isPhpTrailerLine, parsePhpHeaderFrame, parsePhpFrame, } from "./php-frame-parsing.ts";
5
+ export { createPhpSourceLocationResolver } from "./php-source-location-resolver.ts";
6
+ export { createPhpStackTraceParser } from "./php-stack-trace-parser.ts";
7
+ export type { PhpRuntimeAdapterOptions } from "./php-runtime-adapter.ts";
8
+ export { createPhpRuntimeAdapter } from "./php-runtime-adapter.ts";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { createLaravelLineClassifier, decodeLaravelCallee, isLaravelHeaderLine, parseLaravelHeader, } from "./laravel-frame-parsing.js";
2
+ export { createLaravelStackTraceParser } from "./laravel-stack-trace-parser.js";
3
+ export { createPhpLineClassifier, isPhpHeaderLine, isPhpFrameLine, isPhpTrailerLine, parsePhpHeaderFrame, parsePhpFrame, } from "./php-frame-parsing.js";
4
+ export { createPhpSourceLocationResolver } from "./php-source-location-resolver.js";
5
+ export { createPhpStackTraceParser } from "./php-stack-trace-parser.js";
6
+ export { createPhpRuntimeAdapter } from "./php-runtime-adapter.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAEhF,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAExE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Laravel's exception-log grammar, which is **not PHP's** (RT-112).
3
+ *
4
+ * ## The measurement
5
+ *
6
+ * A real Laravel 12.67.0 application on PHP 8.4.4, spawned through the
7
+ * managed lifecycle, failing through its own request pipeline with
8
+ * `LOG_CHANNEL=stderr`:
9
+ *
10
+ * ```
11
+ * 64 lines in -> 0 exceptions, 53 lines in a bare STACK_TRACE, 11 logs
12
+ * ```
13
+ *
14
+ * The **mirror image of Rails' defect** (RT-111), which lost everything.
15
+ * Laravel loses only the identity — and that is arguably worse to receive,
16
+ * because the result looks like working evidence: fifty correct frames with
17
+ * no exception class and no message attached to them.
18
+ *
19
+ * ## Why PHP's classifier half-sees it
20
+ *
21
+ * Laravel logs through Monolog, not through PHP's own
22
+ * `Throwable::__toString()`:
23
+ *
24
+ * ```
25
+ * [2026-08-19 03:45:19] production.ERROR: Trying to access array offset on null {"exception":"[object] (ErrorException(code: 0): Trying to access array offset on null at /app/routes/web.php:9)
26
+ * [stacktrace]
27
+ * #0 /app/vendor/.../HandleExceptions.php(262): Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, '...')
28
+ * #1 /app/routes/web.php(9): ...
29
+ * #11 {main}
30
+ * "}
31
+ * ```
32
+ *
33
+ * Three differences, of which two are fatal and one is silent:
34
+ *
35
+ * 1. **The header says `at file:line)` where PHP says `in file:line`.**
36
+ * PHP's `HEADER_PATTERN` is `$`-anchored on ` in <file>:<line>`, so
37
+ * Laravel's — with the word `at` and a trailing `)` — matches nothing.
38
+ * 2. **The bridge is `[stacktrace]`, not `Stack trace:`.**
39
+ * 3. **Class names are JSON-escaped.** The trace is written inside a JSON
40
+ * string, so `Illuminate\Routing\Router` arrives as
41
+ * `Illuminate\\Routing\\Router`. The frames still match PHP's
42
+ * `FRAME_PATTERN` — which is why the evidence is not lost — and every
43
+ * class name in them is wrong by a backslash, which resolves to no graph
44
+ * node while looking entirely correct. RT-103's file-path corruption,
45
+ * one field over.
46
+ *
47
+ * ## Decoding, not rewriting
48
+ *
49
+ * `\\` is recovered to `\` because the text **is** JSON-escaped and decoding
50
+ * it recovers what the process meant. That is the opposite of RT-105's
51
+ * decision to leave an `eval` frame's synthetic path alone: there, no
52
+ * decoding exists and any "fix" would invent a location; here, the encoding
53
+ * is documented and reversible.
54
+ *
55
+ * Only the callee is decoded. POSIX file paths carry no backslashes, so
56
+ * decoding them would be a no-op with a Windows-shaped hazard attached.
57
+ *
58
+ * ## Laravel's default is worse than any of this, and no parser can fix it
59
+ *
60
+ * `LOG_CHANNEL` defaults to `stack` -> `single`, which writes
61
+ * `storage/logs/laravel.log`. **A collector watching process output sees
62
+ * nothing at all** — not a parse failure, no evidence. Setting
63
+ * `LOG_CHANNEL=stderr` is a first-class Laravel option and it is a
64
+ * *configuration requirement this adapter cannot satisfy on the
65
+ * application's behalf*. It belongs in the disclosure to the user, not in a
66
+ * pattern.
67
+ */
68
+ /**
69
+ * Recovers a JSON-escaped backslash.
70
+ *
71
+ * `Illuminate\\Routing\\Router` -> `Illuminate\Routing\Router`. Applied only
72
+ * to a callee: this is a decode of a known encoding, not a guess, and the
73
+ * file paths in the same line are not escaped.
74
+ */
75
+ export declare function decodeLaravelCallee(callee: string): string;
76
+ export declare function isLaravelHeaderLine(line: string): boolean;
77
+ /** `null` when the line is not Laravel's `(Class(code: N): message at file:line)` shape — never fabricated. */
78
+ export declare function parseLaravelHeader(line: string): {
79
+ readonly exceptionClass: string;
80
+ readonly message: string;
81
+ readonly file: string;
82
+ readonly line: number;
83
+ } | null;
84
+ /**
85
+ * Laravel's classifier: its own header and bridge, PHP's own frames and
86
+ * trailer.
87
+ *
88
+ * The frame and trailer halves are imported rather than restated, because
89
+ * the `#N file(line): callee(args)` shape genuinely is PHP's — Monolog
90
+ * writes `Throwable::getTraceAsString()` verbatim. Only the framing around
91
+ * it is Laravel's. Restating them would be a second copy to keep in step
92
+ * with RT-105's closure-name fix.
93
+ *
94
+ * `isTrailer` is PHP's `#N {main}`, which closes the block and leaves
95
+ * Monolog's own `"}` terminator to fall through as a log line — the same
96
+ * choice PHP's grammar already makes for its `thrown in ...` trailer, and
97
+ * for the same reason: attach what identifies the exception, not everything
98
+ * printed after it.
99
+ *
100
+ * **`isBridge` is mutation-inert, and that is recorded rather than hidden.**
101
+ * Replacing `[stacktrace]` with PHP's `Stack trace:` kills no test: RT-082's
102
+ * held-message absorption picks the line up instead, and the block still
103
+ * forms. It is kept because the line genuinely *is* a bridge and absorbing
104
+ * it spends one of `MAX_HELD_LINES` on transport punctuation — but no test
105
+ * distinguishes the two, so "every guard is mutation-tested" would be the
106
+ * wrong summary of this file.
107
+ */
108
+ export declare function createLaravelLineClassifier(): {
109
+ isHeader(line: string): boolean;
110
+ isFrame(line: string): boolean;
111
+ isBridge(line: string): boolean;
112
+ isTrailer(line: string): boolean;
113
+ };
114
+ //# sourceMappingURL=laravel-frame-parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"laravel-frame-parsing.d.ts","sourceRoot":"","sources":["../src/laravel-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAqBH;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,+GAA+G;AAC/G,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,GACX;IAAE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAapH;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,2BAA2B,IAAI;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC,CAOA"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Laravel's exception-log grammar, which is **not PHP's** (RT-112).
3
+ *
4
+ * ## The measurement
5
+ *
6
+ * A real Laravel 12.67.0 application on PHP 8.4.4, spawned through the
7
+ * managed lifecycle, failing through its own request pipeline with
8
+ * `LOG_CHANNEL=stderr`:
9
+ *
10
+ * ```
11
+ * 64 lines in -> 0 exceptions, 53 lines in a bare STACK_TRACE, 11 logs
12
+ * ```
13
+ *
14
+ * The **mirror image of Rails' defect** (RT-111), which lost everything.
15
+ * Laravel loses only the identity — and that is arguably worse to receive,
16
+ * because the result looks like working evidence: fifty correct frames with
17
+ * no exception class and no message attached to them.
18
+ *
19
+ * ## Why PHP's classifier half-sees it
20
+ *
21
+ * Laravel logs through Monolog, not through PHP's own
22
+ * `Throwable::__toString()`:
23
+ *
24
+ * ```
25
+ * [2026-08-19 03:45:19] production.ERROR: Trying to access array offset on null {"exception":"[object] (ErrorException(code: 0): Trying to access array offset on null at /app/routes/web.php:9)
26
+ * [stacktrace]
27
+ * #0 /app/vendor/.../HandleExceptions.php(262): Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, '...')
28
+ * #1 /app/routes/web.php(9): ...
29
+ * #11 {main}
30
+ * "}
31
+ * ```
32
+ *
33
+ * Three differences, of which two are fatal and one is silent:
34
+ *
35
+ * 1. **The header says `at file:line)` where PHP says `in file:line`.**
36
+ * PHP's `HEADER_PATTERN` is `$`-anchored on ` in <file>:<line>`, so
37
+ * Laravel's — with the word `at` and a trailing `)` — matches nothing.
38
+ * 2. **The bridge is `[stacktrace]`, not `Stack trace:`.**
39
+ * 3. **Class names are JSON-escaped.** The trace is written inside a JSON
40
+ * string, so `Illuminate\Routing\Router` arrives as
41
+ * `Illuminate\\Routing\\Router`. The frames still match PHP's
42
+ * `FRAME_PATTERN` — which is why the evidence is not lost — and every
43
+ * class name in them is wrong by a backslash, which resolves to no graph
44
+ * node while looking entirely correct. RT-103's file-path corruption,
45
+ * one field over.
46
+ *
47
+ * ## Decoding, not rewriting
48
+ *
49
+ * `\\` is recovered to `\` because the text **is** JSON-escaped and decoding
50
+ * it recovers what the process meant. That is the opposite of RT-105's
51
+ * decision to leave an `eval` frame's synthetic path alone: there, no
52
+ * decoding exists and any "fix" would invent a location; here, the encoding
53
+ * is documented and reversible.
54
+ *
55
+ * Only the callee is decoded. POSIX file paths carry no backslashes, so
56
+ * decoding them would be a no-op with a Windows-shaped hazard attached.
57
+ *
58
+ * ## Laravel's default is worse than any of this, and no parser can fix it
59
+ *
60
+ * `LOG_CHANNEL` defaults to `stack` -> `single`, which writes
61
+ * `storage/logs/laravel.log`. **A collector watching process output sees
62
+ * nothing at all** — not a parse failure, no evidence. Setting
63
+ * `LOG_CHANNEL=stderr` is a first-class Laravel option and it is a
64
+ * *configuration requirement this adapter cannot satisfy on the
65
+ * application's behalf*. It belongs in the disclosure to the user, not in a
66
+ * pattern.
67
+ */
68
+ import { isPhpFrameLine, isPhpTrailerLine } from "./php-frame-parsing.js";
69
+ /**
70
+ * `(ErrorException(code: 0): Trying to access array offset on null at /app/routes/web.php:9)`
71
+ *
72
+ * Composed by `Illuminate\Log\Logger`'s exception context. The class group
73
+ * accepts a namespaced name (`Illuminate\Database\QueryException`) in either
74
+ * escaping, since the same shape appears outside a JSON string in some
75
+ * channels.
76
+ *
77
+ * Not anchored at the start: the Monolog line prefix (`[timestamp]
78
+ * env.LEVEL: message `) always precedes it, and matching from anywhere is
79
+ * how PHP's own header pattern already handles its SAPI prefix.
80
+ */
81
+ const LARAVEL_HEADER = /\((?<class>[A-Za-z_\x7f-\xff][\w\\]*)\(code: -?\d+\): (?<message>.*?) at (?<file>[^\s]+):(?<line>\d+)\)/;
82
+ const LARAVEL_BRIDGE = "[stacktrace]";
83
+ /**
84
+ * Recovers a JSON-escaped backslash.
85
+ *
86
+ * `Illuminate\\Routing\\Router` -> `Illuminate\Routing\Router`. Applied only
87
+ * to a callee: this is a decode of a known encoding, not a guess, and the
88
+ * file paths in the same line are not escaped.
89
+ */
90
+ export function decodeLaravelCallee(callee) {
91
+ return callee.replace(/\\\\/g, "\\");
92
+ }
93
+ export function isLaravelHeaderLine(line) {
94
+ return LARAVEL_HEADER.test(line);
95
+ }
96
+ /** `null` when the line is not Laravel's `(Class(code: N): message at file:line)` shape — never fabricated. */
97
+ export function parseLaravelHeader(line) {
98
+ const match = LARAVEL_HEADER.exec(line);
99
+ if (!match?.groups)
100
+ return null;
101
+ const file = match.groups["file"];
102
+ const lineNumber = Number(match.groups["line"]);
103
+ const exceptionClass = match.groups["class"];
104
+ if (file === undefined || exceptionClass === undefined || !Number.isInteger(lineNumber))
105
+ return null;
106
+ return {
107
+ exceptionClass: decodeLaravelCallee(exceptionClass),
108
+ message: match.groups["message"] ?? "",
109
+ file,
110
+ line: lineNumber,
111
+ };
112
+ }
113
+ /**
114
+ * Laravel's classifier: its own header and bridge, PHP's own frames and
115
+ * trailer.
116
+ *
117
+ * The frame and trailer halves are imported rather than restated, because
118
+ * the `#N file(line): callee(args)` shape genuinely is PHP's — Monolog
119
+ * writes `Throwable::getTraceAsString()` verbatim. Only the framing around
120
+ * it is Laravel's. Restating them would be a second copy to keep in step
121
+ * with RT-105's closure-name fix.
122
+ *
123
+ * `isTrailer` is PHP's `#N {main}`, which closes the block and leaves
124
+ * Monolog's own `"}` terminator to fall through as a log line — the same
125
+ * choice PHP's grammar already makes for its `thrown in ...` trailer, and
126
+ * for the same reason: attach what identifies the exception, not everything
127
+ * printed after it.
128
+ *
129
+ * **`isBridge` is mutation-inert, and that is recorded rather than hidden.**
130
+ * Replacing `[stacktrace]` with PHP's `Stack trace:` kills no test: RT-082's
131
+ * held-message absorption picks the line up instead, and the block still
132
+ * forms. It is kept because the line genuinely *is* a bridge and absorbing
133
+ * it spends one of `MAX_HELD_LINES` on transport punctuation — but no test
134
+ * distinguishes the two, so "every guard is mutation-tested" would be the
135
+ * wrong summary of this file.
136
+ */
137
+ export function createLaravelLineClassifier() {
138
+ return {
139
+ isHeader: isLaravelHeaderLine,
140
+ isFrame: isPhpFrameLine,
141
+ isBridge: (line) => line.trim() === LARAVEL_BRIDGE,
142
+ isTrailer: isPhpTrailerLine,
143
+ };
144
+ }
145
+ //# sourceMappingURL=laravel-frame-parsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"laravel-frame-parsing.js","sourceRoot":"","sources":["../src/laravel-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1E;;;;;;;;;;;GAWG;AACH,MAAM,cAAc,GAClB,yGAAyG,CAAC;AAE5G,MAAM,cAAc,GAAG,cAAc,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,kBAAkB,CAChC,IAAY;IAEZ,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,IAAI,KAAK,SAAS,IAAI,cAAc,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrG,OAAO;QACL,cAAc,EAAE,mBAAmB,CAAC,cAAc,CAAC;QACnD,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE;QACtC,IAAI;QACJ,IAAI,EAAE,UAAU;KACjB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,2BAA2B;IAMzC,OAAO;QACL,QAAQ,EAAE,mBAAmB;QAC7B,OAAO,EAAE,cAAc;QACvB,QAAQ,EAAE,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,cAAc;QACnE,SAAS,EAAE,gBAAgB;KAC5B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Composes Laravel's header with PHP's own frames (RT-112).
3
+ *
4
+ * ## Why this is a second parser and not a branch in the PHP one
5
+ *
6
+ * Same reason RT-111 gave for Rails: the two grammars disagree about the
7
+ * shape of the thing `primaryFrameIndex` indexes. They happen to agree on
8
+ * the *answer* — Laravel's header carries `at file:line`, the exception's
9
+ * construction site, exactly as PHP's `in file:line` does, so index 0 is the
10
+ * header-derived frame in both — and agreeing by coincidence is precisely
11
+ * what RT-083 was about. Two parsers each state one answer.
12
+ *
13
+ * The frames themselves are PHP's, parsed by PHP's parser, because Monolog
14
+ * writes `Throwable::getTraceAsString()` verbatim. What differs is the
15
+ * framing and the JSON escaping of class names, and both are handled here.
16
+ */
17
+ import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
18
+ export declare function createLaravelStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
19
+ //# sourceMappingURL=laravel-stack-trace-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"laravel-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/laravel-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAKrG,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAqChG"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Composes Laravel's header with PHP's own frames (RT-112).
3
+ *
4
+ * ## Why this is a second parser and not a branch in the PHP one
5
+ *
6
+ * Same reason RT-111 gave for Rails: the two grammars disagree about the
7
+ * shape of the thing `primaryFrameIndex` indexes. They happen to agree on
8
+ * the *answer* — Laravel's header carries `at file:line`, the exception's
9
+ * construction site, exactly as PHP's `in file:line` does, so index 0 is the
10
+ * header-derived frame in both — and agreeing by coincidence is precisely
11
+ * what RT-083 was about. Two parsers each state one answer.
12
+ *
13
+ * The frames themselves are PHP's, parsed by PHP's parser, because Monolog
14
+ * writes `Throwable::getTraceAsString()` verbatim. What differs is the
15
+ * framing and the JSON escaping of class names, and both are handled here.
16
+ */
17
+ import { decodeLaravelCallee, parseLaravelHeader } from "./laravel-frame-parsing.js";
18
+ import { parsePhpFrame } from "./php-frame-parsing.js";
19
+ export function createLaravelStackTraceParser(resolver) {
20
+ return {
21
+ async parse(rawText) {
22
+ const lines = rawText.split(/\r\n|\r|\n/);
23
+ const frames = [];
24
+ for (const line of lines) {
25
+ const header = parseLaravelHeader(line);
26
+ if (header !== null) {
27
+ // The construction site. `functionName` is null because Laravel,
28
+ // like PHP itself, names no function for it — not because it could
29
+ // not be parsed.
30
+ const location = await resolver.resolve({ file: header.file, line: header.line, column: null, functionName: null });
31
+ frames.push({ location, raw: line });
32
+ continue;
33
+ }
34
+ const raw = parsePhpFrame(line);
35
+ if (raw === null)
36
+ continue;
37
+ const location = await resolver.resolve({
38
+ file: raw.file,
39
+ line: raw.line,
40
+ column: null,
41
+ // Decoded, not rewritten: the trace lives inside a JSON string, so
42
+ // `Illuminate\\Routing\\Router` IS `Illuminate\Routing\Router` and
43
+ // recovering it is reading rather than guessing. Left encoded it
44
+ // resolves to no graph node while looking correct.
45
+ functionName: raw.functionName === null ? null : decodeLaravelCallee(raw.functionName),
46
+ });
47
+ frames.push({ location, raw: line });
48
+ }
49
+ if (frames.length === 0)
50
+ return null;
51
+ return { fidelity: "synchronous", frames, primaryFrameIndex: 0 };
52
+ },
53
+ };
54
+ }
55
+ //# sourceMappingURL=laravel-stack-trace-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"laravel-stack-trace-parser.js","sourceRoot":"","sources":["../src/laravel-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,MAAM,UAAU,6BAA6B,CAAC,QAAgC;IAC5E,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAe;YACzB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAC1C,MAAM,MAAM,GAAiB,EAAE,CAAC;YAEhC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;gBACxC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;oBACpB,iEAAiE;oBACjE,mEAAmE;oBACnE,iBAAiB;oBACjB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;oBACpH,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;oBACrC,SAAS;gBACX,CAAC;gBAED,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;gBAChC,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,mEAAmE;oBACnE,mEAAmE;oBACnE,iEAAiE;oBACjE,mDAAmD;oBACnD,YAAY,EAAE,GAAG,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,YAAY,CAAC;iBACvF,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;gBAAE,OAAO,IAAI,CAAC;YACrC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACnE,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * PHP uncaught-`Throwable` trace line shapes — the `LineClassifier` for
3
+ * `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
4
+ * feeds `SourceLocationResolver`.
5
+ *
6
+ * Grammar verified against `php-src`'s own `Zend/zend_exceptions.c` — the C
7
+ * source that generates this text (`build_trace_string`,
8
+ * `zend_trace_to_string`, and the default `Throwable::__toString`
9
+ * composition around line 780) — and against `php-src`'s own `.phpt`
10
+ * conformance suite, the same category of interpreter-verified source
11
+ * `ruby/spec` was for RT-038. See `test/fixtures/PROVENANCE.md` for the
12
+ * specific citations.
13
+ *
14
+ * **A real `php` runs against this now** — `php-call-shapes.test.ts` spawns
15
+ * 8.4.4 and asserts against its stdout. That upgrade is what found RT-105:
16
+ * the engine-source reading above was correct about the grammar it read and
17
+ * was reading the *older* grammar, and PHP 8.4 had since put parentheses
18
+ * inside a function name. A format read from the generator is only as
19
+ * current as the generator you read.
20
+ *
21
+ * PHP's shape is its own, distinct from V8, CPython and Ruby alike:
22
+ *
23
+ * 1. **The header carries a real location, but it is a separate line from
24
+ * the first frame, with `Stack trace:` sitting between them.**
25
+ * `Fatal error: Uncaught RuntimeException: invoice 3 is corrupt in
26
+ * /app/InvoiceService.php:15` names the exception AND its construction
27
+ * site (`Throwable::getFile()`/`getLine()` — the near-universal `throw
28
+ * new X(...)` pattern makes this the throw site, though strictly it is
29
+ * where the exception object was `new`-ed, which can differ if a caller
30
+ * holds a `Throwable` across lines before throwing it). That makes it a
31
+ * genuine, un-fabricated frame — same reasoning as RT-037's header-only
32
+ * Rust panic, `functionName: null` because PHP names no function for
33
+ * this specific location — but the literal `Stack trace:` line always
34
+ * sits between the header and the first real `#N` frame.
35
+ * `createPhpLineClassifier` reports this via `LineClassifier.isBridge`
36
+ * (RT-048): a lone `Stack trace:` line is never even offered to
37
+ * `isBridge` — `ExceptionBlockDetector` only consults it while a header
38
+ * is already being held — so an application log line that happens to
39
+ * print the same phrase for an unrelated reason cannot open anything on
40
+ * its own, and this classifier no longer needs to track that itself.
41
+ * 2. **A real frame carries a call-type marker PHP itself distinguishes:**
42
+ * `Class->method()` for an instance call, `Class::method()` for a
43
+ * static one (verified verbatim against `Zend/tests/bug65911.phpt`'s
44
+ * `B->go()` and `Zend/tests/inheritance/bug44141.phpt`'s `Y::cheat(5)`).
45
+ * Both are kept exactly as printed, never normalised to one form.
46
+ * 3. **A frame can have no location at all.** `#N [internal function]:
47
+ * Class::method(args)` — a call made through `array_map`,
48
+ * `call_user_func` or similar, where PHP genuinely has no file/line to
49
+ * report — confirmed against `Zend/tests/bug74164.phpt`. This is exactly
50
+ * the case RT-037 widened `RawFrameLocation.file`/`.line` to `| null`
51
+ * for; both are `null` here rather than a fabricated location.
52
+ * 4. **`#N {main}` ends the trace and is a trailer, not a frame** — it has
53
+ * no trailing `(args)`, so `FRAME_PATTERN` never matches it, and
54
+ * `isTrailer` closes the block on it. What follows (` thrown in FILE
55
+ * on line N`, present on the CLI/log-formatted output) is real but
56
+ * entirely redundant with the header's own file:line, so it is left to
57
+ * fall through as a plain log line rather than special-cased into the
58
+ * block — same choice CPython's `isTrailer` design already established
59
+ * for "attach only what identifies the exception, not everything after
60
+ * it."
61
+ * 5. **Chained exceptions (`Throwable::getPrevious()`) need no
62
+ * connective-exclusion logic**, unlike CPython's. `#N {main}` closes the
63
+ * first block cleanly; the next line is a blank line (an honest, empty
64
+ * log event) and then `Next Class: message in file:line` — which
65
+ * `HEADER_PATTERN`'s lack of start-of-line anchoring matches without
66
+ * the literal word `Next` needing any special handling at all, the same
67
+ * way V8's and Ruby's non-anchored header patterns handle a prefix they
68
+ * were never written to expect. Verified against
69
+ * `Zend/tests/throwable_001.phpt`, including the detail that only the
70
+ * **outermost** exception's `thrown in ... on line N` trailer appears —
71
+ * once, at the very end, not once per link.
72
+ */
73
+ export interface RawPhpFrame {
74
+ /** `Class->method`, `Class::method`, a bare function name, or `{closure:file:line}` — kept verbatim, never rewritten. */
75
+ readonly functionName: string | null;
76
+ /** Null for an `[internal function]` frame — no file was ever printed, never fabricated. */
77
+ readonly file: string | null;
78
+ /** Null exactly when `file` is — the two are only ever known or unknown together in this grammar. */
79
+ readonly line: number | null;
80
+ }
81
+ export declare function isPhpHeaderLine(line: string): boolean;
82
+ export declare function isPhpFrameLine(line: string): boolean;
83
+ export declare function isPhpTrailerLine(line: string): boolean;
84
+ /**
85
+ * Stateless (RT-048): the single `Stack trace:` line that separates a
86
+ * header from its first real frame is reported via `isBridge`, which
87
+ * `ExceptionBlockDetector` only ever consults while a header is already
88
+ * being held — never from idle. A `Stack trace:` line with no preceding
89
+ * held header therefore can't reach this method at all, so recognising it
90
+ * unconditionally here is exactly as safe as PHP's previous hand-gated
91
+ * boolean, without the classifier having to track "did I just hold a
92
+ * header" itself.
93
+ *
94
+ * Before RT-048, this correctness property depended on a boolean this
95
+ * classifier tracked and reset itself (`awaitingStackTraceMarker`); it is
96
+ * now a structural property of `ExceptionBlockDetector`'s own state
97
+ * machine instead — see `LineClassifier.isBridge`'s doc comment.
98
+ */
99
+ export declare function createPhpLineClassifier(): {
100
+ isHeader(line: string): boolean;
101
+ isFrame(line: string): boolean;
102
+ isBridge(line: string): boolean;
103
+ isTrailer(line: string): boolean;
104
+ };
105
+ /** `null` when the line doesn't match PHP's `Class[: message] in file:line` header shape — never fabricated. */
106
+ export declare function parsePhpHeaderFrame(line: string): RawPhpFrame | null;
107
+ /** `null` for anything not shaped like a `#N (file(line)|[internal function]): callee(...)` frame — never fabricated. */
108
+ export declare function parsePhpFrame(line: string): RawPhpFrame | null;
109
+ //# sourceMappingURL=php-frame-parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-frame-parsing.d.ts","sourceRoot":"","sources":["../src/php-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,MAAM,WAAW,WAAW;IAC1B,yHAAyH;IACzH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,4FAA4F;IAC5F,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,qGAAqG;IACrG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AA+DD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,IAAI;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC,CAkBA;AAED,gHAAgH;AAChH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAQpE;AAED,yHAAyH;AACzH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAe9D"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * PHP uncaught-`Throwable` trace line shapes — the `LineClassifier` for
3
+ * `ExceptionBlockDetector` plus the raw-text-to-frame-parts extraction that
4
+ * feeds `SourceLocationResolver`.
5
+ *
6
+ * Grammar verified against `php-src`'s own `Zend/zend_exceptions.c` — the C
7
+ * source that generates this text (`build_trace_string`,
8
+ * `zend_trace_to_string`, and the default `Throwable::__toString`
9
+ * composition around line 780) — and against `php-src`'s own `.phpt`
10
+ * conformance suite, the same category of interpreter-verified source
11
+ * `ruby/spec` was for RT-038. See `test/fixtures/PROVENANCE.md` for the
12
+ * specific citations.
13
+ *
14
+ * **A real `php` runs against this now** — `php-call-shapes.test.ts` spawns
15
+ * 8.4.4 and asserts against its stdout. That upgrade is what found RT-105:
16
+ * the engine-source reading above was correct about the grammar it read and
17
+ * was reading the *older* grammar, and PHP 8.4 had since put parentheses
18
+ * inside a function name. A format read from the generator is only as
19
+ * current as the generator you read.
20
+ *
21
+ * PHP's shape is its own, distinct from V8, CPython and Ruby alike:
22
+ *
23
+ * 1. **The header carries a real location, but it is a separate line from
24
+ * the first frame, with `Stack trace:` sitting between them.**
25
+ * `Fatal error: Uncaught RuntimeException: invoice 3 is corrupt in
26
+ * /app/InvoiceService.php:15` names the exception AND its construction
27
+ * site (`Throwable::getFile()`/`getLine()` — the near-universal `throw
28
+ * new X(...)` pattern makes this the throw site, though strictly it is
29
+ * where the exception object was `new`-ed, which can differ if a caller
30
+ * holds a `Throwable` across lines before throwing it). That makes it a
31
+ * genuine, un-fabricated frame — same reasoning as RT-037's header-only
32
+ * Rust panic, `functionName: null` because PHP names no function for
33
+ * this specific location — but the literal `Stack trace:` line always
34
+ * sits between the header and the first real `#N` frame.
35
+ * `createPhpLineClassifier` reports this via `LineClassifier.isBridge`
36
+ * (RT-048): a lone `Stack trace:` line is never even offered to
37
+ * `isBridge` — `ExceptionBlockDetector` only consults it while a header
38
+ * is already being held — so an application log line that happens to
39
+ * print the same phrase for an unrelated reason cannot open anything on
40
+ * its own, and this classifier no longer needs to track that itself.
41
+ * 2. **A real frame carries a call-type marker PHP itself distinguishes:**
42
+ * `Class->method()` for an instance call, `Class::method()` for a
43
+ * static one (verified verbatim against `Zend/tests/bug65911.phpt`'s
44
+ * `B->go()` and `Zend/tests/inheritance/bug44141.phpt`'s `Y::cheat(5)`).
45
+ * Both are kept exactly as printed, never normalised to one form.
46
+ * 3. **A frame can have no location at all.** `#N [internal function]:
47
+ * Class::method(args)` — a call made through `array_map`,
48
+ * `call_user_func` or similar, where PHP genuinely has no file/line to
49
+ * report — confirmed against `Zend/tests/bug74164.phpt`. This is exactly
50
+ * the case RT-037 widened `RawFrameLocation.file`/`.line` to `| null`
51
+ * for; both are `null` here rather than a fabricated location.
52
+ * 4. **`#N {main}` ends the trace and is a trailer, not a frame** — it has
53
+ * no trailing `(args)`, so `FRAME_PATTERN` never matches it, and
54
+ * `isTrailer` closes the block on it. What follows (` thrown in FILE
55
+ * on line N`, present on the CLI/log-formatted output) is real but
56
+ * entirely redundant with the header's own file:line, so it is left to
57
+ * fall through as a plain log line rather than special-cased into the
58
+ * block — same choice CPython's `isTrailer` design already established
59
+ * for "attach only what identifies the exception, not everything after
60
+ * it."
61
+ * 5. **Chained exceptions (`Throwable::getPrevious()`) need no
62
+ * connective-exclusion logic**, unlike CPython's. `#N {main}` closes the
63
+ * first block cleanly; the next line is a blank line (an honest, empty
64
+ * log event) and then `Next Class: message in file:line` — which
65
+ * `HEADER_PATTERN`'s lack of start-of-line anchoring matches without
66
+ * the literal word `Next` needing any special handling at all, the same
67
+ * way V8's and Ruby's non-anchored header patterns handle a prefix they
68
+ * were never written to expect. Verified against
69
+ * `Zend/tests/throwable_001.phpt`, including the detail that only the
70
+ * **outermost** exception's `thrown in ... on line N` trailer appears —
71
+ * once, at the very end, not once per link.
72
+ */
73
+ const STACK_TRACE_MARKER = "Stack trace:";
74
+ /**
75
+ * Not anchored to the start of the line, for the same reason V8's and
76
+ * Ruby's header patterns aren't: PHP's own "Fatal error: " / "PHP Fatal
77
+ * error: " SAPI prefix, an application logger's own prefix, and the
78
+ * chained-exception "Next " marker are all real prefixes this must see
79
+ * through. The greedy `message` group combined with the `$`-anchored
80
+ * `in file:line` suffix correctly resolves to the LAST such suffix on the
81
+ * line even when the message text itself contains the substring " in ".
82
+ */
83
+ const HEADER_PATTERN = /(?<class>[A-Za-z_\x7f-\xff][\w\\]*)(?::\s(?<message>.*))?\s+in\s+(?<file>.+):(?<line>\d+)$/;
84
+ /**
85
+ * `#N file(line): [Class-><or::>]function(args)` or
86
+ * `#N [internal function]: [Class-><or::>]function(args)`. Requiring a
87
+ * trailing `(...)` is what excludes `#N {main}` from this pattern, so no
88
+ * separate exclusion is needed. Args can themselves contain parens
89
+ * (`Object(Closure)`, a nested call's own printed form) and are not
90
+ * otherwise parsed; they stay in `raw` for audit.
91
+ *
92
+ * ## Why the callee is not simply `[^(]+` (RT-105)
93
+ *
94
+ * It was, and **PHP 8.4 put parentheses inside a function name.** The
95
+ * "Closures in stack traces" change (php-src #12178, shipped 2024-11-21)
96
+ * renamed a closure declared inside a method from `{closure}` to
97
+ * `{closure:Class::method():LINE}`. Measured against real `php 8.4.4`:
98
+ *
99
+ * ```
100
+ * #0 /app/Repo.php(2): Repo->{closure:Repo::load():5}()
101
+ * ```
102
+ *
103
+ * `[^(]+` stops at the `(` in `load()`, so the callee parsed as
104
+ * `Repo->{closure:Repo::load` — the line still matched `isFrame`, still
105
+ * carried the right file and line, and reported a **truncated, malformed
106
+ * function name with full confidence.** Not lost evidence: wrong evidence,
107
+ * which is the class this adapter's cross-language controls exist to ban.
108
+ * PHP < 8.4 printed a bare `{closure}` and could not reach it.
109
+ *
110
+ * The fix is a grammar, not a looser pattern. A `{closure:...}` block is
111
+ * the one place a *name* may legitimately contain a paren, so it is matched
112
+ * whole and everything else stays `[^(]`. It must come first in the
113
+ * alternation: `[^(]` would otherwise consume the `{` and leave the engine
114
+ * mid-token when it reached the inner paren.
115
+ *
116
+ * Greedy `.+` was the obvious alternative and is wrong — measured, not
117
+ * assumed: on `handler(Object(Closure))` it backtracks to the *last* `(`
118
+ * and yields the callee `handler(Object`, breaking the nested-args case
119
+ * `[^(]+` was written for in the first place.
120
+ *
121
+ * **Not changed, because it was measured and holds:** the greedy `file`
122
+ * group looks like it should mis-split on a string argument containing
123
+ * `(N): ` — `audit('order(12): x')` — and does not. The engine tries that
124
+ * split first, fails to satisfy the callee-plus-args tail, and backtracks
125
+ * to the real one. Recorded because the next reader will suspect it too.
126
+ */
127
+ const FRAME_PATTERN = /^#\d+\s+(?:(?<file>.+)\((?<line>\d+)\):\s|\[internal function\]:\s)(?<callee>(?:\{closure:[^}]*\}|[^(])+)\(.*\)\s*$/;
128
+ const TRAILER_PATTERN = /^#\d+\s+\{main\}\s*$/;
129
+ export function isPhpHeaderLine(line) {
130
+ return HEADER_PATTERN.test(line);
131
+ }
132
+ export function isPhpFrameLine(line) {
133
+ return FRAME_PATTERN.test(line);
134
+ }
135
+ export function isPhpTrailerLine(line) {
136
+ return TRAILER_PATTERN.test(line);
137
+ }
138
+ /**
139
+ * Stateless (RT-048): the single `Stack trace:` line that separates a
140
+ * header from its first real frame is reported via `isBridge`, which
141
+ * `ExceptionBlockDetector` only ever consults while a header is already
142
+ * being held — never from idle. A `Stack trace:` line with no preceding
143
+ * held header therefore can't reach this method at all, so recognising it
144
+ * unconditionally here is exactly as safe as PHP's previous hand-gated
145
+ * boolean, without the classifier having to track "did I just hold a
146
+ * header" itself.
147
+ *
148
+ * Before RT-048, this correctness property depended on a boolean this
149
+ * classifier tracked and reset itself (`awaitingStackTraceMarker`); it is
150
+ * now a structural property of `ExceptionBlockDetector`'s own state
151
+ * machine instead — see `LineClassifier.isBridge`'s doc comment.
152
+ */
153
+ export function createPhpLineClassifier() {
154
+ return {
155
+ isHeader(line) {
156
+ return isPhpHeaderLine(line);
157
+ },
158
+ isFrame(line) {
159
+ return isPhpFrameLine(line);
160
+ },
161
+ isBridge(line) {
162
+ return line.trim() === STACK_TRACE_MARKER;
163
+ },
164
+ isTrailer(line) {
165
+ return isPhpTrailerLine(line);
166
+ },
167
+ };
168
+ }
169
+ /** `null` when the line doesn't match PHP's `Class[: message] in file:line` header shape — never fabricated. */
170
+ export function parsePhpHeaderFrame(line) {
171
+ const match = HEADER_PATTERN.exec(line);
172
+ if (!match?.groups)
173
+ return null;
174
+ const file = match.groups["file"];
175
+ const lineNumber = Number(match.groups["line"]);
176
+ if (file === undefined || !Number.isInteger(lineNumber))
177
+ return null;
178
+ // PHP names no function for the exception's own construction site.
179
+ return { functionName: null, file, line: lineNumber };
180
+ }
181
+ /** `null` for anything not shaped like a `#N (file(line)|[internal function]): callee(...)` frame — never fabricated. */
182
+ export function parsePhpFrame(line) {
183
+ const match = FRAME_PATTERN.exec(line);
184
+ if (!match?.groups)
185
+ return null;
186
+ const callee = match.groups["callee"];
187
+ if (callee === undefined)
188
+ return null;
189
+ const functionName = callee.trim();
190
+ const file = match.groups["file"];
191
+ if (file === undefined) {
192
+ // The `[internal function]:` branch matched — no location was ever printed.
193
+ return { functionName, file: null, line: null };
194
+ }
195
+ const lineNumber = Number(match.groups["line"]);
196
+ if (!Number.isInteger(lineNumber))
197
+ return null;
198
+ return { functionName, file, line: lineNumber };
199
+ }
200
+ //# sourceMappingURL=php-frame-parsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-frame-parsing.js","sourceRoot":"","sources":["../src/php-frame-parsing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAWH,MAAM,kBAAkB,GAAG,cAAc,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,4FAA4F,CAAC;AAEpH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,aAAa,GACjB,qHAAqH,CAAC;AAExH,MAAM,eAAe,GAAG,sBAAsB,CAAC;AAE/C,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,uBAAuB;IAMrC,OAAO;QACL,QAAQ,CAAC,IAAY;YACnB,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC;QAED,OAAO,CAAC,IAAY;YAClB,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;QAED,QAAQ,CAAC,IAAY;YACnB,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,kBAAkB,CAAC;QAC5C,CAAC;QAED,SAAS,CAAC,IAAY;YACpB,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,mEAAmE;IACnE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AACxD,CAAC;AAED,yHAAyH;AACzH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAEnC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,4EAA4E;QAC5E,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAClD,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAClD,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The concrete `RuntimeAdapter` for PHP.
3
+ *
4
+ * PHP's grammar needed no seam changes beyond RT-037's already-nullable
5
+ * `SourceLocation`/`RawFrameLocation` fields, which an `[internal function]`
6
+ * frame needs but did not itself force — that widening already existed by
7
+ * the time this adapter was built. See `php-frame-parsing.ts` for the one
8
+ * behaviour this language DID need from the classifier: recognising the
9
+ * single `Stack trace:` line that separates PHP's header from its first
10
+ * real frame, and only the one that follows a header just held.
11
+ */
12
+ import type { RuntimeAdapter } from "@descryy/runtime-backend-observation";
13
+ export interface PhpRuntimeAdapterOptions {
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 logging this process uses (§16, RT-112).
18
+ *
19
+ * **Declared, never sniffed**, the same rule ASP.NET's and Rails' options
20
+ * follow. Laravel logs through Monolog rather than through PHP's own
21
+ * `Throwable::__toString()`: the header reads `(Class(code: 0): message at
22
+ * file:line)` where PHP's reads `... in file:line`, the bridge is
23
+ * `[stacktrace]` rather than `Stack trace:`, and class names arrive
24
+ * JSON-escaped. Measured live: **0 exceptions, and a bare 53-line
25
+ * STACK_TRACE with no identity attached to it.**
26
+ *
27
+ * **This option cannot fix Laravel's default log destination**, which is a
28
+ * file (`storage/logs/laravel.log`). A collector watching process output
29
+ * sees nothing at all unless the application sets `LOG_CHANNEL=stderr`.
30
+ * That is a disclosure to the user, not something an adapter can decide.
31
+ */
32
+ readonly framework?: "laravel";
33
+ }
34
+ export declare function createPhpRuntimeAdapter(options?: PhpRuntimeAdapterOptions): RuntimeAdapter;
35
+ //# sourceMappingURL=php-runtime-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-runtime-adapter.d.ts","sourceRoot":"","sources":["../src/php-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,sCAAsC,CAAC;AAUhG,MAAM,WAAW,wBAAwB;IACvC,qGAAqG;IACrG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,wBAA6B,GAAG,cAAc,CA2B9F"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The concrete `RuntimeAdapter` for PHP.
3
+ *
4
+ * PHP's grammar needed no seam changes beyond RT-037's already-nullable
5
+ * `SourceLocation`/`RawFrameLocation` fields, which an `[internal function]`
6
+ * frame needs but did not itself force — that widening already existed by
7
+ * the time this adapter was built. See `php-frame-parsing.ts` for the one
8
+ * behaviour this language DID need from the classifier: recognising the
9
+ * single `Stack trace:` line that separates PHP's header from its first
10
+ * real frame, and only the one that follows a header just held.
11
+ */
12
+ import { createLogCollector } from "@descryy/runtime-backend-observation";
13
+ import { createLaravelLineClassifier } from "./laravel-frame-parsing.js";
14
+ import { createLaravelStackTraceParser } from "./laravel-stack-trace-parser.js";
15
+ import { createPhpLineClassifier } from "./php-frame-parsing.js";
16
+ import { createPhpSourceLocationResolver } from "./php-source-location-resolver.js";
17
+ import { createPhpStackTraceParser } from "./php-stack-trace-parser.js";
18
+ export function createPhpRuntimeAdapter(options = {}) {
19
+ const sourceLocationResolver = createPhpSourceLocationResolver();
20
+ const isLaravel = options.framework === "laravel";
21
+ const stackTraceParser = isLaravel
22
+ ? createLaravelStackTraceParser(sourceLocationResolver)
23
+ : createPhpStackTraceParser(sourceLocationResolver);
24
+ function createBackendCollectors(sources) {
25
+ return sources.map((source) => createLogCollector({
26
+ source,
27
+ // One classifier per collector, never shared: it carries the
28
+ // "just saw a header, expecting Stack trace: next" bit, and two
29
+ // processes' output must not prime each other.
30
+ classifier: isLaravel ? createLaravelLineClassifier() : createPhpLineClassifier(),
31
+ stackTraceParser,
32
+ ...(options.service !== undefined ? { service: options.service } : {}),
33
+ }));
34
+ }
35
+ return {
36
+ language: "php",
37
+ stackTraceParser,
38
+ sourceLocationResolver,
39
+ createBackendCollectors,
40
+ };
41
+ }
42
+ //# sourceMappingURL=php-runtime-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-runtime-adapter.js","sourceRoot":"","sources":["../src/php-runtime-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAE1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAwBxE,MAAM,UAAU,uBAAuB,CAAC,UAAoC,EAAE;IAC5E,MAAM,sBAAsB,GAAG,+BAA+B,EAAE,CAAC;IACjE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;IAClD,MAAM,gBAAgB,GAAG,SAAS;QAChC,CAAC,CAAC,6BAA6B,CAAC,sBAAsB,CAAC;QACvD,CAAC,CAAC,yBAAyB,CAAC,sBAAsB,CAAC,CAAC;IAEtD,SAAS,uBAAuB,CAAC,OAAuC;QACtE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC5B,kBAAkB,CAAC;YACjB,MAAM;YACN,6DAA6D;YAC7D,gEAAgE;YAChE,+CAA+C;YAC/C,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC,uBAAuB,EAAE;YACjF,gBAAgB;YAChB,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,KAAK;QACf,gBAAgB;QAChB,sBAAsB;QACtB,uBAAuB;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * PHP `SourceLocationResolver`.
3
+ *
4
+ * **Every location is `self-contained`, and that is a positive claim rather
5
+ * than a fallback** — the same category RT-003 already assigns PHP traces
6
+ * alongside Python tracebacks and Ruby backtraces. A real frame names the
7
+ * `.php` file the engine was actually executing; there is no compilation
8
+ * step and no separate map that can drift or go missing. The frame IS the
9
+ * answer, when the engine printed one at all.
10
+ *
11
+ * **A frame can have no location.** `#N [internal function]: ...` means
12
+ * PHP genuinely captured no file or line for that call — not that this
13
+ * resolver failed to look. `reliability` stays `self-contained` even then:
14
+ * this is not a side artifact that went missing (there was never a PDB or
15
+ * source map to lose), it is the engine's own honest report that it has
16
+ * nothing to offer for that frame. `file`/`line` pass through as the `null`
17
+ * they arrived as, per RT-037's widening of `SourceLocation` for exactly
18
+ * this shape (a stripped JVM frame first forced it; a PHP internal-function
19
+ * frame is the same shape for a different reason).
20
+ *
21
+ * **What this deliberately does not do:** check that the file exists on
22
+ * disk, or that its contents still match what ran. `self-contained` is a
23
+ * statement about the mechanism, not about whether the working tree has
24
+ * since been edited.
25
+ *
26
+ * `column` is always null. PHP traces never print one.
27
+ */
28
+ import type { SourceLocationResolver } from "@descryy/runtime-backend-observation";
29
+ export declare function createPhpSourceLocationResolver(): SourceLocationResolver;
30
+ //# sourceMappingURL=php-source-location-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-source-location-resolver.d.ts","sourceRoot":"","sources":["../src/php-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAErG,wBAAgB,+BAA+B,IAAI,sBAAsB,CAgBxE"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * PHP `SourceLocationResolver`.
3
+ *
4
+ * **Every location is `self-contained`, and that is a positive claim rather
5
+ * than a fallback** — the same category RT-003 already assigns PHP traces
6
+ * alongside Python tracebacks and Ruby backtraces. A real frame names the
7
+ * `.php` file the engine was actually executing; there is no compilation
8
+ * step and no separate map that can drift or go missing. The frame IS the
9
+ * answer, when the engine printed one at all.
10
+ *
11
+ * **A frame can have no location.** `#N [internal function]: ...` means
12
+ * PHP genuinely captured no file or line for that call — not that this
13
+ * resolver failed to look. `reliability` stays `self-contained` even then:
14
+ * this is not a side artifact that went missing (there was never a PDB or
15
+ * source map to lose), it is the engine's own honest report that it has
16
+ * nothing to offer for that frame. `file`/`line` pass through as the `null`
17
+ * they arrived as, per RT-037's widening of `SourceLocation` for exactly
18
+ * this shape (a stripped JVM frame first forced it; a PHP internal-function
19
+ * frame is the same shape for a different reason).
20
+ *
21
+ * **What this deliberately does not do:** check that the file exists on
22
+ * disk, or that its contents still match what ran. `self-contained` is a
23
+ * statement about the mechanism, not about whether the working tree has
24
+ * since been edited.
25
+ *
26
+ * `column` is always null. PHP traces never print one.
27
+ */
28
+ export function createPhpSourceLocationResolver() {
29
+ return {
30
+ // Async to satisfy the interface; PHP needs no I/O to answer, so this
31
+ // never awaits — same as the Python and Ruby resolvers, for the same
32
+ // reason.
33
+ resolve(frame) {
34
+ return Promise.resolve({
35
+ file: frame.file,
36
+ line: frame.line,
37
+ column: frame.column,
38
+ functionName: frame.functionName,
39
+ reliability: "self-contained",
40
+ resolvedVia: null,
41
+ });
42
+ },
43
+ };
44
+ }
45
+ //# sourceMappingURL=php-source-location-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-source-location-resolver.js","sourceRoot":"","sources":["../src/php-source-location-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,MAAM,UAAU,+BAA+B;IAC7C,OAAO;QACL,sEAAsE;QACtE,qEAAqE;QACrE,UAAU;QACV,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,20 @@
1
+ /**
2
+ * Composes the pure PHP line-recognition functions (`php-frame-parsing.ts`)
3
+ * with a `SourceLocationResolver` to implement the full async
4
+ * `StackTraceParser` contract.
5
+ *
6
+ * **No reversal.** The header's own file:line is the exception's
7
+ * construction site — the innermost real location — and each `#N` frame
8
+ * walks outward toward `{main}`. `StackTrace.frames` is contractually
9
+ * most-recent-first, and that is the order this text already prints in.
10
+ *
11
+ * `fidelity` is always reported `"synchronous"`. A PHP trace is built by
12
+ * walking the actual call frames live at the moment the `Throwable` was
13
+ * constructed — every location shown is a genuine link in the chain that
14
+ * was executing. It cannot show a logical caller across a `Fiber` (PHP
15
+ * 8.1+) suspension boundary, the same class of limit every other adapter
16
+ * in this codebase shares and none claims to see through by text alone.
17
+ */
18
+ import type { SourceLocationResolver, StackTraceParser } from "@descryy/runtime-backend-observation";
19
+ export declare function createPhpStackTraceParser(resolver: SourceLocationResolver): StackTraceParser;
20
+ //# sourceMappingURL=php-stack-trace-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-stack-trace-parser.d.ts","sourceRoot":"","sources":["../src/php-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAIrG,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,sBAAsB,GAAG,gBAAgB,CAiD5F"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Composes the pure PHP line-recognition functions (`php-frame-parsing.ts`)
3
+ * with a `SourceLocationResolver` to implement the full async
4
+ * `StackTraceParser` contract.
5
+ *
6
+ * **No reversal.** The header's own file:line is the exception's
7
+ * construction site — the innermost real location — and each `#N` frame
8
+ * walks outward toward `{main}`. `StackTrace.frames` is contractually
9
+ * most-recent-first, and that is the order this text already prints in.
10
+ *
11
+ * `fidelity` is always reported `"synchronous"`. A PHP trace is built by
12
+ * walking the actual call frames live at the moment the `Throwable` was
13
+ * constructed — every location shown is a genuine link in the chain that
14
+ * was executing. It cannot show a logical caller across a `Fiber` (PHP
15
+ * 8.1+) suspension boundary, the same class of limit every other adapter
16
+ * in this codebase shares and none claims to see through by text alone.
17
+ */
18
+ import { parsePhpHeaderFrame, parsePhpFrame } from "./php-frame-parsing.js";
19
+ export function createPhpStackTraceParser(resolver) {
20
+ return {
21
+ async parse(rawText) {
22
+ const lines = rawText.split(/\r\n|\r|\n/);
23
+ const frames = [];
24
+ for (const line of lines) {
25
+ // Structurally disjoint: a real frame line always starts with `#N`,
26
+ // the header never does, so at most one of these ever matches a
27
+ // given line. `Stack trace:` and the `#N {main}` trailer match
28
+ // neither and are real parts of the block that are simply not
29
+ // frames — nothing is fabricated for them.
30
+ const raw = parsePhpFrame(line) ?? parsePhpHeaderFrame(line);
31
+ if (raw === null)
32
+ continue;
33
+ const location = await resolver.resolve({
34
+ file: raw.file,
35
+ line: raw.line,
36
+ column: null,
37
+ functionName: raw.functionName,
38
+ });
39
+ frames.push({ location, raw: line });
40
+ }
41
+ if (frames.length === 0) {
42
+ // Nothing frame-shaped at all — not recognisable as a PHP
43
+ // uncaught-Throwable trace, per this interface's contract: null,
44
+ // never an empty-but-structured StackTrace standing in for
45
+ // "couldn't parse".
46
+ return null;
47
+ }
48
+ // `primaryFrameIndex` is required so that every adapter states an
49
+ // answer rather than inheriting one (RT-083). PHP's is 0 because the
50
+ // **header carries a location no `#N` frame does** — `getFile()` /
51
+ // `getLine()`, the site where the `Throwable` was constructed — and
52
+ // it is the first line of the block, so it parses to frame 0.
53
+ //
54
+ // This is emphatically not "index 0 happens to work". PHP's `#N`
55
+ // frames name the **callee at the caller's location**, so `#0` is the
56
+ // call site one level out; an adapter that pointed at `#0` would
57
+ // report the caller's line as the failure site and look correct doing
58
+ // it. See `php-frame-parsing.ts` for the one caveat this inherits:
59
+ // strictly the header is where the exception was `new`-ed, which
60
+ // differs from the throw site only if a caller holds a `Throwable`
61
+ // across lines before throwing it.
62
+ return { fidelity: "synchronous", frames, primaryFrameIndex: 0 };
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=php-stack-trace-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"php-stack-trace-parser.js","sourceRoot":"","sources":["../src/php-stack-trace-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5E,MAAM,UAAU,yBAAyB,CAAC,QAAgC;IACxE,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,oEAAoE;gBACpE,gEAAgE;gBAChE,+DAA+D;gBAC/D,8DAA8D;gBAC9D,2CAA2C;gBAC3C,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,CAAC;gBAC7D,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,0DAA0D;gBAC1D,iEAAiE;gBACjE,2DAA2D;gBAC3D,oBAAoB;gBACpB,OAAO,IAAI,CAAC;YACd,CAAC;YAED,kEAAkE;YAClE,qEAAqE;YACrE,mEAAmE;YACnE,oEAAoE;YACpE,8DAA8D;YAC9D,EAAE;YACF,iEAAiE;YACjE,sEAAsE;YACtE,iEAAiE;YACjE,sEAAsE;YACtE,mEAAmE;YACnE,iEAAiE;YACjE,mEAAmE;YACnE,mCAAmC;YACnC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACnE,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@descryy/runtime-adapter-php",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "PHP runtime adapter: uncaught-Throwable trace parsing, self-contained source locations, the concrete RuntimeAdapter for PHP.",
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
+ }