apcore-js 0.1.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 (142) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.gitmessage +60 -0
  3. package/.pre-commit-config.yaml +28 -0
  4. package/CHANGELOG.md +47 -0
  5. package/CLAUDE.md +68 -0
  6. package/README.md +131 -0
  7. package/apcore-logo.svg +79 -0
  8. package/package.json +37 -0
  9. package/planning/acl-system/overview.md +54 -0
  10. package/planning/acl-system/plan.md +92 -0
  11. package/planning/acl-system/state.json +76 -0
  12. package/planning/acl-system/tasks/acl-core.md +226 -0
  13. package/planning/acl-system/tasks/acl-rule.md +92 -0
  14. package/planning/acl-system/tasks/conditional-rules.md +259 -0
  15. package/planning/acl-system/tasks/pattern-matching.md +152 -0
  16. package/planning/acl-system/tasks/yaml-loading.md +271 -0
  17. package/planning/core-executor/overview.md +53 -0
  18. package/planning/core-executor/plan.md +88 -0
  19. package/planning/core-executor/state.json +76 -0
  20. package/planning/core-executor/tasks/async-support.md +106 -0
  21. package/planning/core-executor/tasks/execution-pipeline.md +113 -0
  22. package/planning/core-executor/tasks/redaction.md +85 -0
  23. package/planning/core-executor/tasks/safety-checks.md +65 -0
  24. package/planning/core-executor/tasks/setup.md +75 -0
  25. package/planning/decorator-bindings/overview.md +62 -0
  26. package/planning/decorator-bindings/plan.md +104 -0
  27. package/planning/decorator-bindings/state.json +87 -0
  28. package/planning/decorator-bindings/tasks/binding-directory.md +79 -0
  29. package/planning/decorator-bindings/tasks/binding-loader.md +148 -0
  30. package/planning/decorator-bindings/tasks/explicit-schemas.md +85 -0
  31. package/planning/decorator-bindings/tasks/function-module.md +127 -0
  32. package/planning/decorator-bindings/tasks/module-factory.md +89 -0
  33. package/planning/decorator-bindings/tasks/schema-modes.md +142 -0
  34. package/planning/middleware-system/overview.md +48 -0
  35. package/planning/middleware-system/plan.md +102 -0
  36. package/planning/middleware-system/state.json +65 -0
  37. package/planning/middleware-system/tasks/adapters.md +170 -0
  38. package/planning/middleware-system/tasks/base.md +115 -0
  39. package/planning/middleware-system/tasks/logging-middleware.md +304 -0
  40. package/planning/middleware-system/tasks/manager.md +313 -0
  41. package/planning/observability/overview.md +53 -0
  42. package/planning/observability/plan.md +119 -0
  43. package/planning/observability/state.json +98 -0
  44. package/planning/observability/tasks/context-logger.md +201 -0
  45. package/planning/observability/tasks/exporters.md +121 -0
  46. package/planning/observability/tasks/metrics-collector.md +162 -0
  47. package/planning/observability/tasks/metrics-middleware.md +141 -0
  48. package/planning/observability/tasks/obs-logging-middleware.md +179 -0
  49. package/planning/observability/tasks/span-model.md +120 -0
  50. package/planning/observability/tasks/tracing-middleware.md +179 -0
  51. package/planning/overview.md +81 -0
  52. package/planning/registry-system/overview.md +57 -0
  53. package/planning/registry-system/plan.md +114 -0
  54. package/planning/registry-system/state.json +109 -0
  55. package/planning/registry-system/tasks/dependencies.md +157 -0
  56. package/planning/registry-system/tasks/entry-point.md +148 -0
  57. package/planning/registry-system/tasks/metadata.md +198 -0
  58. package/planning/registry-system/tasks/registry-core.md +323 -0
  59. package/planning/registry-system/tasks/scanner.md +172 -0
  60. package/planning/registry-system/tasks/schema-export.md +261 -0
  61. package/planning/registry-system/tasks/types.md +124 -0
  62. package/planning/registry-system/tasks/validation.md +177 -0
  63. package/planning/schema-system/overview.md +56 -0
  64. package/planning/schema-system/plan.md +121 -0
  65. package/planning/schema-system/state.json +98 -0
  66. package/planning/schema-system/tasks/exporter.md +153 -0
  67. package/planning/schema-system/tasks/loader.md +106 -0
  68. package/planning/schema-system/tasks/ref-resolver.md +133 -0
  69. package/planning/schema-system/tasks/strict-mode.md +140 -0
  70. package/planning/schema-system/tasks/typebox-generation.md +133 -0
  71. package/planning/schema-system/tasks/types-and-annotations.md +160 -0
  72. package/planning/schema-system/tasks/validator.md +149 -0
  73. package/src/acl.ts +188 -0
  74. package/src/bindings.ts +208 -0
  75. package/src/config.ts +24 -0
  76. package/src/context.ts +75 -0
  77. package/src/decorator.ts +110 -0
  78. package/src/errors.ts +369 -0
  79. package/src/executor.ts +348 -0
  80. package/src/index.ts +81 -0
  81. package/src/middleware/adapters.ts +54 -0
  82. package/src/middleware/base.ts +33 -0
  83. package/src/middleware/index.ts +6 -0
  84. package/src/middleware/logging.ts +103 -0
  85. package/src/middleware/manager.ts +105 -0
  86. package/src/module.ts +41 -0
  87. package/src/observability/context-logger.ts +201 -0
  88. package/src/observability/index.ts +4 -0
  89. package/src/observability/metrics.ts +212 -0
  90. package/src/observability/tracing.ts +187 -0
  91. package/src/registry/dependencies.ts +99 -0
  92. package/src/registry/entry-point.ts +64 -0
  93. package/src/registry/index.ts +8 -0
  94. package/src/registry/metadata.ts +111 -0
  95. package/src/registry/registry.ts +314 -0
  96. package/src/registry/scanner.ts +150 -0
  97. package/src/registry/schema-export.ts +177 -0
  98. package/src/registry/types.ts +32 -0
  99. package/src/registry/validation.ts +38 -0
  100. package/src/schema/annotations.ts +67 -0
  101. package/src/schema/exporter.ts +93 -0
  102. package/src/schema/index.ts +14 -0
  103. package/src/schema/loader.ts +270 -0
  104. package/src/schema/ref-resolver.ts +235 -0
  105. package/src/schema/strict.ts +128 -0
  106. package/src/schema/types.ts +73 -0
  107. package/src/schema/validator.ts +82 -0
  108. package/src/utils/index.ts +1 -0
  109. package/src/utils/pattern.ts +30 -0
  110. package/tests/helpers.ts +30 -0
  111. package/tests/integration/test-acl-safety.test.ts +268 -0
  112. package/tests/integration/test-binding-executor.test.ts +194 -0
  113. package/tests/integration/test-e2e-flow.test.ts +117 -0
  114. package/tests/integration/test-error-propagation.test.ts +259 -0
  115. package/tests/integration/test-middleware-chain.test.ts +120 -0
  116. package/tests/integration/test-observability-integration.test.ts +438 -0
  117. package/tests/observability/test-context-logger.test.ts +123 -0
  118. package/tests/observability/test-metrics.test.ts +89 -0
  119. package/tests/observability/test-tracing.test.ts +131 -0
  120. package/tests/registry/test-dependencies.test.ts +70 -0
  121. package/tests/registry/test-entry-point.test.ts +133 -0
  122. package/tests/registry/test-metadata.test.ts +265 -0
  123. package/tests/registry/test-registry.test.ts +140 -0
  124. package/tests/registry/test-scanner.test.ts +257 -0
  125. package/tests/registry/test-schema-export.test.ts +224 -0
  126. package/tests/registry/test-validation.test.ts +75 -0
  127. package/tests/schema/test-loader.test.ts +97 -0
  128. package/tests/schema/test-ref-resolver.test.ts +105 -0
  129. package/tests/schema/test-strict.test.ts +139 -0
  130. package/tests/schema/test-validator.test.ts +64 -0
  131. package/tests/test-acl.test.ts +206 -0
  132. package/tests/test-bindings.test.ts +227 -0
  133. package/tests/test-config.test.ts +76 -0
  134. package/tests/test-context.test.ts +151 -0
  135. package/tests/test-decorator.test.ts +173 -0
  136. package/tests/test-errors.test.ts +204 -0
  137. package/tests/test-executor.test.ts +252 -0
  138. package/tests/test-middleware-manager.test.ts +185 -0
  139. package/tests/test-middleware.test.ts +86 -0
  140. package/tsconfig.build.json +8 -0
  141. package/tsconfig.json +20 -0
  142. package/vitest.config.ts +18 -0
@@ -0,0 +1,304 @@
1
+ # Task: LoggingMiddleware with Logger Interface and Timing
2
+
3
+ ## Goal
4
+
5
+ Implement a `LoggingMiddleware` that provides structured logging for every module call lifecycle phase (before, after, onError). It uses a pluggable `Logger` interface with `info()` and `error()` methods, defaults to `console.log`/`console.error`, and measures execution duration with `performance.now()`. The middleware stores its start timestamp on `context.data['_logging_mw_start']` and computes duration in the `after()` phase. Logging of inputs, outputs, and errors is individually configurable.
6
+
7
+ ## Files Involved
8
+
9
+ | File | Action |
10
+ |------|--------|
11
+ | `src/middleware/logging.ts` | Create -- LoggingMiddleware class and Logger interface |
12
+ | `src/middleware/base.ts` | Read -- Middleware base class (dependency) |
13
+ | `tests/test-middleware.test.ts` | Extend -- Unit tests for logging middleware (or separate test file) |
14
+
15
+ ## Steps (TDD)
16
+
17
+ ### Step 1: Define the Logger interface
18
+
19
+ ```typescript
20
+ export interface Logger {
21
+ info(message: string, extra?: Record<string, unknown>): void;
22
+ error(message: string, extra?: Record<string, unknown>): void;
23
+ }
24
+ ```
25
+
26
+ ### Step 2: Implement the default logger
27
+
28
+ ```typescript
29
+ const defaultLogger: Logger = {
30
+ info(message: string, extra?: Record<string, unknown>) {
31
+ console.log(message, extra ?? '');
32
+ },
33
+ error(message: string, extra?: Record<string, unknown>) {
34
+ console.error(message, extra ?? '');
35
+ },
36
+ };
37
+ ```
38
+
39
+ ### Step 3: Write failing tests for before() logging and timing
40
+
41
+ ```typescript
42
+ import { describe, it, expect, vi } from 'vitest';
43
+ import { LoggingMiddleware, Logger } from '../src/middleware/logging.js';
44
+ import { Context, createIdentity } from '../src/context.js';
45
+
46
+ function makeContext(): Context {
47
+ return Context.create(null, createIdentity('test-user'));
48
+ }
49
+
50
+ describe('LoggingMiddleware', () => {
51
+ it('before() stores start time on context.data and logs START message', () => {
52
+ const logs: Array<{ message: string; extra: Record<string, unknown> }> = [];
53
+ const mockLogger: Logger = {
54
+ info(message, extra) { logs.push({ message, extra: extra ?? {} }); },
55
+ error(message, extra) { logs.push({ message, extra: extra ?? {} }); },
56
+ };
57
+ const mw = new LoggingMiddleware({ logger: mockLogger });
58
+ const ctx = makeContext();
59
+
60
+ const result = mw.before('mod.test', { x: 1 }, ctx);
61
+
62
+ expect(result).toBeNull();
63
+ expect(ctx.data['_logging_mw_start']).toBeTypeOf('number');
64
+ expect(logs).toHaveLength(1);
65
+ expect(logs[0].message).toContain('START mod.test');
66
+ expect(logs[0].extra['moduleId']).toBe('mod.test');
67
+ });
68
+ });
69
+ ```
70
+
71
+ ### Step 4: Implement before() with performance.now() timing
72
+
73
+ ```typescript
74
+ override before(
75
+ moduleId: string,
76
+ inputs: Record<string, unknown>,
77
+ context: Context,
78
+ ): null {
79
+ context.data['_logging_mw_start'] = performance.now();
80
+
81
+ if (this._logInputs) {
82
+ const redacted = context.redactedInputs ?? inputs;
83
+ this._logger.info(`[${context.traceId}] START ${moduleId}`, {
84
+ traceId: context.traceId,
85
+ moduleId,
86
+ callerId: context.callerId,
87
+ inputs: redacted,
88
+ });
89
+ }
90
+
91
+ return null;
92
+ }
93
+ ```
94
+
95
+ Key design choice: `performance.now()` provides sub-millisecond resolution and is available in Node.js 18+ globally. It is preferable to `Date.now()` for duration measurement because it is monotonic and not affected by system clock adjustments.
96
+
97
+ ### Step 5: Write failing tests for after() logging with duration
98
+
99
+ ```typescript
100
+ it('after() logs END message with duration', () => {
101
+ const logs: Array<{ message: string; extra: Record<string, unknown> }> = [];
102
+ const mockLogger: Logger = {
103
+ info(message, extra) { logs.push({ message, extra: extra ?? {} }); },
104
+ error() {},
105
+ };
106
+ const mw = new LoggingMiddleware({ logger: mockLogger });
107
+ const ctx = makeContext();
108
+
109
+ mw.before('mod.test', { x: 1 }, ctx);
110
+ const result = mw.after('mod.test', { x: 1 }, { y: 2 }, ctx);
111
+
112
+ expect(result).toBeNull();
113
+ expect(logs).toHaveLength(2); // START + END
114
+ expect(logs[1].message).toContain('END mod.test');
115
+ expect(logs[1].extra['durationMs']).toBeTypeOf('number');
116
+ });
117
+ ```
118
+
119
+ ### Step 6: Implement after() with duration calculation
120
+
121
+ ```typescript
122
+ override after(
123
+ moduleId: string,
124
+ _inputs: Record<string, unknown>,
125
+ output: Record<string, unknown>,
126
+ context: Context,
127
+ ): null {
128
+ const startTime = (context.data['_logging_mw_start'] as number) ?? performance.now();
129
+ const durationMs = performance.now() - startTime;
130
+
131
+ if (this._logOutputs) {
132
+ this._logger.info(
133
+ `[${context.traceId}] END ${moduleId} (${durationMs.toFixed(2)}ms)`,
134
+ {
135
+ traceId: context.traceId,
136
+ moduleId,
137
+ durationMs,
138
+ output,
139
+ },
140
+ );
141
+ }
142
+
143
+ return null;
144
+ }
145
+ ```
146
+
147
+ ### Step 7: Write failing tests for onError() logging
148
+
149
+ ```typescript
150
+ it('onError() logs ERROR message with redacted inputs', () => {
151
+ const logs: Array<{ message: string; extra: Record<string, unknown> }> = [];
152
+ const mockLogger: Logger = {
153
+ info() {},
154
+ error(message, extra) { logs.push({ message, extra: extra ?? {} }); },
155
+ };
156
+ const mw = new LoggingMiddleware({ logger: mockLogger });
157
+ const ctx = makeContext();
158
+
159
+ const result = mw.onError('mod.test', { x: 1 }, new Error('boom'), ctx);
160
+
161
+ expect(result).toBeNull();
162
+ expect(logs).toHaveLength(1);
163
+ expect(logs[0].message).toContain('ERROR mod.test');
164
+ expect(logs[0].extra['error']).toBe('Error: boom');
165
+ });
166
+ ```
167
+
168
+ ### Step 8: Implement onError()
169
+
170
+ ```typescript
171
+ override onError(
172
+ moduleId: string,
173
+ inputs: Record<string, unknown>,
174
+ error: Error,
175
+ context: Context,
176
+ ): null {
177
+ if (this._logErrors) {
178
+ const redacted = context.redactedInputs ?? inputs;
179
+ this._logger.error(`[${context.traceId}] ERROR ${moduleId}: ${error}`, {
180
+ traceId: context.traceId,
181
+ moduleId,
182
+ error: String(error),
183
+ inputs: redacted,
184
+ });
185
+ }
186
+
187
+ return null;
188
+ }
189
+ ```
190
+
191
+ ### Step 9: Write tests for configuration flags
192
+
193
+ ```typescript
194
+ it('respects logInputs=false', () => {
195
+ const logs: string[] = [];
196
+ const mockLogger: Logger = {
197
+ info(msg) { logs.push(msg); },
198
+ error() {},
199
+ };
200
+ const mw = new LoggingMiddleware({ logger: mockLogger, logInputs: false });
201
+ const ctx = makeContext();
202
+ mw.before('mod.test', { x: 1 }, ctx);
203
+ expect(logs).toHaveLength(0);
204
+ // But timing is still stored
205
+ expect(ctx.data['_logging_mw_start']).toBeTypeOf('number');
206
+ });
207
+
208
+ it('respects logOutputs=false', () => {
209
+ const logs: string[] = [];
210
+ const mockLogger: Logger = {
211
+ info(msg) { logs.push(msg); },
212
+ error() {},
213
+ };
214
+ const mw = new LoggingMiddleware({ logger: mockLogger, logOutputs: false });
215
+ const ctx = makeContext();
216
+ mw.before('mod.test', {}, ctx);
217
+ mw.after('mod.test', {}, { y: 1 }, ctx);
218
+ expect(logs).toHaveLength(1); // Only START, no END
219
+ });
220
+
221
+ it('respects logErrors=false', () => {
222
+ const logs: string[] = [];
223
+ const mockLogger: Logger = {
224
+ info() {},
225
+ error(msg) { logs.push(msg); },
226
+ };
227
+ const mw = new LoggingMiddleware({ logger: mockLogger, logErrors: false });
228
+ const ctx = makeContext();
229
+ mw.onError('mod.test', {}, new Error('boom'), ctx);
230
+ expect(logs).toHaveLength(0);
231
+ });
232
+ ```
233
+
234
+ ### Step 10: Implement constructor with options
235
+
236
+ ```typescript
237
+ export class LoggingMiddleware extends Middleware {
238
+ private _logger: Logger;
239
+ private _logInputs: boolean;
240
+ private _logOutputs: boolean;
241
+ private _logErrors: boolean;
242
+
243
+ constructor(options?: {
244
+ logger?: Logger;
245
+ logInputs?: boolean;
246
+ logOutputs?: boolean;
247
+ logErrors?: boolean;
248
+ }) {
249
+ super();
250
+ this._logger = options?.logger ?? defaultLogger;
251
+ this._logInputs = options?.logInputs ?? true;
252
+ this._logOutputs = options?.logOutputs ?? true;
253
+ this._logErrors = options?.logErrors ?? true;
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### Step 11: Write test for redactedInputs usage
259
+
260
+ ```typescript
261
+ it('uses context.redactedInputs when available', () => {
262
+ const logs: Array<{ extra: Record<string, unknown> }> = [];
263
+ const mockLogger: Logger = {
264
+ info(_msg, extra) { logs.push({ extra: extra ?? {} }); },
265
+ error() {},
266
+ };
267
+ const mw = new LoggingMiddleware({ logger: mockLogger });
268
+ const ctx = makeContext();
269
+ (ctx as any).redactedInputs = { x: '***' };
270
+
271
+ mw.before('mod.test', { x: 'secret' }, ctx);
272
+
273
+ expect(logs[0].extra['inputs']).toEqual({ x: '***' });
274
+ });
275
+ ```
276
+
277
+ ### Step 12: Run all tests and confirm green
278
+
279
+ ```bash
280
+ npx vitest run tests/test-middleware.test.ts
281
+ ```
282
+
283
+ ## Acceptance Criteria
284
+
285
+ - [x] `Logger` interface is exported with `info(message, extra?)` and `error(message, extra?)` methods
286
+ - [x] Default logger delegates to `console.log` and `console.error`
287
+ - [x] `LoggingMiddleware` extends `Middleware`
288
+ - [x] `before()` stores `performance.now()` on `context.data['_logging_mw_start']` and logs START
289
+ - [x] `after()` computes duration from stored start time and logs END with `durationMs`
290
+ - [x] `onError()` logs ERROR with trace ID, module ID, and stringified error
291
+ - [x] Uses `context.redactedInputs` when available instead of raw inputs
292
+ - [x] `logInputs`, `logOutputs`, `logErrors` flags default to `true` and are individually configurable
293
+ - [x] All hooks return `null` (logging middleware does not transform inputs/outputs)
294
+ - [x] Duration is formatted with `.toFixed(2)` in the log message
295
+ - [x] All tests pass with `vitest`
296
+
297
+ ## Dependencies
298
+
299
+ - `src/middleware/base.ts` -- `Middleware` base class (task: base)
300
+ - `src/context.ts` -- `Context` class with `traceId`, `callerId`, `data`, `redactedInputs` properties
301
+
302
+ ## Estimated Time
303
+
304
+ 2 hours
@@ -0,0 +1,313 @@
1
+ # Task: MiddlewareManager with Onion-Model Execution
2
+
3
+ ## Goal
4
+
5
+ Implement the `MiddlewareManager` class that manages middleware registration and executes the onion-model pipeline. The manager provides `add()`, `remove()` (by reference identity), and `snapshot()` for list management. It exposes `executeBefore()` (forward order), `executeAfter()` (reverse order), and `executeOnError()` (reverse over executed subset, first non-null recovery wins). Also implement `MiddlewareChainError` extending `ModuleError` to wrap failures during the `before()` phase with tracking of which middlewares have already executed.
6
+
7
+ ## Files Involved
8
+
9
+ | File | Action |
10
+ |------|--------|
11
+ | `src/middleware/manager.ts` | Create -- MiddlewareManager and MiddlewareChainError |
12
+ | `src/errors.ts` | Read -- ModuleError base class (dependency) |
13
+ | `tests/test-middleware-manager.test.ts` | Create -- Unit tests for manager and chain error |
14
+
15
+ ## Steps (TDD)
16
+
17
+ ### Step 1: Write failing tests for add/remove/snapshot
18
+
19
+ ```typescript
20
+ import { describe, it, expect } from 'vitest';
21
+ import { Middleware } from '../src/middleware/base.js';
22
+ import { MiddlewareManager } from '../src/middleware/manager.js';
23
+
24
+ describe('MiddlewareManager', () => {
25
+ it('starts empty', () => {
26
+ const mgr = new MiddlewareManager();
27
+ expect(mgr.snapshot()).toEqual([]);
28
+ });
29
+
30
+ it('add and snapshot', () => {
31
+ const mgr = new MiddlewareManager();
32
+ mgr.add(new Middleware());
33
+ mgr.add(new Middleware());
34
+ expect(mgr.snapshot()).toHaveLength(2);
35
+ });
36
+
37
+ it('snapshot returns a copy', () => {
38
+ const mgr = new MiddlewareManager();
39
+ mgr.add(new Middleware());
40
+ const snap = mgr.snapshot();
41
+ snap.pop();
42
+ expect(mgr.snapshot()).toHaveLength(1);
43
+ });
44
+
45
+ it('remove by identity', () => {
46
+ const mgr = new MiddlewareManager();
47
+ const mw1 = new Middleware();
48
+ const mw2 = new Middleware();
49
+ mgr.add(mw1);
50
+ mgr.add(mw2);
51
+ expect(mgr.remove(mw1)).toBe(true);
52
+ expect(mgr.snapshot()).toEqual([mw2]);
53
+ });
54
+
55
+ it('remove returns false when not found', () => {
56
+ const mgr = new MiddlewareManager();
57
+ expect(mgr.remove(new Middleware())).toBe(false);
58
+ });
59
+ });
60
+ ```
61
+
62
+ ### Step 2: Implement add, remove, snapshot
63
+
64
+ ```typescript
65
+ import { Middleware } from './base.js';
66
+
67
+ export class MiddlewareManager {
68
+ private _middlewares: Middleware[] = [];
69
+
70
+ add(middleware: Middleware): void {
71
+ this._middlewares.push(middleware);
72
+ }
73
+
74
+ remove(middleware: Middleware): boolean {
75
+ for (let i = 0; i < this._middlewares.length; i++) {
76
+ if (this._middlewares[i] === middleware) {
77
+ this._middlewares.splice(i, 1);
78
+ return true;
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+
84
+ snapshot(): Middleware[] {
85
+ return [...this._middlewares];
86
+ }
87
+ }
88
+ ```
89
+
90
+ No thread locking is needed -- Node.js is single-threaded. The Python implementation uses `threading.Lock`; in TypeScript, `snapshot()` returning a shallow copy is sufficient to guard against mutations during iteration.
91
+
92
+ ### Step 3: Write failing tests for executeBefore (forward order)
93
+
94
+ ```typescript
95
+ it('executeBefore runs in forward order', () => {
96
+ const mgr = new MiddlewareManager();
97
+ mgr.add(new TaggingMiddleware('A'));
98
+ mgr.add(new TaggingMiddleware('B'));
99
+ mgr.add(new TaggingMiddleware('C'));
100
+ const ctx = makeContext();
101
+ const [result, executed] = mgr.executeBefore('mod.test', { trail: '' }, ctx);
102
+ expect(result['trail']).toBe('ABC');
103
+ expect(executed).toHaveLength(3);
104
+ });
105
+
106
+ it('executeBefore passes original inputs when all return null', () => {
107
+ const mgr = new MiddlewareManager();
108
+ mgr.add(new Middleware());
109
+ const ctx = makeContext();
110
+ const [result] = mgr.executeBefore('mod.test', { x: 42 }, ctx);
111
+ expect(result).toEqual({ x: 42 });
112
+ });
113
+ ```
114
+
115
+ ### Step 4: Implement executeBefore
116
+
117
+ ```typescript
118
+ executeBefore(
119
+ moduleId: string,
120
+ inputs: Record<string, unknown>,
121
+ context: Context,
122
+ ): [Record<string, unknown>, Middleware[]] {
123
+ let currentInputs = inputs;
124
+ const executedMiddlewares: Middleware[] = [];
125
+ const middlewares = this.snapshot();
126
+
127
+ for (const mw of middlewares) {
128
+ executedMiddlewares.push(mw);
129
+ try {
130
+ const result = mw.before(moduleId, currentInputs, context);
131
+ if (result !== null) {
132
+ currentInputs = result;
133
+ }
134
+ } catch (e) {
135
+ throw new MiddlewareChainError(e as Error, executedMiddlewares);
136
+ }
137
+ }
138
+
139
+ return [currentInputs, executedMiddlewares];
140
+ }
141
+ ```
142
+
143
+ ### Step 5: Write failing tests for executeAfter (reverse order)
144
+
145
+ ```typescript
146
+ it('executeAfter runs in reverse order', () => {
147
+ const mgr = new MiddlewareManager();
148
+ mgr.add(new TaggingMiddleware('A'));
149
+ mgr.add(new TaggingMiddleware('B'));
150
+ mgr.add(new TaggingMiddleware('C'));
151
+ const ctx = makeContext();
152
+ const result = mgr.executeAfter('mod.test', {}, { trail: '' }, ctx);
153
+ expect(result['trail']).toBe('CBA');
154
+ });
155
+ ```
156
+
157
+ ### Step 6: Implement executeAfter
158
+
159
+ ```typescript
160
+ executeAfter(
161
+ moduleId: string,
162
+ inputs: Record<string, unknown>,
163
+ output: Record<string, unknown>,
164
+ context: Context,
165
+ ): Record<string, unknown> {
166
+ let currentOutput = output;
167
+ const middlewares = this.snapshot();
168
+
169
+ for (let i = middlewares.length - 1; i >= 0; i--) {
170
+ const result = middlewares[i].after(moduleId, inputs, currentOutput, context);
171
+ if (result !== null) {
172
+ currentOutput = result;
173
+ }
174
+ }
175
+
176
+ return currentOutput;
177
+ }
178
+ ```
179
+
180
+ ### Step 7: Write failing tests for executeOnError (reverse, first recovery wins, errors swallowed)
181
+
182
+ ```typescript
183
+ it('executeOnError returns first non-null recovery (reverse order)', () => {
184
+ const mgr = new MiddlewareManager();
185
+ const mwA = new RecoveringMiddleware({ recovered: 'A' });
186
+ const mwB = new RecoveringMiddleware({ recovered: 'B' });
187
+ mgr.add(mwA);
188
+ mgr.add(mwB);
189
+ const ctx = makeContext();
190
+ const result = mgr.executeOnError('mod.test', {}, new Error('oops'), ctx, [mwA, mwB]);
191
+ expect(result).toEqual({ recovered: 'B' });
192
+ });
193
+
194
+ it('executeOnError returns null when no recovery', () => {
195
+ const mgr = new MiddlewareManager();
196
+ const mw = new Middleware();
197
+ mgr.add(mw);
198
+ const ctx = makeContext();
199
+ const result = mgr.executeOnError('mod.test', {}, new Error('oops'), ctx, [mw]);
200
+ expect(result).toBeNull();
201
+ });
202
+
203
+ it('executeOnError swallows errors in onError handlers', () => {
204
+ // ThrowingOnError middleware throws inside onError()
205
+ // RecoveringMiddleware before it should still provide recovery
206
+ const result = mgr.executeOnError('mod.test', {}, new Error('original'), ctx, [mwRecover, mwThrow]);
207
+ expect(result).toEqual({ safe: true });
208
+ });
209
+ ```
210
+
211
+ ### Step 8: Implement executeOnError
212
+
213
+ ```typescript
214
+ executeOnError(
215
+ moduleId: string,
216
+ inputs: Record<string, unknown>,
217
+ error: Error,
218
+ context: Context,
219
+ executedMiddlewares: Middleware[],
220
+ ): Record<string, unknown> | null {
221
+ for (let i = executedMiddlewares.length - 1; i >= 0; i--) {
222
+ try {
223
+ const result = executedMiddlewares[i].onError(moduleId, inputs, error, context);
224
+ if (result !== null) {
225
+ return result;
226
+ }
227
+ } catch {
228
+ // Swallow errors in onError handlers
229
+ continue;
230
+ }
231
+ }
232
+ return null;
233
+ }
234
+ ```
235
+
236
+ ### Step 9: Write failing test for MiddlewareChainError
237
+
238
+ ```typescript
239
+ it('MiddlewareChainError wraps before() failure', () => {
240
+ class FailingBefore extends Middleware {
241
+ override before(): Record<string, unknown> | null {
242
+ throw new Error('before exploded');
243
+ }
244
+ }
245
+ const mgr = new MiddlewareManager();
246
+ mgr.add(new TaggingMiddleware('A'));
247
+ mgr.add(new FailingBefore());
248
+ const ctx = makeContext();
249
+
250
+ let caught: MiddlewareChainError | undefined;
251
+ try {
252
+ mgr.executeBefore('mod.test', { trail: '' }, ctx);
253
+ } catch (e) {
254
+ caught = e as MiddlewareChainError;
255
+ }
256
+
257
+ expect(caught).toBeInstanceOf(MiddlewareChainError);
258
+ expect(caught!.original.message).toBe('before exploded');
259
+ expect(caught!.executedMiddlewares).toHaveLength(2);
260
+ });
261
+ ```
262
+
263
+ ### Step 10: Implement MiddlewareChainError
264
+
265
+ ```typescript
266
+ import { ModuleError } from '../errors.js';
267
+
268
+ export class MiddlewareChainError extends ModuleError {
269
+ readonly original: Error;
270
+ readonly executedMiddlewares: Middleware[];
271
+
272
+ constructor(original: Error, executedMiddlewares: Middleware[]) {
273
+ super('MIDDLEWARE_CHAIN_ERROR', String(original));
274
+ this.name = 'MiddlewareChainError';
275
+ this.original = original;
276
+ this.executedMiddlewares = executedMiddlewares;
277
+ }
278
+ }
279
+ ```
280
+
281
+ Key difference from Python: extends `ModuleError` (which extends `Error`), not `Exception`. This integrates with the framework's structured error hierarchy providing `code`, `details`, and `timestamp` properties.
282
+
283
+ ### Step 11: Run all tests and confirm green
284
+
285
+ ```bash
286
+ npx vitest run tests/test-middleware-manager.test.ts
287
+ ```
288
+
289
+ ## Acceptance Criteria
290
+
291
+ - [x] `MiddlewareManager` is exported from `src/middleware/manager.ts`
292
+ - [x] `add()` appends a middleware to the internal list
293
+ - [x] `remove()` removes by strict reference identity (`===`) and returns `boolean`
294
+ - [x] `snapshot()` returns a shallow copy; mutations to the copy do not affect the internal list
295
+ - [x] `executeBefore()` runs in forward registration order, returns `[transformedInputs, executedMiddlewares]`
296
+ - [x] `executeBefore()` wraps middleware errors in `MiddlewareChainError`
297
+ - [x] `executeAfter()` runs in reverse registration order
298
+ - [x] `executeOnError()` runs in reverse over the executed middlewares subset
299
+ - [x] `executeOnError()` returns the first non-null recovery value
300
+ - [x] `executeOnError()` swallows errors thrown by `onError()` handlers
301
+ - [x] `MiddlewareChainError` extends `ModuleError` with `original` and `executedMiddlewares` properties
302
+ - [x] No thread locking is used (Node.js single-threaded model)
303
+ - [x] All tests pass with `vitest`
304
+
305
+ ## Dependencies
306
+
307
+ - `src/middleware/base.ts` -- `Middleware` base class (task: base)
308
+ - `src/errors.ts` -- `ModuleError` base error class
309
+ - `src/context.ts` -- `Context` class (parameter type)
310
+
311
+ ## Estimated Time
312
+
313
+ 3 hours
@@ -0,0 +1,53 @@
1
+ # Feature: Observability
2
+
3
+ ## Overview
4
+
5
+ The Observability module provides the three pillars of runtime visibility for apcore: distributed tracing, metrics collection, and structured logging. All three integrate into the executor's middleware pipeline as composable `Middleware` subclasses that use stack-based state in the shared `context.data` record to correctly handle nested module-to-module calls. Tracing produces `Span` objects exported through pluggable `SpanExporter` implementations. Metrics are collected via `MetricsCollector` with Prometheus-compatible text export. Logging is handled by `ContextLogger` with JSON and text output formats, automatic `_secret_`-prefixed key redaction, and a `fromContext()` factory that binds trace/module/caller metadata.
6
+
7
+ ## Scope
8
+
9
+ ### Included
10
+
11
+ - `Span` interface and `createSpan()` factory function for trace span creation
12
+ - `SpanExporter` interface with `StdoutExporter` (JSON.stringify to stdout) and `InMemoryExporter` (bounded array with shift() eviction)
13
+ - `TracingMiddleware` with stack-based span management and 4 sampling strategies: `full`, `proportional`, `error_first`, `off`
14
+ - `MetricsCollector` with counter and histogram support, string-encoded composite keys (`name|key1=val1,key2=val2`), Prometheus text format export, and convenience methods (`incrementCalls`, `incrementErrors`, `observeDuration`)
15
+ - `MetricsMiddleware` with stack-based `performance.now()` timing and automatic call/error/duration recording
16
+ - `ContextLogger` with JSON and text output formats, level filtering (trace/debug/info/warn/error/fatal), `_secret_`-prefixed key redaction, and `fromContext()` static factory
17
+ - `ObsLoggingMiddleware` with stack-based timing, configurable input/output logging, and start/complete/error structured log events
18
+
19
+ ### Excluded
20
+
21
+ - `OTLPExporter` for OpenTelemetry Protocol export (not yet implemented -- noted as a gap for future work)
22
+ - OpenTelemetry SDK bridge or direct OTel integration
23
+ - Persistent metric storage or time-series database integration
24
+ - Log file rotation or external log shipping
25
+ - Dashboard or alerting configuration
26
+
27
+ ## Technology Stack
28
+
29
+ - **TypeScript 5.5+** with strict mode
30
+ - **Node.js >= 18.0.0** with ES Module support (`performance.now()`, `node:crypto`)
31
+ - **vitest** for unit testing
32
+
33
+ ## Task Execution Order
34
+
35
+ | # | Task File | Description | Status |
36
+ |---|-----------|-------------|--------|
37
+ | 1 | [span-model](./tasks/span-model.md) | Span interface and createSpan() factory, SpanExporter interface | completed |
38
+ | 2 | [exporters](./tasks/exporters.md) | StdoutExporter and InMemoryExporter implementations | completed |
39
+ | 3 | [tracing-middleware](./tasks/tracing-middleware.md) | TracingMiddleware with stack-based spans and sampling strategies | completed |
40
+ | 4 | [metrics-collector](./tasks/metrics-collector.md) | MetricsCollector with counters, histograms, and Prometheus export | completed |
41
+ | 5 | [metrics-middleware](./tasks/metrics-middleware.md) | MetricsMiddleware with stack-based performance.now() timing | completed |
42
+ | 6 | [context-logger](./tasks/context-logger.md) | ContextLogger with JSON/text formats, redaction, fromContext() | completed |
43
+ | 7 | [obs-logging-middleware](./tasks/obs-logging-middleware.md) | ObsLoggingMiddleware with stack-based timing and structured logs | completed |
44
+
45
+ ## Progress
46
+
47
+ | Total | Completed | In Progress | Pending |
48
+ |-------|-----------|-------------|---------|
49
+ | 7 | 7 | 0 | 0 |
50
+
51
+ ## Reference Documents
52
+
53
+ - [Observability Feature Specification](../../features/observability.md)