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,142 @@
1
+ # Task: Schema Resolution Modes: Inline, schema_ref, Permissive Fallback
2
+
3
+ ## Goal
4
+
5
+ Implement the three schema resolution modes used by `BindingLoader._createModuleFromBinding()` when constructing `FunctionModule` instances from YAML binding entries. Each binding entry can specify schemas in one of three ways: inline `input_schema`/`output_schema` JSON Schema objects, an external `schema_ref` file path, or no schema at all (permissive fallback). This task also covers the `jsonSchemaToTypeBox()` integration and notes the dead `JSON_SCHEMA_TYPE_MAP` code that should be cleaned up.
6
+
7
+ ## Files Involved
8
+
9
+ - `src/bindings.ts` -- `_createModuleFromBinding()` schema resolution logic, `buildSchemaFromJsonSchema()` wrapper, dead `JSON_SCHEMA_TYPE_MAP`
10
+ - `src/schema/loader.ts` -- `jsonSchemaToTypeBox()` consumed for JSON Schema to TypeBox conversion
11
+ - `tests/test-bindings.test.ts` -- Tests for each schema resolution mode
12
+
13
+ ## Steps
14
+
15
+ ### 1. Write failing tests (TDD)
16
+
17
+ Create tests for:
18
+ - **Inline input_schema/output_schema**: Binding with inline JSON Schema objects produces FunctionModule with correct TypeBox schemas
19
+ - **Only input_schema present**: Missing `output_schema` defaults to empty object schema `{}`
20
+ - **Only output_schema present**: Missing `input_schema` defaults to empty object schema `{}`
21
+ - **schema_ref mode**: Binding with `schema_ref: ./schemas/my-module.yaml` loads schemas from the referenced file
22
+ - **schema_ref file not found**: Throws `BindingFileInvalidError` when referenced file does not exist
23
+ - **schema_ref invalid YAML**: Throws `BindingFileInvalidError` on malformed YAML in referenced file
24
+ - **Permissive fallback**: Binding with no schema keys produces permissive `Type.Record(Type.String(), Type.Unknown())` for both input and output
25
+ - **Schema priority**: `input_schema`/`output_schema` takes precedence over `schema_ref` when both present
26
+
27
+ ### 2. Implement schema resolution in _createModuleFromBinding()
28
+
29
+ ```typescript
30
+ private async _createModuleFromBinding(
31
+ binding: Record<string, unknown>,
32
+ bindingFileDir: string,
33
+ ): Promise<FunctionModule> {
34
+ const func = await this.resolveTarget(binding['target'] as string);
35
+ const moduleId = binding['module_id'] as string;
36
+
37
+ let inputSchema: TSchema;
38
+ let outputSchema: TSchema;
39
+
40
+ if ('input_schema' in binding || 'output_schema' in binding) {
41
+ // Mode 1: Inline JSON Schema
42
+ const inputSchemaDict = (binding['input_schema'] as Record<string, unknown>) ?? {};
43
+ const outputSchemaDict = (binding['output_schema'] as Record<string, unknown>) ?? {};
44
+ inputSchema = buildSchemaFromJsonSchema(inputSchemaDict);
45
+ outputSchema = buildSchemaFromJsonSchema(outputSchemaDict);
46
+ } else if ('schema_ref' in binding) {
47
+ // Mode 2: External schema reference file
48
+ const refPath = resolve(bindingFileDir, binding['schema_ref'] as string);
49
+ if (!existsSync(refPath)) {
50
+ throw new BindingFileInvalidError(refPath, 'Schema reference file not found');
51
+ }
52
+ let refData: Record<string, unknown>;
53
+ try {
54
+ refData = (yaml.load(readFileSync(refPath, 'utf-8')) as Record<string, unknown>) ?? {};
55
+ } catch (e) {
56
+ throw new BindingFileInvalidError(refPath, `YAML parse error: ${e}`);
57
+ }
58
+ inputSchema = buildSchemaFromJsonSchema(
59
+ (refData['input_schema'] as Record<string, unknown>) ?? {},
60
+ );
61
+ outputSchema = buildSchemaFromJsonSchema(
62
+ (refData['output_schema'] as Record<string, unknown>) ?? {},
63
+ );
64
+ } else {
65
+ // Mode 3: Permissive fallback -- no schema specified
66
+ inputSchema = Type.Record(Type.String(), Type.Unknown());
67
+ outputSchema = Type.Record(Type.String(), Type.Unknown());
68
+ }
69
+
70
+ return new FunctionModule({
71
+ execute: async (inputs, context) => {
72
+ const result = await func(inputs, context);
73
+ if (result === null || result === undefined) return {};
74
+ if (typeof result === 'object' && !Array.isArray(result))
75
+ return result as Record<string, unknown>;
76
+ return { result };
77
+ },
78
+ moduleId,
79
+ inputSchema,
80
+ outputSchema,
81
+ description: (binding['description'] as string) ?? undefined,
82
+ tags: (binding['tags'] as string[]) ?? null,
83
+ version: (binding['version'] as string) ?? '1.0.0',
84
+ });
85
+ }
86
+ ```
87
+
88
+ ### 3. Note buildSchemaFromJsonSchema() wrapper and dead code
89
+
90
+ The `buildSchemaFromJsonSchema()` function is a thin wrapper around `jsonSchemaToTypeBox()`:
91
+
92
+ ```typescript
93
+ function buildSchemaFromJsonSchema(schema: Record<string, unknown>): TSchema {
94
+ return jsonSchemaToTypeBox(schema);
95
+ }
96
+ ```
97
+
98
+ The `JSON_SCHEMA_TYPE_MAP` constant defined at the top of `bindings.ts` is dead code. It was part of an earlier implementation before `jsonSchemaToTypeBox()` was extracted to the schema system. It maps JSON Schema type strings to TypeBox constructors but is never referenced. This should be removed in a cleanup pass.
99
+
100
+ ```typescript
101
+ // DEAD CODE -- cleanup needed
102
+ const JSON_SCHEMA_TYPE_MAP: Record<string, () => TSchema> = {
103
+ string: () => Type.String(),
104
+ integer: () => Type.Integer(),
105
+ number: () => Type.Number(),
106
+ boolean: () => Type.Boolean(),
107
+ array: () => Type.Array(Type.Unknown()),
108
+ object: () => Type.Record(Type.String(), Type.Unknown()),
109
+ };
110
+ ```
111
+
112
+ ### 4. Document the three modes
113
+
114
+ | Mode | Trigger | Behavior |
115
+ |------|---------|----------|
116
+ | Inline | `input_schema` or `output_schema` key present | Parse inline JSON Schema objects via `jsonSchemaToTypeBox()`. Missing schema defaults to `{}` (empty object). |
117
+ | schema_ref | `schema_ref` key present (no inline schemas) | Load external YAML file relative to binding file directory. Parse `input_schema` and `output_schema` from it. |
118
+ | Permissive | No schema keys present | Use `Type.Record(Type.String(), Type.Unknown())` for both input and output. Accepts any key-value pairs. |
119
+
120
+ ### 5. Verify tests pass
121
+
122
+ Run `npx vitest run tests/test-bindings.test.ts`.
123
+
124
+ ## Acceptance Criteria
125
+
126
+ - [x] Inline mode: `input_schema`/`output_schema` JSON Schema objects are converted to TypeBox via `jsonSchemaToTypeBox()`
127
+ - [x] Inline mode: Missing input or output schema defaults to empty `{}` (produces `Type.Object({})` equivalent)
128
+ - [x] schema_ref mode: External YAML file is loaded relative to the binding file's directory
129
+ - [x] schema_ref mode: `BindingFileInvalidError` thrown when reference file is missing or unparseable
130
+ - [x] Permissive mode: No schema keys produces `Type.Record(Type.String(), Type.Unknown())` for both schemas
131
+ - [x] Inline mode takes precedence over schema_ref when both are present
132
+ - [x] `buildSchemaFromJsonSchema()` delegates to `jsonSchemaToTypeBox()` from schema system
133
+ - [x] Dead `JSON_SCHEMA_TYPE_MAP` code is identified and documented for cleanup
134
+
135
+ ## Dependencies
136
+
137
+ - `binding-loader` -- Requires `BindingLoader` class with `resolveTarget()` and `loadBindings()` structure
138
+ - `schema-system` (external) -- Consumes `jsonSchemaToTypeBox()` for JSON Schema to TypeBox conversion
139
+
140
+ ## Estimated Time
141
+
142
+ 3 hours
@@ -0,0 +1,48 @@
1
+ # Feature: Middleware System
2
+
3
+ ## Overview
4
+
5
+ The Middleware System provides a composable onion-model pipeline for intercepting module calls in apcore. Each middleware can hook into three lifecycle phases -- `before()` (pre-execution input transformation), `after()` (post-execution output transformation), and `onError()` (error recovery). The `MiddlewareManager` orchestrates execution in forward order for `before`, reverse order for `after` and `onError`, and wraps middleware failures in `MiddlewareChainError`. Function adapter classes (`BeforeMiddleware`, `AfterMiddleware`) allow lightweight callback-based middleware without subclassing, and a built-in `LoggingMiddleware` provides structured call tracing with `performance.now()` timing.
6
+
7
+ ## Scope
8
+
9
+ ### Included
10
+
11
+ - `Middleware` base class with no-op `before()`, `after()`, `onError()` lifecycle hooks
12
+ - `MiddlewareManager` with `add()`, `remove()`, `snapshot()`, and onion-model `executeBefore()` / `executeAfter()` / `executeOnError()` methods
13
+ - `MiddlewareChainError` extending `ModuleError` for wrapping middleware-phase failures with executed-middleware tracking
14
+ - `BeforeMiddleware` and `AfterMiddleware` adapter classes with `BeforeCallback` and `AfterCallback` type aliases
15
+ - `LoggingMiddleware` with pluggable `Logger` interface, configurable input/output/error logging, and `performance.now()` duration measurement
16
+ - Barrel export via `middleware/index.ts`
17
+
18
+ ### Excluded
19
+
20
+ - Executor integration (consumed by `core-executor` module)
21
+ - Observability middleware (tracing, metrics -- implemented in `observability` module)
22
+ - Async middleware support (all hooks are synchronous by design)
23
+
24
+ ## Technology Stack
25
+
26
+ - **TypeScript 5.5+** with strict mode
27
+ - **Node.js >= 18.0.0** with ES Module support
28
+ - **vitest** for unit testing
29
+
30
+ ## Task Execution Order
31
+
32
+ | # | Task File | Description | Status |
33
+ |---|-----------|-------------|--------|
34
+ | 1 | [base](./tasks/base.md) | Middleware base class with no-op lifecycle hooks | completed |
35
+ | 2 | [manager](./tasks/manager.md) | MiddlewareManager with onion-model execution and MiddlewareChainError | completed |
36
+ | 3 | [adapters](./tasks/adapters.md) | BeforeMiddleware and AfterMiddleware adapter classes with callback types | completed |
37
+ | 4 | [logging-middleware](./tasks/logging-middleware.md) | LoggingMiddleware with Logger interface and performance.now() timing | completed |
38
+
39
+ ## Progress
40
+
41
+ | Total | Completed | In Progress | Pending |
42
+ |-------|-----------|-------------|---------|
43
+ | 4 | 4 | 0 | 0 |
44
+
45
+ ## Reference Documents
46
+
47
+ - [Implementation Plan](./plan.md)
48
+ - [Project Overview](../overview.md)
@@ -0,0 +1,102 @@
1
+ # Implementation Plan: Middleware System
2
+
3
+ ## Goal
4
+
5
+ Implement a composable onion-model middleware pipeline that allows interception, transformation, and error recovery at each phase of a module call. The system provides a base class for subclassing, function adapters for lightweight usage, a manager for orchestration, and a built-in logging middleware with high-resolution timing.
6
+
7
+ ## Architecture Design
8
+
9
+ ### Component Structure
10
+
11
+ - **Middleware** (`middleware/base.ts`, ~30 lines) -- Abstract base class with three no-op lifecycle hooks: `before(moduleId, inputs, context)`, `after(moduleId, inputs, output, context)`, and `onError(moduleId, inputs, error, context)`. Each returns `Record<string, unknown> | null`; returning `null` signals "no transformation". Imports `Context` from the core context module.
12
+
13
+ - **MiddlewareManager** (`middleware/manager.ts`, ~100 lines) -- Onion-model execution engine. Maintains an internal `_middlewares` array with `add()`, `remove()` (by reference identity), and `snapshot()` (shallow copy). Exposes `executeBefore()` (forward order, returns `[transformedInputs, executedMiddlewares]` tuple), `executeAfter()` (reverse order), and `executeOnError()` (reverse over executed middlewares, first non-null recovery wins, errors in handlers are swallowed).
14
+
15
+ - **MiddlewareChainError** (`middleware/manager.ts`) -- Error subclass extending `ModuleError` (not `Exception` as in Python). Captures the `original` error and the `executedMiddlewares` array at the point of failure, enabling the executor to run `onError()` recovery on the correct subset of middlewares.
16
+
17
+ - **BeforeMiddleware / AfterMiddleware** (`middleware/adapters.ts`, ~55 lines) -- Adapter classes that wrap a single callback function (`BeforeCallback` or `AfterCallback` type alias) and delegate to the corresponding lifecycle hook. All other hooks remain no-op from the base class. Enables functional middleware patterns without subclassing.
18
+
19
+ - **LoggingMiddleware** (`middleware/logging.ts`, ~100 lines) -- Concrete middleware that logs structured messages at each phase via a pluggable `Logger` interface (`info` and `error` methods). Uses `performance.now()` for high-resolution timing stored on `context.data['_logging_mw_start']`. Supports configurable flags: `logInputs`, `logOutputs`, `logErrors` (all default `true`). Uses `context.redactedInputs` when available to avoid logging sensitive data.
20
+
21
+ - **Barrel Export** (`middleware/index.ts`) -- Re-exports all public classes, types, and interfaces.
22
+
23
+ ### Data Flow
24
+
25
+ The middleware pipeline integrates into the executor's 10-step call sequence:
26
+
27
+ ```
28
+ Inputs ──> [Before Phase: forward order] ──> Module Execution ──> [After Phase: reverse order] ──> Output
29
+
30
+ ▼ (on error)
31
+ [OnError Phase: reverse over executed middlewares]
32
+ ```
33
+
34
+ 1. **Before Phase** -- `executeBefore()` iterates middlewares in registration order. Each `before()` can return transformed inputs or `null` (pass-through). If any middleware throws, a `MiddlewareChainError` is raised carrying the list of already-executed middlewares.
35
+
36
+ 2. **After Phase** -- `executeAfter()` iterates middlewares in reverse registration order (onion unwinding). Each `after()` can return transformed output or `null` (pass-through).
37
+
38
+ 3. **OnError Phase** -- `executeOnError()` iterates the `executedMiddlewares` list in reverse. The first middleware returning a non-null value provides the recovery result. Errors thrown inside `onError()` handlers are swallowed to prevent cascading failures.
39
+
40
+ ### Technical Choices and Rationale
41
+
42
+ - **No thread locking**: Node.js runs on a single-threaded event loop. The Python implementation uses `threading.Lock` for middleware list mutations; this is unnecessary in TypeScript. The `snapshot()` method provides a stable copy for iteration, which is sufficient to guard against mutations during execution.
43
+
44
+ - **`MiddlewareChainError` extends `ModuleError`**: TypeScript uses `Error` (not `Exception` as in Python). `MiddlewareChainError` extends `ModuleError` to integrate with the framework's structured error hierarchy (code, details, timestamp). It carries `original: Error` and `executedMiddlewares: Middleware[]` for error recovery.
45
+
46
+ - **`performance.now()` for timing**: Provides sub-millisecond resolution timing available in Node.js 18+ without additional dependencies. The start timestamp is stored on `context.data` (shared between parent/child contexts) rather than middleware instance state, ensuring correct timing in concurrent call scenarios.
47
+
48
+ - **Synchronous lifecycle hooks**: All middleware hooks are synchronous. This is a deliberate design choice -- middleware should perform lightweight transformations and logging, not I/O. The executor wraps the entire pipeline in its async `call()` method.
49
+
50
+ - **Identity-based `remove()`**: Middleware removal uses strict reference equality (`===`), matching the Python implementation's `is` comparison. This ensures that only the exact instance is removed, not a structurally equivalent one.
51
+
52
+ ## Task Breakdown
53
+
54
+ ```mermaid
55
+ graph TD
56
+ T1[base] --> T2[manager]
57
+ T1 --> T3[adapters]
58
+ T1 --> T4[logging-middleware]
59
+ T2 --> T4
60
+ ```
61
+
62
+ | Task ID | Title | Estimated Time | Dependencies |
63
+ |---------|-------|---------------|--------------|
64
+ | base | Middleware base class with no-op hooks | 1h | none |
65
+ | manager | MiddlewareManager with onion-model execution | 3h | base |
66
+ | adapters | BeforeMiddleware and AfterMiddleware adapters | 1.5h | base |
67
+ | logging-middleware | LoggingMiddleware with Logger and timing | 2h | base, manager |
68
+
69
+ ## Risks and Considerations
70
+
71
+ - **Middleware ordering sensitivity**: The onion model means registration order directly affects behavior. Middlewares registered first run their `before()` first but their `after()` last. Users must understand this ordering to compose middleware correctly.
72
+ - **Error swallowing in `onError()`**: Errors thrown inside `onError()` handlers are silently swallowed. This prevents cascading failures but can hide bugs in error-recovery logic. Consider adding debug-level logging in the future.
73
+ - **`context.data` key collisions**: `LoggingMiddleware` writes to `context.data['_logging_mw_start']`. Other middlewares writing to the same key would corrupt timing. The `_logging_mw_` prefix is a convention to reduce collision risk, but there is no namespace enforcement.
74
+ - **No async middleware support**: If a use case requires async middleware (e.g., fetching external config before execution), the current synchronous design does not support it. This is an intentional trade-off for simplicity and performance.
75
+
76
+ ## Acceptance Criteria
77
+
78
+ - [x] `Middleware` base class has `before()`, `after()`, `onError()` returning `null` by default
79
+ - [x] `MiddlewareManager.add()` appends and `remove()` removes by reference identity
80
+ - [x] `snapshot()` returns a shallow copy that does not affect the internal list
81
+ - [x] `executeBefore()` runs middlewares in forward order and returns `[inputs, executedMiddlewares]`
82
+ - [x] `executeAfter()` runs middlewares in reverse order
83
+ - [x] `executeOnError()` runs in reverse over executed middlewares; first non-null wins; errors in handlers are swallowed
84
+ - [x] `MiddlewareChainError` extends `ModuleError` with `original` and `executedMiddlewares` properties
85
+ - [x] `BeforeMiddleware` delegates only `before()` to the callback; other hooks remain no-op
86
+ - [x] `AfterMiddleware` delegates only `after()` to the callback; other hooks remain no-op
87
+ - [x] `LoggingMiddleware` logs START/END/ERROR with trace ID, module ID, and timing
88
+ - [x] `LoggingMiddleware` uses `performance.now()` for sub-millisecond duration measurement
89
+ - [x] `LoggingMiddleware` respects `logInputs`, `logOutputs`, `logErrors` configuration flags
90
+ - [x] `LoggingMiddleware` uses `context.redactedInputs` when available
91
+ - [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
92
+
93
+ ## References
94
+
95
+ - `src/middleware/base.ts` -- Middleware base class
96
+ - `src/middleware/manager.ts` -- MiddlewareManager and MiddlewareChainError
97
+ - `src/middleware/adapters.ts` -- BeforeMiddleware, AfterMiddleware, and callback types
98
+ - `src/middleware/logging.ts` -- LoggingMiddleware and Logger interface
99
+ - `src/middleware/index.ts` -- Barrel export
100
+ - `src/errors.ts` -- ModuleError base class
101
+ - `tests/test-middleware.test.ts` -- Base class and adapter tests
102
+ - `tests/test-middleware-manager.test.ts` -- Manager and MiddlewareChainError tests
@@ -0,0 +1,65 @@
1
+ {
2
+ "feature": "middleware-system",
3
+ "created": "2026-02-16T00:00:00Z",
4
+ "updated": "2026-02-16T00:00:00Z",
5
+ "status": "completed",
6
+ "execution_order": [
7
+ "base",
8
+ "manager",
9
+ "adapters",
10
+ "logging-middleware"
11
+ ],
12
+ "progress": {
13
+ "total_tasks": 4,
14
+ "completed": 4,
15
+ "in_progress": 0,
16
+ "pending": 0
17
+ },
18
+ "tasks": [
19
+ {
20
+ "id": "base",
21
+ "file": "tasks/base.md",
22
+ "title": "Middleware Base Class",
23
+ "status": "completed",
24
+ "started_at": "2026-02-16T08:00:00Z",
25
+ "completed_at": "2026-02-16T08:45:00Z",
26
+ "assignee": null,
27
+ "commits": []
28
+ },
29
+ {
30
+ "id": "manager",
31
+ "file": "tasks/manager.md",
32
+ "title": "MiddlewareManager with Onion-Model Execution",
33
+ "status": "completed",
34
+ "started_at": "2026-02-16T08:45:00Z",
35
+ "completed_at": "2026-02-16T11:00:00Z",
36
+ "assignee": null,
37
+ "commits": []
38
+ },
39
+ {
40
+ "id": "adapters",
41
+ "file": "tasks/adapters.md",
42
+ "title": "BeforeMiddleware and AfterMiddleware Adapters",
43
+ "status": "completed",
44
+ "started_at": "2026-02-16T11:00:00Z",
45
+ "completed_at": "2026-02-16T12:15:00Z",
46
+ "assignee": null,
47
+ "commits": []
48
+ },
49
+ {
50
+ "id": "logging-middleware",
51
+ "file": "tasks/logging-middleware.md",
52
+ "title": "LoggingMiddleware with Logger Interface and Timing",
53
+ "status": "completed",
54
+ "started_at": "2026-02-16T12:15:00Z",
55
+ "completed_at": "2026-02-16T13:45:00Z",
56
+ "assignee": null,
57
+ "commits": []
58
+ }
59
+ ],
60
+ "metadata": {
61
+ "source_doc": "planning/overview.md",
62
+ "created_by": "code-forge",
63
+ "version": "1.0"
64
+ }
65
+ }
@@ -0,0 +1,170 @@
1
+ # Task: BeforeMiddleware and AfterMiddleware Adapters
2
+
3
+ ## Goal
4
+
5
+ Implement `BeforeMiddleware` and `AfterMiddleware` adapter classes that wrap callback functions into the middleware interface. These adapters enable lightweight, functional middleware creation without requiring users to subclass `Middleware`. Each adapter delegates a single lifecycle hook to the provided callback while inheriting no-op behavior for the other hooks from the base class. Also define `BeforeCallback` and `AfterCallback` type aliases for the callback signatures.
6
+
7
+ ## Files Involved
8
+
9
+ | File | Action |
10
+ |------|--------|
11
+ | `src/middleware/adapters.ts` | Create -- BeforeMiddleware, AfterMiddleware, and callback types |
12
+ | `tests/test-middleware.test.ts` | Extend -- Unit tests for adapter classes |
13
+
14
+ ## Steps (TDD)
15
+
16
+ ### Step 1: Define callback type aliases
17
+
18
+ ```typescript
19
+ export type BeforeCallback = (
20
+ moduleId: string,
21
+ inputs: Record<string, unknown>,
22
+ context: Context,
23
+ ) => Record<string, unknown> | null;
24
+
25
+ export type AfterCallback = (
26
+ moduleId: string,
27
+ inputs: Record<string, unknown>,
28
+ output: Record<string, unknown>,
29
+ context: Context,
30
+ ) => Record<string, unknown> | null;
31
+ ```
32
+
33
+ ### Step 2: Write failing tests for BeforeMiddleware
34
+
35
+ ```typescript
36
+ describe('BeforeMiddleware', () => {
37
+ it('wraps a callback and delegates to before()', () => {
38
+ const mw = new BeforeMiddleware((moduleId, inputs) => {
39
+ return { ...inputs, injected: moduleId };
40
+ });
41
+ const ctx = makeContext();
42
+ const result = mw.before('mod.x', { a: 1 }, ctx);
43
+ expect(result).toEqual({ a: 1, injected: 'mod.x' });
44
+ });
45
+
46
+ it('after() still returns null', () => {
47
+ const mw = new BeforeMiddleware(() => ({ replaced: true }));
48
+ const ctx = makeContext();
49
+ expect(mw.after('mod.x', {}, {}, ctx)).toBeNull();
50
+ });
51
+
52
+ it('onError() still returns null', () => {
53
+ const mw = new BeforeMiddleware(() => ({ replaced: true }));
54
+ const ctx = makeContext();
55
+ expect(mw.onError('mod.x', {}, new Error('fail'), ctx)).toBeNull();
56
+ });
57
+
58
+ it('can return null from callback', () => {
59
+ const mw = new BeforeMiddleware(() => null);
60
+ const ctx = makeContext();
61
+ expect(mw.before('mod.x', { a: 1 }, ctx)).toBeNull();
62
+ });
63
+ });
64
+ ```
65
+
66
+ ### Step 3: Implement BeforeMiddleware
67
+
68
+ ```typescript
69
+ import type { Context } from '../context.js';
70
+ import { Middleware } from './base.js';
71
+
72
+ export class BeforeMiddleware extends Middleware {
73
+ private _callback: BeforeCallback;
74
+
75
+ constructor(callback: BeforeCallback) {
76
+ super();
77
+ this._callback = callback;
78
+ }
79
+
80
+ override before(
81
+ moduleId: string,
82
+ inputs: Record<string, unknown>,
83
+ context: Context,
84
+ ): Record<string, unknown> | null {
85
+ return this._callback(moduleId, inputs, context);
86
+ }
87
+ }
88
+ ```
89
+
90
+ ### Step 4: Write failing tests for AfterMiddleware
91
+
92
+ ```typescript
93
+ describe('AfterMiddleware', () => {
94
+ it('wraps a callback and delegates to after()', () => {
95
+ const mw = new AfterMiddleware((moduleId, _inputs, output) => {
96
+ return { ...output, processedBy: moduleId };
97
+ });
98
+ const ctx = makeContext();
99
+ const result = mw.after('mod.y', { a: 1 }, { out: 42 }, ctx);
100
+ expect(result).toEqual({ out: 42, processedBy: 'mod.y' });
101
+ });
102
+
103
+ it('before() still returns null', () => {
104
+ const mw = new AfterMiddleware(() => ({ replaced: true }));
105
+ const ctx = makeContext();
106
+ expect(mw.before('mod.y', {}, ctx)).toBeNull();
107
+ });
108
+
109
+ it('onError() still returns null', () => {
110
+ const mw = new AfterMiddleware(() => ({ replaced: true }));
111
+ const ctx = makeContext();
112
+ expect(mw.onError('mod.y', {}, new Error('fail'), ctx)).toBeNull();
113
+ });
114
+
115
+ it('can return null from callback', () => {
116
+ const mw = new AfterMiddleware(() => null);
117
+ const ctx = makeContext();
118
+ expect(mw.after('mod.y', {}, { out: 1 }, ctx)).toBeNull();
119
+ });
120
+ });
121
+ ```
122
+
123
+ ### Step 5: Implement AfterMiddleware
124
+
125
+ ```typescript
126
+ export class AfterMiddleware extends Middleware {
127
+ private _callback: AfterCallback;
128
+
129
+ constructor(callback: AfterCallback) {
130
+ super();
131
+ this._callback = callback;
132
+ }
133
+
134
+ override after(
135
+ moduleId: string,
136
+ inputs: Record<string, unknown>,
137
+ output: Record<string, unknown>,
138
+ context: Context,
139
+ ): Record<string, unknown> | null {
140
+ return this._callback(moduleId, inputs, output, context);
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### Step 6: Run tests and confirm all pass
146
+
147
+ ```bash
148
+ npx vitest run tests/test-middleware.test.ts
149
+ ```
150
+
151
+ ## Acceptance Criteria
152
+
153
+ - [x] `BeforeCallback` type alias is exported with signature `(moduleId, inputs, context) => Record<string, unknown> | null`
154
+ - [x] `AfterCallback` type alias is exported with signature `(moduleId, inputs, output, context) => Record<string, unknown> | null`
155
+ - [x] `BeforeMiddleware` extends `Middleware` and delegates only `before()` to the callback
156
+ - [x] `BeforeMiddleware.after()` and `BeforeMiddleware.onError()` return `null` (inherited no-op)
157
+ - [x] `AfterMiddleware` extends `Middleware` and delegates only `after()` to the callback
158
+ - [x] `AfterMiddleware.before()` and `AfterMiddleware.onError()` return `null` (inherited no-op)
159
+ - [x] Callbacks stored as `private _callback` fields
160
+ - [x] Both adapters use `override` keyword on the delegated method
161
+ - [x] All tests pass with `vitest`
162
+
163
+ ## Dependencies
164
+
165
+ - `src/middleware/base.ts` -- `Middleware` base class (task: base)
166
+ - `src/context.ts` -- `Context` class (type import)
167
+
168
+ ## Estimated Time
169
+
170
+ 1.5 hours
@@ -0,0 +1,115 @@
1
+ # Task: Middleware Base Class
2
+
3
+ ## Goal
4
+
5
+ Implement the `Middleware` base class that defines the three lifecycle hooks for the middleware pipeline: `before()`, `after()`, and `onError()`. Each hook returns `Record<string, unknown> | null` -- returning `null` signals "no transformation" and the pipeline passes the original value through. The base class provides no-op defaults so subclasses only need to override the hooks they care about.
6
+
7
+ ## Files Involved
8
+
9
+ | File | Action |
10
+ |------|--------|
11
+ | `src/middleware/base.ts` | Create -- Middleware base class |
12
+ | `tests/test-middleware.test.ts` | Create -- Unit tests for base class hooks |
13
+
14
+ ## Steps (TDD)
15
+
16
+ ### Step 1: Write failing tests for `before()` default behavior
17
+
18
+ Write a test that instantiates `Middleware` and calls `before()` with a module ID, inputs record, and context. Assert it returns `null`.
19
+
20
+ ```typescript
21
+ import { describe, it, expect } from 'vitest';
22
+ import { Middleware } from '../src/middleware/base.js';
23
+ import { Context, createIdentity } from '../src/context.js';
24
+
25
+ function makeContext(): Context {
26
+ return Context.create(null, createIdentity('test-user'));
27
+ }
28
+
29
+ describe('Middleware base class', () => {
30
+ it('before() returns null by default', () => {
31
+ const mw = new Middleware();
32
+ const ctx = makeContext();
33
+ expect(mw.before('mod.a', { x: 1 }, ctx)).toBeNull();
34
+ });
35
+ });
36
+ ```
37
+
38
+ ### Step 2: Write failing tests for `after()` default behavior
39
+
40
+ ```typescript
41
+ it('after() returns null by default', () => {
42
+ const mw = new Middleware();
43
+ const ctx = makeContext();
44
+ expect(mw.after('mod.a', { x: 1 }, { y: 2 }, ctx)).toBeNull();
45
+ });
46
+ ```
47
+
48
+ ### Step 3: Write failing tests for `onError()` default behavior
49
+
50
+ ```typescript
51
+ it('onError() returns null by default', () => {
52
+ const mw = new Middleware();
53
+ const ctx = makeContext();
54
+ expect(mw.onError('mod.a', { x: 1 }, new Error('boom'), ctx)).toBeNull();
55
+ });
56
+ ```
57
+
58
+ ### Step 4: Implement the Middleware class
59
+
60
+ ```typescript
61
+ import type { Context } from '../context.js';
62
+
63
+ export class Middleware {
64
+ before(
65
+ _moduleId: string,
66
+ _inputs: Record<string, unknown>,
67
+ _context: Context,
68
+ ): Record<string, unknown> | null {
69
+ return null;
70
+ }
71
+
72
+ after(
73
+ _moduleId: string,
74
+ _inputs: Record<string, unknown>,
75
+ _output: Record<string, unknown>,
76
+ _context: Context,
77
+ ): Record<string, unknown> | null {
78
+ return null;
79
+ }
80
+
81
+ onError(
82
+ _moduleId: string,
83
+ _inputs: Record<string, unknown>,
84
+ _error: Error,
85
+ _context: Context,
86
+ ): Record<string, unknown> | null {
87
+ return null;
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### Step 5: Run tests and confirm all pass
93
+
94
+ ```bash
95
+ npx vitest run tests/test-middleware.test.ts
96
+ ```
97
+
98
+ ## Acceptance Criteria
99
+
100
+ - [x] `Middleware` class is exported from `src/middleware/base.ts`
101
+ - [x] `before(moduleId, inputs, context)` returns `null` by default
102
+ - [x] `after(moduleId, inputs, output, context)` returns `null` by default
103
+ - [x] `onError(moduleId, inputs, error, context)` returns `null` by default
104
+ - [x] All parameters use underscore-prefixed names to indicate intentional non-use
105
+ - [x] Type signature uses `Record<string, unknown> | null` return type
106
+ - [x] Imports `Context` as a type-only import
107
+ - [x] All tests pass with `vitest`
108
+
109
+ ## Dependencies
110
+
111
+ - `src/context.ts` -- `Context` class (used as parameter type)
112
+
113
+ ## Estimated Time
114
+
115
+ 1 hour