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,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