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,106 @@
|
|
|
1
|
+
# Task: Unified Async Execution Path
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the unified async execution model for the TypeScript executor. Unlike the Python implementation which requires separate `call()` (sync) and `call_async()` methods with a complex sync/async bridge (daemon threads, new event loops, `asyncio.to_thread`), the TypeScript implementation uses a single async `call()` method that transparently handles both sync and async module `execute()` functions via `Promise.resolve()`.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/executor.ts` -- `call()` method, `_executeWithTimeout()` (~50 lines of async-specific logic)
|
|
10
|
+
- `tests/test-executor.test.ts` -- Tests for async/sync module handling
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Verify Promise.resolve() handles both sync and async (TDD)
|
|
15
|
+
|
|
16
|
+
Write tests demonstrating that `Promise.resolve(syncFn())` and `Promise.resolve(asyncFn())` both produce the same awaitable result:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
describe('unified async execution', () => {
|
|
20
|
+
it('handles sync execute function', async () => {
|
|
21
|
+
const syncModule = {
|
|
22
|
+
execute: (inputs: Record<string, unknown>) => ({ result: inputs['x'] }),
|
|
23
|
+
// ... schemas
|
|
24
|
+
};
|
|
25
|
+
const result = await executor.call('sync.mod', { x: 42 });
|
|
26
|
+
expect(result).toEqual({ result: 42 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('handles async execute function', async () => {
|
|
30
|
+
const asyncModule = {
|
|
31
|
+
execute: async (inputs: Record<string, unknown>) => {
|
|
32
|
+
await new Promise(r => setTimeout(r, 10));
|
|
33
|
+
return { result: inputs['x'] };
|
|
34
|
+
},
|
|
35
|
+
// ... schemas
|
|
36
|
+
};
|
|
37
|
+
const result = await executor.call('async.mod', { x: 42 });
|
|
38
|
+
expect(result).toEqual({ result: 42 });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Verify timeout works for both execution modes (TDD)
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
it('times out slow sync execution', async () => {
|
|
47
|
+
// Sync module that blocks (while-loop spin)
|
|
48
|
+
// Promise.race with setTimeout catches this
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('times out slow async execution', async () => {
|
|
52
|
+
// Async module with long await
|
|
53
|
+
// Promise.race with setTimeout catches this
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Document the architectural simplification
|
|
58
|
+
|
|
59
|
+
The Python implementation requires ~260 lines for sync/async bridging:
|
|
60
|
+
- `call_async()` -- async pipeline duplicate
|
|
61
|
+
- `_execute_async()` -- async-aware execution dispatch
|
|
62
|
+
- `_run_async_in_sync()` -- bridge for async modules in sync context
|
|
63
|
+
- `_run_in_new_thread()` -- daemon thread with new event loop
|
|
64
|
+
- `_execute_on_error_async()` -- async-aware error recovery
|
|
65
|
+
- `_is_async_module()` -- cached async detection with thread lock
|
|
66
|
+
|
|
67
|
+
The TypeScript version eliminates all of this with a single pattern:
|
|
68
|
+
```typescript
|
|
69
|
+
const result = await Promise.resolve(module.execute(inputs, ctx));
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This works because:
|
|
73
|
+
1. `Promise.resolve(value)` wraps sync values in a resolved Promise
|
|
74
|
+
2. `Promise.resolve(promise)` returns the same Promise (identity for thenables)
|
|
75
|
+
3. `await` unwraps both cases identically
|
|
76
|
+
4. `Promise.race` provides timeout for both sync and async paths
|
|
77
|
+
|
|
78
|
+
### 4. Verify middleware hooks work for both module types (TDD)
|
|
79
|
+
|
|
80
|
+
Test that middleware `before()`, `after()`, and `onError()` execute correctly regardless of whether the module's `execute()` is sync or async.
|
|
81
|
+
|
|
82
|
+
### 5. Run full test suite
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx vitest run tests/test-executor.test.ts
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Acceptance Criteria
|
|
89
|
+
|
|
90
|
+
- [x] Single `call()` method handles both sync and async modules
|
|
91
|
+
- [x] `Promise.resolve()` transparently wraps sync return values
|
|
92
|
+
- [x] `Promise.race` provides timeout enforcement for both execution modes
|
|
93
|
+
- [x] No async detection cache needed (no `_isAsyncModule()`, no thread lock)
|
|
94
|
+
- [x] No separate `callAsync()` method needed
|
|
95
|
+
- [x] No daemon threads, new event loops, or `to_thread()` bridges needed
|
|
96
|
+
- [x] Middleware hooks work identically for sync and async modules
|
|
97
|
+
- [x] Error propagation works for both sync throws and async rejections
|
|
98
|
+
- [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
|
|
99
|
+
|
|
100
|
+
## Dependencies
|
|
101
|
+
|
|
102
|
+
- Task: execution-pipeline (base `call()` implementation with `_executeWithTimeout()`)
|
|
103
|
+
|
|
104
|
+
## Estimated Time
|
|
105
|
+
|
|
106
|
+
4 hours
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Task: 10-Step Async Execution Pipeline
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the complete execution pipeline in the `Executor.call()` method, integrating all 10 steps: context creation, safety checks, module lookup, ACL enforcement, input validation with redaction, middleware before chain, module execution with timeout, output validation, middleware after chain, and result return. Unlike the Python implementation which has separate `call()` and `call_async()`, the TypeScript version uses a single async `call()` method.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/executor.ts` -- `Executor` class: `constructor()`, `call()`, `validate()`, `_executeWithTimeout()`, middleware registration methods (~300 lines)
|
|
10
|
+
- `src/errors.ts` -- `ModuleNotFoundError`, `ACLDeniedError`, `SchemaValidationError`, `ModuleTimeoutError`, `InvalidInputError`
|
|
11
|
+
- `tests/test-executor.test.ts` -- Full pipeline unit and integration tests
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
### 1. Implement Executor constructor (TDD)
|
|
16
|
+
|
|
17
|
+
- Accept options object: `{ registry, middlewares?, acl?, config? }`
|
|
18
|
+
- Initialize `MiddlewareManager` and register provided middlewares
|
|
19
|
+
- Read config values for `defaultTimeout` (30000ms), `globalTimeout` (60000ms), `maxCallDepth` (32), `maxModuleRepeat` (3)
|
|
20
|
+
|
|
21
|
+
### 2. Implement middleware registration (TDD)
|
|
22
|
+
|
|
23
|
+
- `use(middleware)` -- adds class-based middleware, returns `this` for chaining
|
|
24
|
+
- `useBefore(callback)` -- wraps in `BeforeMiddleware` adapter
|
|
25
|
+
- `useAfter(callback)` -- wraps in `AfterMiddleware` adapter
|
|
26
|
+
- `remove(middleware)` -- delegates to `MiddlewareManager.remove()`
|
|
27
|
+
|
|
28
|
+
### 3. Implement call() pipeline (TDD)
|
|
29
|
+
|
|
30
|
+
- **Step 1**: Create or derive Context via `Context.create()` + `child()` or `context.child()`
|
|
31
|
+
- **Step 2**: Run `_checkSafety(moduleId, ctx)`
|
|
32
|
+
- **Step 3**: `registry.get(moduleId)`, throw `ModuleNotFoundError` if null
|
|
33
|
+
- **Step 4**: `acl.check()` if ACL configured, throw `ACLDeniedError` if denied
|
|
34
|
+
- **Step 5**: TypeBox `Value.Check()` for input validation, build `redactedInputs` via `redactSensitive()`
|
|
35
|
+
- **Step 6**: `executeBefore()`, handle `MiddlewareChainError` with `onError` recovery
|
|
36
|
+
- **Step 7**: `_executeWithTimeout()` via `Promise.race`
|
|
37
|
+
- **Step 8**: TypeBox `Value.Check()` for output validation
|
|
38
|
+
- **Step 9**: `executeAfter()` in reverse order
|
|
39
|
+
- **Step 10**: Return output or propagate error with `onError` recovery
|
|
40
|
+
|
|
41
|
+
### 4. Implement _executeWithTimeout (TDD)
|
|
42
|
+
|
|
43
|
+
- Wrap module execution in `Promise.race` against a timeout promise
|
|
44
|
+
- Timeout promise rejects with `ModuleTimeoutError` after `timeout_ms` milliseconds
|
|
45
|
+
- Uses `setTimeout` for the timeout timer
|
|
46
|
+
- Zero timeout: log warning, execute without timeout enforcement
|
|
47
|
+
- Negative timeout: throw `InvalidInputError`
|
|
48
|
+
- Both sync and async `execute()` return values handled via `Promise.resolve()`
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
private async _executeWithTimeout(
|
|
52
|
+
module: unknown,
|
|
53
|
+
inputs: Record<string, unknown>,
|
|
54
|
+
ctx: Context,
|
|
55
|
+
timeoutMs: number,
|
|
56
|
+
): Promise<Record<string, unknown>> {
|
|
57
|
+
if (timeoutMs < 0) throw new InvalidInputError('Timeout cannot be negative');
|
|
58
|
+
|
|
59
|
+
const executeFn = (module as any).execute.bind(module);
|
|
60
|
+
const resultPromise = Promise.resolve(executeFn(inputs, ctx));
|
|
61
|
+
|
|
62
|
+
if (timeoutMs === 0) {
|
|
63
|
+
// Warning: timeout disabled
|
|
64
|
+
return resultPromise;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
68
|
+
setTimeout(() => reject(new ModuleTimeoutError(moduleId, timeoutMs)), timeoutMs);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return Promise.race([resultPromise, timeoutPromise]);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 5. Implement validate() (TDD)
|
|
76
|
+
|
|
77
|
+
- Standalone pre-flight check without execution
|
|
78
|
+
- Returns `{ valid: boolean, errors: Array<{ path, message }> }`
|
|
79
|
+
- Throws `ModuleNotFoundError` if module not found
|
|
80
|
+
- Uses TypeBox `Value.Check()` and `Value.Errors()` for validation
|
|
81
|
+
|
|
82
|
+
### 6. Verify full pipeline tests pass
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx vitest run tests/test-executor.test.ts
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Acceptance Criteria
|
|
89
|
+
|
|
90
|
+
- [x] All 10 steps execute in order for the success path
|
|
91
|
+
- [x] Each step can independently throw its specific error type
|
|
92
|
+
- [x] MiddlewareChainError triggers onError recovery before re-throwing
|
|
93
|
+
- [x] Outer exception handler catches errors from steps 6-9 and runs onError on executed middlewares
|
|
94
|
+
- [x] Recovery output from onError short-circuits the error and returns the recovery dict
|
|
95
|
+
- [x] Timeout enforcement uses `Promise.race` with `setTimeout`
|
|
96
|
+
- [x] Timeout of 0 disables enforcement with a logged warning
|
|
97
|
+
- [x] Negative timeout throws `InvalidInputError`
|
|
98
|
+
- [x] `validate()` returns structured errors without executing the module
|
|
99
|
+
- [x] Both sync and async module `execute()` methods handled via `Promise.resolve()`
|
|
100
|
+
- [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
|
|
101
|
+
|
|
102
|
+
## Dependencies
|
|
103
|
+
|
|
104
|
+
- Task: setup (Context, Config)
|
|
105
|
+
- Task: safety-checks (`_checkSafety` method)
|
|
106
|
+
- Task: redaction (`redactSensitive` utility)
|
|
107
|
+
- Registry system (module lookup)
|
|
108
|
+
- Middleware system (MiddlewareManager)
|
|
109
|
+
- Schema system (TypeBox validation)
|
|
110
|
+
|
|
111
|
+
## Estimated Time
|
|
112
|
+
|
|
113
|
+
6 hours
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Task: Sensitive Field Redaction Utility
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `redactSensitive` utility that walks input/output dictionaries and replaces values of fields marked `x-sensitive: true` in the schema with `***REDACTED***`. This ensures sensitive data never appears in logs or error reports.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/executor.ts` -- `redactSensitive()`, `_redactFields()`, `_redactSecretPrefix()`, `REDACTED_VALUE` constant
|
|
10
|
+
- `tests/test-redaction.test.ts` -- Redaction unit tests
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Define REDACTED_VALUE constant (TDD)
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
export const REDACTED_VALUE = '***REDACTED***';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Test: verify constant value is `"***REDACTED***"`.
|
|
21
|
+
|
|
22
|
+
### 2. Implement redactSensitive (TDD)
|
|
23
|
+
|
|
24
|
+
- Accept `data: Record<string, unknown>` and `schemaDict: Record<string, unknown>`
|
|
25
|
+
- Deep copy via `JSON.parse(JSON.stringify(data))` to avoid mutating original
|
|
26
|
+
- Call `_redactFields()` for schema-based redaction
|
|
27
|
+
- Call `_redactSecretPrefix()` for key-prefix-based redaction
|
|
28
|
+
- Return the redacted copy
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
export function redactSensitive(
|
|
32
|
+
data: Record<string, unknown>,
|
|
33
|
+
schemaDict: Record<string, unknown>,
|
|
34
|
+
): Record<string, unknown> {
|
|
35
|
+
const copy = JSON.parse(JSON.stringify(data));
|
|
36
|
+
_redactFields(copy, schemaDict);
|
|
37
|
+
_redactSecretPrefix(copy);
|
|
38
|
+
return copy;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Test: deep copy (original not mutated), no mutation of original data.
|
|
43
|
+
|
|
44
|
+
### 3. Implement _redactFields (TDD)
|
|
45
|
+
|
|
46
|
+
- In-place redaction on the deep copy
|
|
47
|
+
- Read `properties` from `schemaDict`; return early if missing
|
|
48
|
+
- For each property: if `x-sensitive: true`, replace value with `REDACTED_VALUE` (skip `null`/`undefined`)
|
|
49
|
+
- For nested objects (`type: 'object'` with `properties`): recurse into the value dict
|
|
50
|
+
- For arrays (`type: 'array'` with `items`): if items have `x-sensitive`, redact each item; if items are objects with properties, recurse into each dict item
|
|
51
|
+
|
|
52
|
+
Test: flat fields, nested objects, arrays with `x-sensitive` items.
|
|
53
|
+
|
|
54
|
+
### 4. Implement _redactSecretPrefix (TDD)
|
|
55
|
+
|
|
56
|
+
- In-place redaction of any key starting with `_secret_`
|
|
57
|
+
- Replace non-null values with `REDACTED_VALUE`
|
|
58
|
+
|
|
59
|
+
Test: keys starting with `_secret_` redacted, non-matching keys preserved.
|
|
60
|
+
|
|
61
|
+
### 5. Verify tests pass
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx vitest run tests/test-redaction.test.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Acceptance Criteria
|
|
68
|
+
|
|
69
|
+
- [x] Original data dict is never mutated (`JSON.parse(JSON.stringify())` deep copy)
|
|
70
|
+
- [x] Fields with `x-sensitive: true` in schema are replaced with `***REDACTED***`
|
|
71
|
+
- [x] `null`/`undefined` values are not redacted (remain as-is)
|
|
72
|
+
- [x] Nested object fields are recursively redacted
|
|
73
|
+
- [x] Array items with `x-sensitive` are individually redacted
|
|
74
|
+
- [x] Array items that are objects with properties are recursively redacted
|
|
75
|
+
- [x] Keys starting with `_secret_` are redacted regardless of schema
|
|
76
|
+
- [x] Non-sensitive fields pass through unchanged
|
|
77
|
+
- [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
|
|
78
|
+
|
|
79
|
+
## Dependencies
|
|
80
|
+
|
|
81
|
+
- None (standalone utility, used by execution-pipeline task at step 5)
|
|
82
|
+
|
|
83
|
+
## Estimated Time
|
|
84
|
+
|
|
85
|
+
2 hours
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Task: Safety Checks -- Call Depth, Circular Detection, Frequency Throttling
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the three safety mechanisms evaluated at step 2 of the execution pipeline: call depth limiting, circular call detection, and per-module frequency throttling. These prevent unbounded recursion, circular invocation chains, and tight-loop abuse.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/executor.ts` -- `_checkSafety()` method
|
|
10
|
+
- `src/errors.ts` -- `CallDepthExceededError`, `CircularCallError`, `CallFrequencyExceededError`
|
|
11
|
+
- `tests/test-executor.test.ts` -- Safety check unit tests
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
### 1. Implement error types (TDD)
|
|
16
|
+
|
|
17
|
+
Write tests for each error's code, message, and details, then implement:
|
|
18
|
+
|
|
19
|
+
- `CallDepthExceededError(depth, maxDepth, callChain)` with code `CALL_DEPTH_EXCEEDED`
|
|
20
|
+
- `CircularCallError(moduleId, callChain)` with code `CIRCULAR_CALL`
|
|
21
|
+
- `CallFrequencyExceededError(moduleId, count, maxRepeat, callChain)` with code `CALL_FREQUENCY_EXCEEDED`
|
|
22
|
+
|
|
23
|
+
All extend `ModuleError` with `timestamp`, `code`, `message`, `details` record, and optional `cause`.
|
|
24
|
+
|
|
25
|
+
### 2. Implement call depth check (TDD)
|
|
26
|
+
|
|
27
|
+
- Compare `callChain.length` against `_maxCallDepth` (default 32)
|
|
28
|
+
- Throw `CallDepthExceededError` when exceeded
|
|
29
|
+
- Test: depth at limit (pass), below limit (pass), above limit (throw)
|
|
30
|
+
|
|
31
|
+
### 3. Implement circular call detection (TDD)
|
|
32
|
+
|
|
33
|
+
- Examine `callChain.slice(0, -1)` (prior chain, since `child()` already appended moduleId)
|
|
34
|
+
- If `moduleId` is found in prior chain, extract subsequence between last occurrence and end
|
|
35
|
+
- Only throw `CircularCallError` if subsequence length > 0 (true cycle of length >= 2)
|
|
36
|
+
- Test: A->B->A cycle (throw), A->B->C->B cycle (throw), non-cycle repetition (pass)
|
|
37
|
+
|
|
38
|
+
### 4. Implement frequency throttling (TDD)
|
|
39
|
+
|
|
40
|
+
- Count occurrences of `moduleId` in `callChain` using `filter().length`
|
|
41
|
+
- Throw `CallFrequencyExceededError` when count exceeds `_maxModuleRepeat` (default 3)
|
|
42
|
+
- Test: count at limit (pass), below limit (pass), above limit (throw)
|
|
43
|
+
|
|
44
|
+
### 5. Verify tests pass
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx vitest run tests/test-executor.test.ts
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Acceptance Criteria
|
|
51
|
+
|
|
52
|
+
- [x] Call depth check rejects chains exceeding maxCallDepth
|
|
53
|
+
- [x] Circular detection identifies A->B->A patterns but allows simple repetition (A->A)
|
|
54
|
+
- [x] Frequency throttle fires when a module appears more than maxModuleRepeat times in the chain
|
|
55
|
+
- [x] All errors carry full callChain in details for debugging
|
|
56
|
+
- [x] Configurable limits via Config (`executor.max_call_depth`, `executor.max_module_repeat`)
|
|
57
|
+
- [x] All error types extend ModuleError with timestamp and structured details
|
|
58
|
+
|
|
59
|
+
## Dependencies
|
|
60
|
+
|
|
61
|
+
- Task: setup (Context with callChain, Config with dot-path access)
|
|
62
|
+
|
|
63
|
+
## Estimated Time
|
|
64
|
+
|
|
65
|
+
2 hours
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Task: Context, Identity, and Config Classes
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the foundational data structures for the execution pipeline: `Context` class for per-call metadata propagation, `Identity` interface with `createIdentity()` factory for caller representation, and `Config` accessor for dot-path configuration.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/context.ts` -- `Context` class and `Identity` interface with `createIdentity()`
|
|
10
|
+
- `src/config.ts` -- `Config` class with dot-path key support
|
|
11
|
+
- `tests/test-context.test.ts` -- Unit tests for Context and Identity
|
|
12
|
+
- `tests/test-config.test.ts` -- Unit tests for Config
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
### 1. Write failing tests (TDD)
|
|
17
|
+
|
|
18
|
+
Create tests for:
|
|
19
|
+
- **Identity**: `createIdentity({ id: 'user-1' })` creates frozen object with defaults (type="user", roles=[], attrs={})
|
|
20
|
+
- **Identity immutability**: Frozen identity cannot be mutated at runtime
|
|
21
|
+
- **Context.create()**: Creates root context with UUID traceId, empty callChain, shared data dict
|
|
22
|
+
- **Context.child()**: Creates child context inheriting traceId, appending moduleId to callChain, sharing data by reference
|
|
23
|
+
- **Config.get()**: Dot-path navigation (e.g., `config.get('executor.default_timeout')`)
|
|
24
|
+
- **Config.get() with default**: Returns default when key not found
|
|
25
|
+
|
|
26
|
+
### 2. Implement Identity interface and createIdentity()
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
export interface Identity {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly type: string;
|
|
32
|
+
readonly roles: readonly string[];
|
|
33
|
+
readonly attrs: Readonly<Record<string, unknown>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createIdentity(options: { id: string; type?: string; roles?: string[]; attrs?: Record<string, unknown> }): Identity {
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
id: options.id,
|
|
39
|
+
type: options.type ?? 'user',
|
|
40
|
+
roles: Object.freeze([...(options.roles ?? [])]),
|
|
41
|
+
attrs: Object.freeze({ ...(options.attrs ?? {}) }),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Implement Context class
|
|
47
|
+
|
|
48
|
+
- `create()` static factory: generates traceId via `crypto.randomUUID()`, initializes empty callChain and data
|
|
49
|
+
- `child()` method: copies traceId, appends current moduleId to callChain, shares data reference
|
|
50
|
+
|
|
51
|
+
### 4. Implement Config class
|
|
52
|
+
|
|
53
|
+
- Constructor takes nested `Record<string, unknown>`
|
|
54
|
+
- `get(key, defaultValue?)` splits on `.` and traverses nested records
|
|
55
|
+
|
|
56
|
+
### 5. Verify tests pass
|
|
57
|
+
|
|
58
|
+
Run `npx vitest run tests/test-context.test.ts tests/test-config.test.ts`.
|
|
59
|
+
|
|
60
|
+
## Acceptance Criteria
|
|
61
|
+
|
|
62
|
+
- [x] `createIdentity()` returns frozen Identity with correct defaults
|
|
63
|
+
- [x] `Context.create()` generates UUID v4 traceId
|
|
64
|
+
- [x] `Context.child()` shares data dict by reference, appends to callChain
|
|
65
|
+
- [x] `Config.get()` supports dot-path navigation
|
|
66
|
+
- [x] `Config.get()` returns defaultValue when key not found
|
|
67
|
+
- [x] All fields correctly typed with readonly where appropriate
|
|
68
|
+
|
|
69
|
+
## Dependencies
|
|
70
|
+
|
|
71
|
+
None -- these are foundational data structures.
|
|
72
|
+
|
|
73
|
+
## Estimated Time
|
|
74
|
+
|
|
75
|
+
2 hours
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Feature: Decorator Bindings
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Decorator Bindings module provides the primary API for defining apcore modules from TypeScript functions. Unlike the Python implementation which uses `@module` decorators with runtime type introspection, the TypeScript version uses a `module()` factory function that accepts an options object with explicit TypeBox schemas. The `FunctionModule` class wraps any async or sync function with `inputSchema`/`outputSchema`, description, and metadata. The `normalizeResult()` utility (exported, unlike Python where it is private) standardizes return values. The `BindingLoader` class enables YAML-driven zero-code-modification module registration, resolving targets to callables via async `import()` and building schemas from JSON Schema definitions or external schema reference files.
|
|
6
|
+
|
|
7
|
+
Note: TypeScript cannot introspect types at runtime (type erasure at compile time), so there is no `auto_schema` mode and no type inference from function signatures. All schemas must be provided explicitly as TypeBox `TSchema` objects or via JSON Schema in YAML binding files.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
### Included
|
|
12
|
+
|
|
13
|
+
- `FunctionModule` class wrapping an execute function with explicit `inputSchema`/`outputSchema` (TypeBox `TSchema`), `description`, `documentation`, `tags`, `version`, `annotations`, `metadata`, `examples`
|
|
14
|
+
- `normalizeResult()` exported utility: `null`/`undefined` -> `{}`, `Record` -> passthrough, other -> `{ result: value }`
|
|
15
|
+
- `makeAutoId(name)` for ID generation from arbitrary strings (simpler than Python's `__module__`+`__qualname__`)
|
|
16
|
+
- `module()` factory function accepting an options object with explicit schemas and optional `registry` for auto-registration
|
|
17
|
+
- `BindingLoader` class with async `loadBindings(filePath, registry)` for YAML-driven module registration
|
|
18
|
+
- `loadBindingDir(dirPath, registry, pattern)` for directory scanning of binding YAML files
|
|
19
|
+
- Schema resolution modes in bindings: inline `input_schema`/`output_schema`, `schema_ref` to external YAML, permissive fallback
|
|
20
|
+
- `jsonSchemaToTypeBox()` integration for schema building from JSON Schema in binding files
|
|
21
|
+
- Dead `JSON_SCHEMA_TYPE_MAP` code in `bindings.ts` (noted as cleanup needed)
|
|
22
|
+
|
|
23
|
+
### Excluded
|
|
24
|
+
|
|
25
|
+
- Runtime type inference / `auto_schema` mode (impossible in TypeScript due to type erasure)
|
|
26
|
+
- Python-style `@module` decorator syntax (TypeScript uses factory function pattern)
|
|
27
|
+
- Schema system internals (consumed via `jsonSchemaToTypeBox()` from `schema-system`)
|
|
28
|
+
- Registry implementation (consumed as a dependency for module registration)
|
|
29
|
+
- Executor pipeline integration (FunctionModule is consumed by `core-executor`)
|
|
30
|
+
|
|
31
|
+
## Technology Stack
|
|
32
|
+
|
|
33
|
+
- **TypeScript 5.5+** with strict mode
|
|
34
|
+
- **@sinclair/typebox >= 0.34.0** for schema representation (`TSchema`, `Type.Object()`, `Type.Record()`, etc.)
|
|
35
|
+
- **js-yaml** for YAML binding file parsing
|
|
36
|
+
- **Node.js >= 18.0.0** with ES Module support (`node:fs`, `node:path`, dynamic `import()`)
|
|
37
|
+
- **vitest** for unit testing
|
|
38
|
+
|
|
39
|
+
## Task Execution Order
|
|
40
|
+
|
|
41
|
+
| # | Task File | Description | Status |
|
|
42
|
+
|---|-----------|-------------|--------|
|
|
43
|
+
| 1 | [function-module](./tasks/function-module.md) | FunctionModule class with execute, schemas, description, normalizeResult() | completed |
|
|
44
|
+
| 2 | [module-factory](./tasks/module-factory.md) | module() factory function with options object pattern | completed |
|
|
45
|
+
| 3 | [explicit-schemas](./tasks/explicit-schemas.md) | Explicit TypeBox schema passing (vs Python's auto-generation) | completed |
|
|
46
|
+
| 4 | [binding-loader](./tasks/binding-loader.md) | BindingLoader with async loadBindings() from YAML | completed |
|
|
47
|
+
| 5 | [binding-directory](./tasks/binding-directory.md) | loadBindingDir() for directory scanning of binding YAML files | completed |
|
|
48
|
+
| 6 | [schema-modes](./tasks/schema-modes.md) | Schema resolution modes: input_schema, output_schema, schema_ref, permissive fallback | completed |
|
|
49
|
+
|
|
50
|
+
## Progress
|
|
51
|
+
|
|
52
|
+
| Total | Completed | In Progress | Pending |
|
|
53
|
+
|-------|-----------|-------------|---------|
|
|
54
|
+
| 6 | 6 | 0 | 0 |
|
|
55
|
+
|
|
56
|
+
## Reference Documents
|
|
57
|
+
|
|
58
|
+
- `src/decorator.ts` -- FunctionModule class, module() factory, normalizeResult(), makeAutoId() (~110 lines)
|
|
59
|
+
- `src/bindings.ts` -- BindingLoader class with YAML loading and target resolution (~208 lines)
|
|
60
|
+
- `src/errors.ts` -- Binding error hierarchy (BindingInvalidTargetError, BindingFileInvalidError, etc.)
|
|
61
|
+
- `tests/test-decorator.test.ts` -- Unit tests for FunctionModule, module(), normalizeResult(), makeAutoId()
|
|
62
|
+
- `tests/test-bindings.test.ts` -- Unit tests for BindingLoader
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Implementation Plan: Decorator Bindings
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the TypeScript module definition and YAML binding system for apcore, providing a `FunctionModule` class that wraps execute functions with explicit TypeBox schemas, a `module()` factory for ergonomic module creation, and a `BindingLoader` for zero-code-modification module registration from YAML configuration files. Unlike the Python implementation, TypeScript cannot introspect types at runtime, so all schemas must be explicitly provided.
|
|
6
|
+
|
|
7
|
+
## Architecture Design
|
|
8
|
+
|
|
9
|
+
### Component Structure
|
|
10
|
+
|
|
11
|
+
- **FunctionModule** (`decorator.ts`, ~60 lines) -- Core wrapper class that associates an execute function with `inputSchema`/`outputSchema` (TypeBox `TSchema`), `description`, `documentation`, `tags`, `version`, `annotations`, `metadata`, and `examples`. The `execute()` method calls the wrapped function and passes the result through `normalizeResult()`. All optional fields default to sensible values (description defaults to `"Module {moduleId}"`, version to `"1.0.0"`, others to `null`).
|
|
12
|
+
|
|
13
|
+
- **normalizeResult()** (`decorator.ts`, ~4 lines) -- Exported utility that standardizes module return values: `null`/`undefined` becomes `{}`, plain `Record` objects pass through unchanged, and all other values (strings, numbers, booleans, arrays) are wrapped in `{ result: value }`. Exported (unlike Python where it is private) for use by `BindingLoader` and testing.
|
|
14
|
+
|
|
15
|
+
- **makeAutoId()** (`decorator.ts`, ~7 lines) -- ID generation utility that lowercases a name string, replaces non-alphanumeric/non-dot/non-underscore characters with underscores, and prefixes digit-leading segments. Simpler than Python's `__module__`+`__qualname__` approach since TypeScript functions lack equivalent introspectable qualified names.
|
|
16
|
+
|
|
17
|
+
- **module()** (`decorator.ts`, ~25 lines) -- Factory function that creates a `FunctionModule` from an options object. This is NOT a decorator (unlike Python's `@module`); it is a plain function call that returns a `FunctionModule`. Accepts optional `registry` for auto-registration. Generates an auto ID via `makeAutoId('anonymous')` when `id` is not provided.
|
|
18
|
+
|
|
19
|
+
- **BindingLoader** (`bindings.ts`, ~175 lines) -- YAML binding file loader that reads binding configuration, resolves `target` strings to callable functions via dynamic `import()`, builds schemas from JSON Schema definitions, and registers `FunctionModule` instances with a `Registry`. Supports target resolution for both exported functions (`module:funcName`) and class methods (`module:ClassName.methodName`). Schema resolution has three modes: inline `input_schema`/`output_schema`, external `schema_ref`, and permissive fallback.
|
|
20
|
+
|
|
21
|
+
- **Error Hierarchy** (`errors.ts`) -- Six binding-specific error classes extending `ModuleError`: `BindingInvalidTargetError`, `BindingModuleNotFoundError`, `BindingCallableNotFoundError`, `BindingNotCallableError`, `BindingSchemaMissingError`, `BindingFileInvalidError`.
|
|
22
|
+
|
|
23
|
+
### Data Flow
|
|
24
|
+
|
|
25
|
+
Module creation via `module()` factory:
|
|
26
|
+
|
|
27
|
+
1. Caller provides options object with `execute`, `inputSchema`, `outputSchema`, and optional metadata
|
|
28
|
+
2. `module()` resolves `moduleId` from `id` option or generates one via `makeAutoId('anonymous')`
|
|
29
|
+
3. `module()` constructs a `FunctionModule` with all provided options
|
|
30
|
+
4. If `registry` is provided, the module is registered via `registry.register()`
|
|
31
|
+
5. `FunctionModule.execute()` calls the wrapped function and passes the result through `normalizeResult()`
|
|
32
|
+
|
|
33
|
+
Module creation via YAML binding:
|
|
34
|
+
|
|
35
|
+
1. `BindingLoader.loadBindings()` reads and parses a YAML file containing a `bindings` array
|
|
36
|
+
2. For each binding entry, `resolveTarget()` dynamically imports the target module and resolves the callable
|
|
37
|
+
3. Schema is resolved via one of three modes: inline JSON Schema, external `schema_ref` file, or permissive fallback
|
|
38
|
+
4. A `FunctionModule` is constructed wrapping the resolved callable with the resolved schemas
|
|
39
|
+
5. The module is registered with the provided `Registry`
|
|
40
|
+
|
|
41
|
+
### Technical Choices and Rationale
|
|
42
|
+
|
|
43
|
+
- **Factory function instead of decorator**: TypeScript decorators (TC39 Stage 3) operate on class methods/fields, not standalone functions. A factory function is more natural for wrapping plain functions and aligns with TypeScript's functional composition patterns.
|
|
44
|
+
|
|
45
|
+
- **Explicit schemas required**: TypeScript types are erased at compile time. Unlike Python where `inspect.signature()` and type annotations can be read at runtime, TypeScript provides no runtime type metadata. Therefore, all schemas must be provided as TypeBox `TSchema` objects at the call site. This is the fundamental architectural difference from the Python implementation.
|
|
46
|
+
|
|
47
|
+
- **`normalizeResult()` exported**: Made public (unlike Python's private method) because `BindingLoader._createModuleFromBinding()` duplicates the normalization logic inline. The export enables both `FunctionModule.execute()` and future binding code to share the same normalization behavior. The duplication in `_createModuleFromBinding` is a known minor issue.
|
|
48
|
+
|
|
49
|
+
- **Async binding loading with sync file I/O**: `loadBindings()` and `loadBindingDir()` are `async` methods because target resolution uses dynamic `import()`, which is inherently asynchronous. However, YAML file reading uses `readFileSync` for simplicity since binding loading is a startup-time operation.
|
|
50
|
+
|
|
51
|
+
- **`JSON_SCHEMA_TYPE_MAP` dead code**: The `bindings.ts` file contains a `JSON_SCHEMA_TYPE_MAP` constant and `buildSchemaFromJsonSchema()` wrapper that delegates to `jsonSchemaToTypeBox()` from the schema system. The map is unused dead code from an earlier iteration and should be cleaned up.
|
|
52
|
+
|
|
53
|
+
## Task Breakdown
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
graph TD
|
|
57
|
+
T1[function-module] --> T2[module-factory]
|
|
58
|
+
T1 --> T3[explicit-schemas]
|
|
59
|
+
T3 --> T4[binding-loader]
|
|
60
|
+
T4 --> T5[binding-directory]
|
|
61
|
+
T4 --> T6[schema-modes]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Task ID | Title | Estimated Time | Dependencies |
|
|
65
|
+
|---------|-------|---------------|--------------|
|
|
66
|
+
| function-module | FunctionModule class with execute, schemas, normalizeResult() | 2h | none |
|
|
67
|
+
| module-factory | module() factory function with options object pattern | 2h | function-module |
|
|
68
|
+
| explicit-schemas | Explicit TypeBox schema passing | 1h | function-module |
|
|
69
|
+
| binding-loader | BindingLoader with async loadBindings() from YAML | 4h | explicit-schemas |
|
|
70
|
+
| binding-directory | loadBindingDir() for directory scanning | 2h | binding-loader |
|
|
71
|
+
| schema-modes | Schema resolution modes: inline, schema_ref, permissive | 3h | binding-loader |
|
|
72
|
+
|
|
73
|
+
## Risks and Considerations
|
|
74
|
+
|
|
75
|
+
- **`normalizeResult()` duplication**: The `_createModuleFromBinding()` method in `BindingLoader` duplicates the normalization logic inline rather than calling the exported `normalizeResult()`. This creates a maintenance risk if the normalization rules change. Should be refactored to use the shared utility.
|
|
76
|
+
- **Dead code**: `JSON_SCHEMA_TYPE_MAP` in `bindings.ts` is unreferenced dead code. It was superseded by `jsonSchemaToTypeBox()` from the schema system and should be removed in a cleanup pass.
|
|
77
|
+
- **Sync file I/O in async methods**: `readFileSync` is used inside async `loadBindings()`. This blocks the event loop during file reads. Acceptable for startup-time binding loading but would be problematic if called during request handling.
|
|
78
|
+
- **Target resolution security**: Dynamic `import()` in `resolveTarget()` can import arbitrary modules. There is no allowlist or sandboxing of target module paths. This is by design for flexibility but should be documented.
|
|
79
|
+
- **No schema validation of binding YAML structure**: The binding YAML structure (required keys like `module_id`, `target`) is validated via imperative checks rather than a formal schema. A YAML schema for binding files could improve error messages.
|
|
80
|
+
|
|
81
|
+
## Acceptance Criteria
|
|
82
|
+
|
|
83
|
+
- [x] `FunctionModule` wraps execute functions with explicit `inputSchema`/`outputSchema`
|
|
84
|
+
- [x] `FunctionModule.execute()` calls the wrapped function and normalizes the result
|
|
85
|
+
- [x] `normalizeResult()` handles null/undefined -> `{}`, Record -> passthrough, other -> `{ result: value }`
|
|
86
|
+
- [x] `makeAutoId()` generates valid module IDs from arbitrary strings
|
|
87
|
+
- [x] `module()` factory creates `FunctionModule` with all options forwarded
|
|
88
|
+
- [x] `module()` auto-registers with registry when `registry` option is provided
|
|
89
|
+
- [x] `module()` generates anonymous ID when `id` is not provided
|
|
90
|
+
- [x] `BindingLoader.loadBindings()` reads YAML and creates registered modules
|
|
91
|
+
- [x] `BindingLoader.resolveTarget()` resolves `module:funcName` and `module:Class.method` targets
|
|
92
|
+
- [x] `loadBindingDir()` scans directories for `*.binding.yaml` files
|
|
93
|
+
- [x] Schema resolution supports inline, schema_ref, and permissive fallback modes
|
|
94
|
+
- [x] All binding error types are thrown with correct error classes
|
|
95
|
+
- [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
|
|
96
|
+
|
|
97
|
+
## References
|
|
98
|
+
|
|
99
|
+
- `src/decorator.ts` -- FunctionModule, module(), normalizeResult(), makeAutoId()
|
|
100
|
+
- `src/bindings.ts` -- BindingLoader class
|
|
101
|
+
- `src/errors.ts` -- Binding error hierarchy
|
|
102
|
+
- `src/schema/loader.ts` -- jsonSchemaToTypeBox() used by binding schema resolution
|
|
103
|
+
- `tests/test-decorator.test.ts` -- FunctionModule and module() tests
|
|
104
|
+
- `tests/test-bindings.test.ts` -- BindingLoader tests
|