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.
- package/.claude/settings.local.json +11 -0
- package/.gitmessage +60 -0
- package/.pre-commit-config.yaml +28 -0
- package/CHANGELOG.md +47 -0
- package/CLAUDE.md +68 -0
- package/README.md +131 -0
- package/apcore-logo.svg +79 -0
- package/package.json +37 -0
- package/planning/acl-system/overview.md +54 -0
- package/planning/acl-system/plan.md +92 -0
- package/planning/acl-system/state.json +76 -0
- package/planning/acl-system/tasks/acl-core.md +226 -0
- package/planning/acl-system/tasks/acl-rule.md +92 -0
- package/planning/acl-system/tasks/conditional-rules.md +259 -0
- package/planning/acl-system/tasks/pattern-matching.md +152 -0
- package/planning/acl-system/tasks/yaml-loading.md +271 -0
- package/planning/core-executor/overview.md +53 -0
- package/planning/core-executor/plan.md +88 -0
- package/planning/core-executor/state.json +76 -0
- package/planning/core-executor/tasks/async-support.md +106 -0
- package/planning/core-executor/tasks/execution-pipeline.md +113 -0
- package/planning/core-executor/tasks/redaction.md +85 -0
- package/planning/core-executor/tasks/safety-checks.md +65 -0
- package/planning/core-executor/tasks/setup.md +75 -0
- package/planning/decorator-bindings/overview.md +62 -0
- package/planning/decorator-bindings/plan.md +104 -0
- package/planning/decorator-bindings/state.json +87 -0
- package/planning/decorator-bindings/tasks/binding-directory.md +79 -0
- package/planning/decorator-bindings/tasks/binding-loader.md +148 -0
- package/planning/decorator-bindings/tasks/explicit-schemas.md +85 -0
- package/planning/decorator-bindings/tasks/function-module.md +127 -0
- package/planning/decorator-bindings/tasks/module-factory.md +89 -0
- package/planning/decorator-bindings/tasks/schema-modes.md +142 -0
- package/planning/middleware-system/overview.md +48 -0
- package/planning/middleware-system/plan.md +102 -0
- package/planning/middleware-system/state.json +65 -0
- package/planning/middleware-system/tasks/adapters.md +170 -0
- package/planning/middleware-system/tasks/base.md +115 -0
- package/planning/middleware-system/tasks/logging-middleware.md +304 -0
- package/planning/middleware-system/tasks/manager.md +313 -0
- package/planning/observability/overview.md +53 -0
- package/planning/observability/plan.md +119 -0
- package/planning/observability/state.json +98 -0
- package/planning/observability/tasks/context-logger.md +201 -0
- package/planning/observability/tasks/exporters.md +121 -0
- package/planning/observability/tasks/metrics-collector.md +162 -0
- package/planning/observability/tasks/metrics-middleware.md +141 -0
- package/planning/observability/tasks/obs-logging-middleware.md +179 -0
- package/planning/observability/tasks/span-model.md +120 -0
- package/planning/observability/tasks/tracing-middleware.md +179 -0
- package/planning/overview.md +81 -0
- package/planning/registry-system/overview.md +57 -0
- package/planning/registry-system/plan.md +114 -0
- package/planning/registry-system/state.json +109 -0
- package/planning/registry-system/tasks/dependencies.md +157 -0
- package/planning/registry-system/tasks/entry-point.md +148 -0
- package/planning/registry-system/tasks/metadata.md +198 -0
- package/planning/registry-system/tasks/registry-core.md +323 -0
- package/planning/registry-system/tasks/scanner.md +172 -0
- package/planning/registry-system/tasks/schema-export.md +261 -0
- package/planning/registry-system/tasks/types.md +124 -0
- package/planning/registry-system/tasks/validation.md +177 -0
- package/planning/schema-system/overview.md +56 -0
- package/planning/schema-system/plan.md +121 -0
- package/planning/schema-system/state.json +98 -0
- package/planning/schema-system/tasks/exporter.md +153 -0
- package/planning/schema-system/tasks/loader.md +106 -0
- package/planning/schema-system/tasks/ref-resolver.md +133 -0
- package/planning/schema-system/tasks/strict-mode.md +140 -0
- package/planning/schema-system/tasks/typebox-generation.md +133 -0
- package/planning/schema-system/tasks/types-and-annotations.md +160 -0
- package/planning/schema-system/tasks/validator.md +149 -0
- package/src/acl.ts +188 -0
- package/src/bindings.ts +208 -0
- package/src/config.ts +24 -0
- package/src/context.ts +75 -0
- package/src/decorator.ts +110 -0
- package/src/errors.ts +369 -0
- package/src/executor.ts +348 -0
- package/src/index.ts +81 -0
- package/src/middleware/adapters.ts +54 -0
- package/src/middleware/base.ts +33 -0
- package/src/middleware/index.ts +6 -0
- package/src/middleware/logging.ts +103 -0
- package/src/middleware/manager.ts +105 -0
- package/src/module.ts +41 -0
- package/src/observability/context-logger.ts +201 -0
- package/src/observability/index.ts +4 -0
- package/src/observability/metrics.ts +212 -0
- package/src/observability/tracing.ts +187 -0
- package/src/registry/dependencies.ts +99 -0
- package/src/registry/entry-point.ts +64 -0
- package/src/registry/index.ts +8 -0
- package/src/registry/metadata.ts +111 -0
- package/src/registry/registry.ts +314 -0
- package/src/registry/scanner.ts +150 -0
- package/src/registry/schema-export.ts +177 -0
- package/src/registry/types.ts +32 -0
- package/src/registry/validation.ts +38 -0
- package/src/schema/annotations.ts +67 -0
- package/src/schema/exporter.ts +93 -0
- package/src/schema/index.ts +14 -0
- package/src/schema/loader.ts +270 -0
- package/src/schema/ref-resolver.ts +235 -0
- package/src/schema/strict.ts +128 -0
- package/src/schema/types.ts +73 -0
- package/src/schema/validator.ts +82 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/pattern.ts +30 -0
- package/tests/helpers.ts +30 -0
- package/tests/integration/test-acl-safety.test.ts +268 -0
- package/tests/integration/test-binding-executor.test.ts +194 -0
- package/tests/integration/test-e2e-flow.test.ts +117 -0
- package/tests/integration/test-error-propagation.test.ts +259 -0
- package/tests/integration/test-middleware-chain.test.ts +120 -0
- package/tests/integration/test-observability-integration.test.ts +438 -0
- package/tests/observability/test-context-logger.test.ts +123 -0
- package/tests/observability/test-metrics.test.ts +89 -0
- package/tests/observability/test-tracing.test.ts +131 -0
- package/tests/registry/test-dependencies.test.ts +70 -0
- package/tests/registry/test-entry-point.test.ts +133 -0
- package/tests/registry/test-metadata.test.ts +265 -0
- package/tests/registry/test-registry.test.ts +140 -0
- package/tests/registry/test-scanner.test.ts +257 -0
- package/tests/registry/test-schema-export.test.ts +224 -0
- package/tests/registry/test-validation.test.ts +75 -0
- package/tests/schema/test-loader.test.ts +97 -0
- package/tests/schema/test-ref-resolver.test.ts +105 -0
- package/tests/schema/test-strict.test.ts +139 -0
- package/tests/schema/test-validator.test.ts +64 -0
- package/tests/test-acl.test.ts +206 -0
- package/tests/test-bindings.test.ts +227 -0
- package/tests/test-config.test.ts +76 -0
- package/tests/test-context.test.ts +151 -0
- package/tests/test-decorator.test.ts +173 -0
- package/tests/test-errors.test.ts +204 -0
- package/tests/test-executor.test.ts +252 -0
- package/tests/test-middleware-manager.test.ts +185 -0
- package/tests/test-middleware.test.ts +86 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +20 -0
- 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
|