@setu-ts/kernel 0.0.0-stage → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/AGENT_CARD.md +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +152 -2
  4. package/dist/application/application.d.ts +174 -0
  5. package/dist/application/application.d.ts.map +1 -0
  6. package/dist/application/application.js +1905 -0
  7. package/dist/application/application.js.map +1 -0
  8. package/dist/application/inject-body.d.ts +58 -0
  9. package/dist/application/inject-body.d.ts.map +1 -0
  10. package/dist/application/inject-body.js +82 -0
  11. package/dist/application/inject-body.js.map +1 -0
  12. package/dist/context/request-context.d.ts +32 -0
  13. package/dist/context/request-context.d.ts.map +1 -0
  14. package/dist/context/request-context.js +136 -0
  15. package/dist/context/request-context.js.map +1 -0
  16. package/dist/context/response.d.ts +60 -0
  17. package/dist/context/response.d.ts.map +1 -0
  18. package/dist/context/response.js +193 -0
  19. package/dist/context/response.js.map +1 -0
  20. package/dist/diagnostics/buffer.d.ts +143 -0
  21. package/dist/diagnostics/buffer.d.ts.map +1 -0
  22. package/dist/diagnostics/buffer.js +190 -0
  23. package/dist/diagnostics/buffer.js.map +1 -0
  24. package/dist/diagnostics/collector.d.ts +284 -0
  25. package/dist/diagnostics/collector.d.ts.map +1 -0
  26. package/dist/diagnostics/collector.js +810 -0
  27. package/dist/diagnostics/collector.js.map +1 -0
  28. package/dist/diagnostics/projection.d.ts +153 -0
  29. package/dist/diagnostics/projection.d.ts.map +1 -0
  30. package/dist/diagnostics/projection.js +226 -0
  31. package/dist/diagnostics/projection.js.map +1 -0
  32. package/dist/index.d.ts +12 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +11 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/lifecycle/lifecycle-manager.d.ts +136 -0
  37. package/dist/lifecycle/lifecycle-manager.d.ts.map +1 -0
  38. package/dist/lifecycle/lifecycle-manager.js +206 -0
  39. package/dist/lifecycle/lifecycle-manager.js.map +1 -0
  40. package/dist/pipeline/execute-chain.d.ts +71 -0
  41. package/dist/pipeline/execute-chain.d.ts.map +1 -0
  42. package/dist/pipeline/execute-chain.js +104 -0
  43. package/dist/pipeline/execute-chain.js.map +1 -0
  44. package/dist/pipeline/middleware-pipeline.d.ts +68 -0
  45. package/dist/pipeline/middleware-pipeline.d.ts.map +1 -0
  46. package/dist/pipeline/middleware-pipeline.js +82 -0
  47. package/dist/pipeline/middleware-pipeline.js.map +1 -0
  48. package/dist/registry/plugin-resolver.d.ts +42 -0
  49. package/dist/registry/plugin-resolver.d.ts.map +1 -0
  50. package/dist/registry/plugin-resolver.js +140 -0
  51. package/dist/registry/plugin-resolver.js.map +1 -0
  52. package/dist/registry/service-registry.d.ts +110 -0
  53. package/dist/registry/service-registry.d.ts.map +1 -0
  54. package/dist/registry/service-registry.js +197 -0
  55. package/dist/registry/service-registry.js.map +1 -0
  56. package/dist/router/route-matcher.d.ts +85 -0
  57. package/dist/router/route-matcher.d.ts.map +1 -0
  58. package/dist/router/route-matcher.js +105 -0
  59. package/dist/router/route-matcher.js.map +1 -0
  60. package/dist/router/router.d.ts +130 -0
  61. package/dist/router/router.d.ts.map +1 -0
  62. package/dist/router/router.js +350 -0
  63. package/dist/router/router.js.map +1 -0
  64. package/package.json +49 -3
  65. package/src/application/application.ts +2326 -0
  66. package/src/application/inject-body.ts +130 -0
  67. package/src/context/request-context.ts +169 -0
  68. package/src/context/response.ts +246 -0
  69. package/src/diagnostics/buffer.ts +243 -0
  70. package/src/diagnostics/collector.ts +1030 -0
  71. package/src/diagnostics/projection.ts +322 -0
  72. package/src/index.ts +20 -0
  73. package/src/lifecycle/lifecycle-manager.ts +282 -0
  74. package/src/pipeline/execute-chain.ts +162 -0
  75. package/src/pipeline/middleware-pipeline.ts +129 -0
  76. package/src/registry/plugin-resolver.ts +185 -0
  77. package/src/registry/service-registry.ts +262 -0
  78. package/src/router/route-matcher.ts +134 -0
  79. package/src/router/router.ts +485 -0
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Diagnostics event buffer — a bounded, non-destructive ring of frozen
3
+ * execution records with dense per-instance sequence numbers.
4
+ *
5
+ * Two bounds are enforced here, both fixed v1 constants and deliberately not
6
+ * options: the ring holds 1,024 events, and one event may encode to at most
7
+ * 1,024 UTF-8 bytes. An oversized event is dropped WHOLE — truncating one
8
+ * would mint a record whose shape the contract never defined. Eviction is
9
+ * oldest-first and is reported to readers as `lost` sequence numbers rather
10
+ * than hidden.
11
+ *
12
+ * Readers never mutate the ring: `read` is a pure window over the retained
13
+ * range, so two readers polling at different cursors never steal from each
14
+ * other and a slow consumer cannot backpressure the application.
15
+ *
16
+ * @module
17
+ */
18
+ import type { DiagnosticsEvent } from '@setu-ts/common';
19
+
20
+ import { saturatingNext } from './projection.ts';
21
+
22
+ /** Fixed v1 limit: events retained by the ring. */
23
+ export const EVENT_CAPACITY = 1024;
24
+
25
+ /** Fixed v1 limit: maximum UTF-8 byte length of one compact-encoded event. */
26
+ export const MAX_EVENT_BYTES = 1024;
27
+
28
+ /** Fixed v1 limit: maximum events returned by one read. */
29
+ export const MAX_READ_LIMIT = 128;
30
+
31
+ const encoder = new TextEncoder();
32
+
33
+ /**
34
+ * Bounded length of every string field an event may carry. Ids, stages, and
35
+ * outcomes are collector-owned fixed-vocabulary or bounded-identifier strings;
36
+ * the collector refuses any field over this bound at capture, dropping the
37
+ * event WHOLE.
38
+ */
39
+ const MAX_FIELD_LENGTH = 64;
40
+
41
+ /**
42
+ * Compact-JSON skeleton overhead of an event with empty variable fields:
43
+ * braces, keys, commas, the fixed kind/stage/outcome vocabulary, and maximal
44
+ * numeric fields. Every string field is collector-owned ASCII (ids, fixed
45
+ * vocabulary, validated hex identifiers), so the UTF-8 length equals the
46
+ * character count and the skeleton constant is an upper bound, not an
47
+ * estimate.
48
+ */
49
+ const EVENT_SKELETON_BYTES = 220;
50
+
51
+ /**
52
+ * The pre-serialization byte bound for one event — the M98a §3.6 rule that
53
+ * strings and counts are bounded BEFORE serialization, not measured after.
54
+ * With every field bounded at capture, the 1,024-byte cap holds
55
+ * structurally, so the request path never pays for a `JSON.stringify` it
56
+ * does not need.
57
+ *
58
+ * @param fieldLengths - Character lengths of the variable string fields
59
+ * @returns `true` when the encoded event is within {@linkcode MAX_EVENT_BYTES}
60
+ * @since 0.8.0
61
+ */
62
+ export function eventWithinByteCap(fieldLengths: {
63
+ readonly operationId: number;
64
+ readonly parentOperationId: number;
65
+ readonly nodeId: number;
66
+ readonly traceId: number;
67
+ readonly spanId: number;
68
+ readonly statusCode: boolean;
69
+ }): boolean {
70
+ const bounded =
71
+ EVENT_SKELETON_BYTES +
72
+ fieldLengths.operationId +
73
+ fieldLengths.parentOperationId +
74
+ fieldLengths.nodeId +
75
+ fieldLengths.traceId +
76
+ fieldLengths.spanId +
77
+ (fieldLengths.statusCode ? 21 : 0);
78
+ return bounded <= MAX_EVENT_BYTES;
79
+ }
80
+
81
+ /** The capture-time bound for one string field of an event. */
82
+ export const MAX_EVENT_FIELD_LENGTH: number = MAX_FIELD_LENGTH;
83
+
84
+ /**
85
+ * The exact UTF-8 byte length of the event's compact JSON encoding — used by
86
+ * tests to prove the structural bound above is sound against a real
87
+ * encoding, never on the request path.
88
+ *
89
+ * @param event - The event to measure
90
+ * @returns Encoded byte length
91
+ * @since 0.8.0
92
+ */
93
+ export function encodedEventByteLength(event: DiagnosticsEvent): number {
94
+ return encoder.encode(JSON.stringify(event)).length;
95
+ }
96
+
97
+ /**
98
+ * Cursor validation for {@linkcode IDiagnosticsSource.read}-shaped reads.
99
+ * Exported pure so the exact refusal behavior is unit-testable without a
100
+ * collector.
101
+ *
102
+ * @param after - The requested sequence cursor
103
+ * @param limit - The requested page size (`undefined` for the 128 default)
104
+ * @param lastSequence - The ring's most recently assigned sequence
105
+ * @returns The normalized `{ after, limit }`
106
+ * @throws {RangeError} With a fixed, VALUE-FREE message when `after` is not a
107
+ * non-negative safe integer, when `limit` is not an integer from 1 to 128, or
108
+ * when `after` is beyond the current sequence
109
+ * @since 0.8.0
110
+ */
111
+ export function validateReadCursor(
112
+ after: number,
113
+ limit: number | undefined,
114
+ lastSequence: number,
115
+ ): { after: number; limit: number } {
116
+ if (typeof after !== 'number' || !Number.isSafeInteger(after) || after < 0) {
117
+ throw new RangeError('Invalid diagnostics cursor: expected a non-negative safe integer.');
118
+ }
119
+ const effectiveLimit = limit ?? MAX_READ_LIMIT;
120
+ if (
121
+ typeof effectiveLimit !== 'number' ||
122
+ !Number.isSafeInteger(effectiveLimit) ||
123
+ effectiveLimit < 1 ||
124
+ effectiveLimit > MAX_READ_LIMIT
125
+ ) {
126
+ throw new RangeError(
127
+ `Invalid diagnostics read limit: expected an integer from 1 to ${MAX_READ_LIMIT}.`,
128
+ );
129
+ }
130
+ if (after > lastSequence) {
131
+ throw new RangeError('Invalid diagnostics cursor: beyond the current sequence.');
132
+ }
133
+ return { after, limit: effectiveLimit };
134
+ }
135
+
136
+ /**
137
+ * Bounded event ring. Sequences are allocated ONLY on a successful store, so
138
+ * the numbering is dense and a reader's `lost` count means exactly one thing:
139
+ * records between its cursor and the oldest retained record that are gone —
140
+ * evicted when the ring is full, or discarded by {@linkcode clear}, which
141
+ * never reuses a sequence number.
142
+ *
143
+ * @since 0.8.0
144
+ */
145
+ export class DiagnosticsEventRing {
146
+ #firstSequence = 1;
147
+ #lastSequence = 0;
148
+ readonly #slots: (DiagnosticsEvent | undefined)[] = new Array(EVENT_CAPACITY);
149
+ #closed = false;
150
+
151
+ /** Most recently assigned sequence number. */
152
+ get lastSequence(): number {
153
+ return this.#lastSequence;
154
+ }
155
+
156
+ /**
157
+ * Oldest retained sequence number: 1 while nothing has been evicted or
158
+ * cleared, and `lastSequence + 1` (nothing retained) right after a clear.
159
+ */
160
+ get firstSequence(): number {
161
+ return this.#firstSequence;
162
+ }
163
+
164
+ /** Whether the ring is closed to further writes (application stopped). */
165
+ get closed(): boolean {
166
+ return this.#closed;
167
+ }
168
+
169
+ /**
170
+ * Allocates the next sequence number, or `null` at saturation — the caller
171
+ * then stops event collection rather than letting identifiers wrap.
172
+ *
173
+ * @returns The next sequence number, or `null` when saturated
174
+ */
175
+ allocateSequence(): number | null {
176
+ const next = saturatingNext(this.#lastSequence);
177
+ if (next === null) {
178
+ return null;
179
+ }
180
+ return next;
181
+ }
182
+
183
+ /**
184
+ * Stores a pre-built, pre-measured frozen event under the given sequence.
185
+ * Evicts the oldest retained event when the ring is full.
186
+ *
187
+ * @param sequence - The sequence allocated by {@linkcode allocateSequence}
188
+ * @param event - The frozen event to retain
189
+ */
190
+ store(sequence: number, event: DiagnosticsEvent): void {
191
+ this.#slots[(sequence - 1) % EVENT_CAPACITY] = event;
192
+ this.#lastSequence = sequence;
193
+ if (sequence - this.#firstSequence >= EVENT_CAPACITY) {
194
+ this.#firstSequence = sequence - EVENT_CAPACITY + 1;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Returns the event stored under a sequence number, or `undefined` when that
200
+ * sequence was evicted or never allocated.
201
+ *
202
+ * @param sequence - The sequence number to read
203
+ * @returns The frozen event, or `undefined`
204
+ */
205
+ at(sequence: number): DiagnosticsEvent | undefined {
206
+ if (sequence < this.#firstSequence || sequence > this.#lastSequence) {
207
+ return undefined;
208
+ }
209
+ return this.#slots[(sequence - 1) % EVENT_CAPACITY];
210
+ }
211
+
212
+ /**
213
+ * Closes the ring to further writes.
214
+ *
215
+ * Retained events are NOT discarded here — {@linkcode clear} does that —
216
+ * but the collector's terminal shutdown path calls both, and its `read()`
217
+ * short-circuits on a closed ring, so a consumer never drains a tail after
218
+ * shutdown: it gets an empty `closed` batch. Closing and clearing are kept
219
+ * separate so the ring has one reason to change at a time.
220
+ */
221
+ close(): void {
222
+ this.#closed = true;
223
+ }
224
+
225
+ /**
226
+ * Discards every retained event. Used only by the teardown path: a failed
227
+ * startup or a final shutdown must not leave collected metadata recoverable
228
+ * through a read.
229
+ *
230
+ * The sequence counter is deliberately NOT reset: the discarded range is
231
+ * treated exactly like an eviction. A start that failed before any plugin
232
+ * registered can be corrected and retried on the same instance, and restarting at 1 would REUSE sequence
233
+ * numbers a reader already holds — a cursor past the new last sequence would
234
+ * then be refused as "beyond the current sequence", and one inside the new
235
+ * range would silently skip the retry's first events. Keeping the numbering
236
+ * monotonic means the retry's first event is `lastSequence + 1`, and a
237
+ * reader's `lost` counts the discarded records honestly.
238
+ */
239
+ clear(): void {
240
+ this.#slots.fill(undefined);
241
+ this.#firstSequence = this.#lastSequence + 1;
242
+ }
243
+ }