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,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"feature": "decorator-bindings",
|
|
3
|
+
"created": "2026-02-16T00:00:00Z",
|
|
4
|
+
"updated": "2026-02-16T00:00:00Z",
|
|
5
|
+
"status": "completed",
|
|
6
|
+
"execution_order": [
|
|
7
|
+
"function-module",
|
|
8
|
+
"module-factory",
|
|
9
|
+
"explicit-schemas",
|
|
10
|
+
"binding-loader",
|
|
11
|
+
"binding-directory",
|
|
12
|
+
"schema-modes"
|
|
13
|
+
],
|
|
14
|
+
"progress": {
|
|
15
|
+
"total_tasks": 6,
|
|
16
|
+
"completed": 6,
|
|
17
|
+
"in_progress": 0,
|
|
18
|
+
"pending": 0
|
|
19
|
+
},
|
|
20
|
+
"tasks": [
|
|
21
|
+
{
|
|
22
|
+
"id": "function-module",
|
|
23
|
+
"file": "tasks/function-module.md",
|
|
24
|
+
"title": "FunctionModule Class with Execute, Schemas, and normalizeResult()",
|
|
25
|
+
"status": "completed",
|
|
26
|
+
"started_at": "2026-02-16T08:00:00Z",
|
|
27
|
+
"completed_at": "2026-02-16T09:30:00Z",
|
|
28
|
+
"assignee": null,
|
|
29
|
+
"commits": []
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "module-factory",
|
|
33
|
+
"file": "tasks/module-factory.md",
|
|
34
|
+
"title": "module() Factory Function with Options Object Pattern",
|
|
35
|
+
"status": "completed",
|
|
36
|
+
"started_at": "2026-02-16T09:30:00Z",
|
|
37
|
+
"completed_at": "2026-02-16T11:00:00Z",
|
|
38
|
+
"assignee": null,
|
|
39
|
+
"commits": []
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "explicit-schemas",
|
|
43
|
+
"file": "tasks/explicit-schemas.md",
|
|
44
|
+
"title": "Explicit TypeBox Schema Passing",
|
|
45
|
+
"status": "completed",
|
|
46
|
+
"started_at": "2026-02-16T11:00:00Z",
|
|
47
|
+
"completed_at": "2026-02-16T11:45:00Z",
|
|
48
|
+
"assignee": null,
|
|
49
|
+
"commits": []
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "binding-loader",
|
|
53
|
+
"file": "tasks/binding-loader.md",
|
|
54
|
+
"title": "BindingLoader with Async loadBindings() from YAML",
|
|
55
|
+
"status": "completed",
|
|
56
|
+
"started_at": "2026-02-16T11:45:00Z",
|
|
57
|
+
"completed_at": "2026-02-16T14:30:00Z",
|
|
58
|
+
"assignee": null,
|
|
59
|
+
"commits": []
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "binding-directory",
|
|
63
|
+
"file": "tasks/binding-directory.md",
|
|
64
|
+
"title": "loadBindingDir() for Directory Scanning of Binding YAML Files",
|
|
65
|
+
"status": "completed",
|
|
66
|
+
"started_at": "2026-02-16T14:30:00Z",
|
|
67
|
+
"completed_at": "2026-02-16T16:00:00Z",
|
|
68
|
+
"assignee": null,
|
|
69
|
+
"commits": []
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "schema-modes",
|
|
73
|
+
"file": "tasks/schema-modes.md",
|
|
74
|
+
"title": "Schema Resolution Modes: Inline, schema_ref, Permissive Fallback",
|
|
75
|
+
"status": "completed",
|
|
76
|
+
"started_at": "2026-02-16T16:00:00Z",
|
|
77
|
+
"completed_at": "2026-02-16T18:30:00Z",
|
|
78
|
+
"assignee": null,
|
|
79
|
+
"commits": []
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"metadata": {
|
|
83
|
+
"source_doc": "planning/features/decorator-bindings.md",
|
|
84
|
+
"created_by": "code-forge",
|
|
85
|
+
"version": "1.0"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Task: loadBindingDir() for Directory Scanning of Binding YAML Files
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `loadBindingDir()` method on `BindingLoader` that scans a directory for binding YAML files matching a glob pattern (default `*.binding.yaml`), loads each one via `loadBindings()`, and returns all created `FunctionModule` instances. This enables convention-based module registration where all binding files in a directory are automatically discovered and loaded.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/bindings.ts` -- `BindingLoader.loadBindingDir()` method
|
|
10
|
+
- `tests/test-bindings.test.ts` -- Unit tests for loadBindingDir()
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Write failing tests (TDD)
|
|
15
|
+
|
|
16
|
+
Create tests for:
|
|
17
|
+
- **loadBindingDir throws on nonexistent directory**: Throws `BindingFileInvalidError` with "Directory does not exist"
|
|
18
|
+
- **loadBindingDir throws on file path (not directory)**: A path pointing to a file throws `BindingFileInvalidError`
|
|
19
|
+
- **loadBindingDir returns empty array for empty directory**: Directory with no matching files returns `[]`
|
|
20
|
+
- **loadBindingDir loads matching files in sorted order**: Files are processed alphabetically
|
|
21
|
+
- **loadBindingDir respects custom pattern**: `pattern: '*.modules.yaml'` only matches files with that suffix
|
|
22
|
+
|
|
23
|
+
### 2. Implement loadBindingDir()
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
async loadBindingDir(
|
|
27
|
+
dirPath: string,
|
|
28
|
+
registry: Registry,
|
|
29
|
+
pattern: string = '*.binding.yaml',
|
|
30
|
+
): Promise<FunctionModule[]> {
|
|
31
|
+
if (!existsSync(dirPath) || !statSync(dirPath).isDirectory()) {
|
|
32
|
+
throw new BindingFileInvalidError(dirPath, 'Directory does not exist');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const files = readdirSync(dirPath)
|
|
36
|
+
.filter((f) => {
|
|
37
|
+
// Simple glob matching: extract suffix after the wildcard
|
|
38
|
+
const suffix = pattern.replace('*', '');
|
|
39
|
+
return f.endsWith(suffix);
|
|
40
|
+
})
|
|
41
|
+
.sort();
|
|
42
|
+
|
|
43
|
+
const results: FunctionModule[] = [];
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
const fms = await this.loadBindings(join(dirPath, f), registry);
|
|
46
|
+
results.push(...fms);
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Key design decisions:
|
|
53
|
+
- **Simple glob matching**: Only supports `*` prefix patterns (e.g., `*.binding.yaml`). The wildcard is stripped and the remaining suffix is used for `endsWith()` matching. This covers the primary use case without introducing a glob library dependency.
|
|
54
|
+
- **Sorted file order**: Files are sorted alphabetically to ensure deterministic loading order across platforms.
|
|
55
|
+
- **Sequential loading**: Files are loaded sequentially (not in parallel) to maintain deterministic registration order in the registry. This is acceptable since binding loading is a startup-time operation.
|
|
56
|
+
- **Sync directory listing**: Uses `readdirSync` and `statSync` for directory operations (same rationale as `loadBindings`'s use of `readFileSync`).
|
|
57
|
+
|
|
58
|
+
### 3. Verify tests pass
|
|
59
|
+
|
|
60
|
+
Run `npx vitest run tests/test-bindings.test.ts`.
|
|
61
|
+
|
|
62
|
+
## Acceptance Criteria
|
|
63
|
+
|
|
64
|
+
- [x] `loadBindingDir()` scans directory for files matching the pattern
|
|
65
|
+
- [x] Default pattern is `'*.binding.yaml'`
|
|
66
|
+
- [x] Custom patterns like `'*.modules.yaml'` are supported
|
|
67
|
+
- [x] Files are processed in sorted (alphabetical) order
|
|
68
|
+
- [x] Each matching file is loaded via `loadBindings()` and results are aggregated
|
|
69
|
+
- [x] Throws `BindingFileInvalidError` when directory does not exist
|
|
70
|
+
- [x] Throws `BindingFileInvalidError` when path is a file, not a directory
|
|
71
|
+
- [x] Returns empty array when no files match the pattern
|
|
72
|
+
|
|
73
|
+
## Dependencies
|
|
74
|
+
|
|
75
|
+
- `binding-loader` -- Requires `BindingLoader` class with `loadBindings()` method
|
|
76
|
+
|
|
77
|
+
## Estimated Time
|
|
78
|
+
|
|
79
|
+
2 hours
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Task: BindingLoader with Async loadBindings() from YAML
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `BindingLoader` class that reads YAML binding configuration files, resolves target strings to callable functions via dynamic `import()`, wraps them in `FunctionModule` instances, and registers them with a `Registry`. This enables zero-code-modification module registration: existing functions can be exposed as apcore modules purely through YAML configuration.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/bindings.ts` -- `BindingLoader` class with `loadBindings()`, `resolveTarget()`, `_createModuleFromBinding()`
|
|
10
|
+
- `src/errors.ts` -- `BindingFileInvalidError`, `BindingInvalidTargetError`, `BindingModuleNotFoundError`, `BindingCallableNotFoundError`, `BindingNotCallableError`
|
|
11
|
+
- `tests/test-bindings.test.ts` -- Unit tests for BindingLoader
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
### 1. Write failing tests (TDD)
|
|
16
|
+
|
|
17
|
+
Create tests for:
|
|
18
|
+
- **BindingLoader instantiation**: `new BindingLoader()` creates a valid instance
|
|
19
|
+
- **resolveTarget throws on missing colon**: `resolveTarget('no_colon')` throws `BindingInvalidTargetError`
|
|
20
|
+
- **loadBindings throws on nonexistent file**: Throws `BindingFileInvalidError` with file path
|
|
21
|
+
- **loadBindings throws on invalid YAML**: Malformed YAML throws `BindingFileInvalidError`
|
|
22
|
+
- **loadBindings throws on missing bindings key**: YAML without `bindings` key throws `BindingFileInvalidError`
|
|
23
|
+
- **loadBindings throws on non-array bindings**: `bindings: "not_a_list"` throws `BindingFileInvalidError`
|
|
24
|
+
- **loadBindings throws on missing module_id**: Binding entry without `module_id` throws `BindingFileInvalidError`
|
|
25
|
+
- **loadBindings throws on missing target**: Binding entry without `target` throws `BindingFileInvalidError`
|
|
26
|
+
|
|
27
|
+
### 2. Implement resolveTarget()
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
async resolveTarget(targetString: string): Promise<(...args: unknown[]) => unknown> {
|
|
31
|
+
if (!targetString.includes(':')) {
|
|
32
|
+
throw new BindingInvalidTargetError(targetString);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const [modulePath, callableName] = targetString.split(':', 2);
|
|
36
|
+
|
|
37
|
+
let mod: Record<string, unknown>;
|
|
38
|
+
try {
|
|
39
|
+
mod = await import(modulePath);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
throw new BindingModuleNotFoundError(modulePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Handle Class.method syntax
|
|
45
|
+
if (callableName.includes('.')) {
|
|
46
|
+
const [className, methodName] = callableName.split('.', 2);
|
|
47
|
+
const cls = mod[className];
|
|
48
|
+
if (cls == null) throw new BindingCallableNotFoundError(className, modulePath);
|
|
49
|
+
let instance: Record<string, unknown>;
|
|
50
|
+
try {
|
|
51
|
+
instance = new (cls as new () => Record<string, unknown>)();
|
|
52
|
+
} catch {
|
|
53
|
+
throw new BindingCallableNotFoundError(callableName, modulePath);
|
|
54
|
+
}
|
|
55
|
+
const method = instance[methodName];
|
|
56
|
+
if (method == null) throw new BindingCallableNotFoundError(callableName, modulePath);
|
|
57
|
+
if (typeof method !== 'function') throw new BindingNotCallableError(targetString);
|
|
58
|
+
return method.bind(instance) as (...args: unknown[]) => unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle plain function export
|
|
62
|
+
const result = mod[callableName];
|
|
63
|
+
if (result == null) throw new BindingCallableNotFoundError(callableName, modulePath);
|
|
64
|
+
if (typeof result !== 'function') throw new BindingNotCallableError(targetString);
|
|
65
|
+
return result as (...args: unknown[]) => unknown;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Implement loadBindings()
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
async loadBindings(filePath: string, registry: Registry): Promise<FunctionModule[]> {
|
|
73
|
+
const bindingFileDir = dirname(filePath);
|
|
74
|
+
|
|
75
|
+
let content: string;
|
|
76
|
+
try {
|
|
77
|
+
content = readFileSync(filePath, 'utf-8');
|
|
78
|
+
} catch (e) {
|
|
79
|
+
throw new BindingFileInvalidError(filePath, String(e));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let data: unknown;
|
|
83
|
+
try {
|
|
84
|
+
data = yaml.load(content);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
throw new BindingFileInvalidError(filePath, `YAML parse error: ${e}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate structure
|
|
90
|
+
if (data === null || data === undefined) {
|
|
91
|
+
throw new BindingFileInvalidError(filePath, 'File is empty');
|
|
92
|
+
}
|
|
93
|
+
const dataObj = data as Record<string, unknown>;
|
|
94
|
+
if (!('bindings' in dataObj)) {
|
|
95
|
+
throw new BindingFileInvalidError(filePath, "Missing 'bindings' key");
|
|
96
|
+
}
|
|
97
|
+
const bindings = dataObj['bindings'];
|
|
98
|
+
if (!Array.isArray(bindings)) {
|
|
99
|
+
throw new BindingFileInvalidError(filePath, "'bindings' must be a list");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const results: FunctionModule[] = [];
|
|
103
|
+
for (const entry of bindings) {
|
|
104
|
+
const entryObj = entry as Record<string, unknown>;
|
|
105
|
+
if (!('module_id' in entryObj)) {
|
|
106
|
+
throw new BindingFileInvalidError(filePath, "Binding entry missing 'module_id'");
|
|
107
|
+
}
|
|
108
|
+
if (!('target' in entryObj)) {
|
|
109
|
+
throw new BindingFileInvalidError(filePath, "Binding entry missing 'target'");
|
|
110
|
+
}
|
|
111
|
+
const fm = await this._createModuleFromBinding(entryObj, bindingFileDir);
|
|
112
|
+
registry.register(entryObj['module_id'] as string, fm);
|
|
113
|
+
results.push(fm);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. Implement _createModuleFromBinding()
|
|
121
|
+
|
|
122
|
+
Private method that resolves the target callable, builds schemas (delegated to schema-modes task), and constructs a `FunctionModule`.
|
|
123
|
+
|
|
124
|
+
### 5. Verify tests pass
|
|
125
|
+
|
|
126
|
+
Run `npx vitest run tests/test-bindings.test.ts`.
|
|
127
|
+
|
|
128
|
+
## Acceptance Criteria
|
|
129
|
+
|
|
130
|
+
- [x] `BindingLoader` class is instantiable with no constructor arguments
|
|
131
|
+
- [x] `resolveTarget()` resolves `modulePath:funcName` to the exported function
|
|
132
|
+
- [x] `resolveTarget()` resolves `modulePath:ClassName.methodName` to a bound method
|
|
133
|
+
- [x] `resolveTarget()` throws `BindingInvalidTargetError` when target string has no colon
|
|
134
|
+
- [x] `resolveTarget()` throws `BindingModuleNotFoundError` when module cannot be imported
|
|
135
|
+
- [x] `resolveTarget()` throws `BindingCallableNotFoundError` when export is not found
|
|
136
|
+
- [x] `resolveTarget()` throws `BindingNotCallableError` when export is not a function
|
|
137
|
+
- [x] `loadBindings()` reads YAML, creates FunctionModules, and registers them
|
|
138
|
+
- [x] `loadBindings()` throws `BindingFileInvalidError` for missing files, invalid YAML, missing keys
|
|
139
|
+
- [x] Each binding entry requires `module_id` and `target` keys
|
|
140
|
+
|
|
141
|
+
## Dependencies
|
|
142
|
+
|
|
143
|
+
- `explicit-schemas` -- Requires understanding of explicit schema passing for FunctionModule construction
|
|
144
|
+
- `schema-system` (external) -- Consumes `jsonSchemaToTypeBox()` for JSON Schema conversion
|
|
145
|
+
|
|
146
|
+
## Estimated Time
|
|
147
|
+
|
|
148
|
+
4 hours
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Task: Explicit TypeBox Schema Passing
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Establish and enforce the pattern of explicit TypeBox schema passing for all module definitions. In Python's apcore, `@module` can introspect function signatures and type annotations at runtime to auto-generate schemas. TypeScript erases types at compile time, making this impossible. This task documents and validates the explicit schema requirement: every `FunctionModule` and `module()` call must receive `inputSchema` and `outputSchema` as TypeBox `TSchema` objects.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/decorator.ts` -- `FunctionModule` constructor requiring `inputSchema`/`outputSchema` as `TSchema`
|
|
10
|
+
- `tests/test-decorator.test.ts` -- Tests validating schema presence and TypeBox compatibility
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Write failing tests (TDD)
|
|
15
|
+
|
|
16
|
+
Create tests for:
|
|
17
|
+
- **FunctionModule requires inputSchema and outputSchema**: Constructing without schemas causes TypeScript compilation error (structural test via type assertions)
|
|
18
|
+
- **Schemas are stored as readonly TSchema**: `fm.inputSchema` and `fm.outputSchema` are accessible and match the provided TypeBox schemas
|
|
19
|
+
- **Complex schemas work**: Nested `Type.Object()` with optional fields, `Type.Array()`, `Type.Union()` all accepted as valid schemas
|
|
20
|
+
- **Schema is not validated at construction**: FunctionModule stores schemas but does not validate inputs against them (validation is the executor's responsibility)
|
|
21
|
+
|
|
22
|
+
### 2. Validate TypeBox schema integration
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Type, type TSchema } from '@sinclair/typebox';
|
|
26
|
+
|
|
27
|
+
// Simple schemas
|
|
28
|
+
const inputSchema = Type.Object({ name: Type.String() });
|
|
29
|
+
const outputSchema = Type.Object({ greeting: Type.String() });
|
|
30
|
+
|
|
31
|
+
// Complex schemas
|
|
32
|
+
const complexInput = Type.Object({
|
|
33
|
+
query: Type.String(),
|
|
34
|
+
options: Type.Optional(Type.Object({
|
|
35
|
+
limit: Type.Number(),
|
|
36
|
+
offset: Type.Number(),
|
|
37
|
+
})),
|
|
38
|
+
tags: Type.Array(Type.String()),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const complexOutput = Type.Object({
|
|
42
|
+
results: Type.Array(Type.Object({
|
|
43
|
+
id: Type.String(),
|
|
44
|
+
score: Type.Number(),
|
|
45
|
+
})),
|
|
46
|
+
total: Type.Integer(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Both simple and complex schemas are accepted by FunctionModule
|
|
50
|
+
const fm = new FunctionModule({
|
|
51
|
+
execute: (inputs) => ({ results: [], total: 0 }),
|
|
52
|
+
moduleId: 'search.query',
|
|
53
|
+
inputSchema: complexInput,
|
|
54
|
+
outputSchema: complexOutput,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Document the no-auto-schema rationale
|
|
59
|
+
|
|
60
|
+
The TypeScript implementation intentionally omits the Python `auto_schema` mode because:
|
|
61
|
+
- TypeScript types are erased at compile time; `typeof` at runtime only yields `"object"`, `"string"`, etc.
|
|
62
|
+
- There is no equivalent of Python's `inspect.signature()` or `typing.get_type_hints()` for TypeScript
|
|
63
|
+
- TypeBox provides a runtime-accessible schema representation that doubles as both a TypeScript type (via `Static<T>`) and a JSON Schema
|
|
64
|
+
- Explicit schemas are self-documenting and enable IDE autocompletion via `Static<typeof inputSchema>`
|
|
65
|
+
|
|
66
|
+
### 4. Verify tests pass
|
|
67
|
+
|
|
68
|
+
Run `npx vitest run tests/test-decorator.test.ts` and confirm schema-related tests pass.
|
|
69
|
+
|
|
70
|
+
## Acceptance Criteria
|
|
71
|
+
|
|
72
|
+
- [x] `FunctionModule` constructor requires `inputSchema: TSchema` and `outputSchema: TSchema`
|
|
73
|
+
- [x] TypeBox schemas of any complexity are accepted (`Type.Object`, `Type.Array`, `Type.Union`, `Type.Optional`, etc.)
|
|
74
|
+
- [x] `fm.inputSchema` and `fm.outputSchema` are accessible as `readonly TSchema`
|
|
75
|
+
- [x] No auto-schema mode exists (verified by absence of introspection code)
|
|
76
|
+
- [x] `module()` factory also requires explicit `inputSchema` and `outputSchema`
|
|
77
|
+
- [x] Complex nested schemas round-trip correctly through FunctionModule construction
|
|
78
|
+
|
|
79
|
+
## Dependencies
|
|
80
|
+
|
|
81
|
+
- `function-module` -- Requires `FunctionModule` class with `inputSchema`/`outputSchema` fields
|
|
82
|
+
|
|
83
|
+
## Estimated Time
|
|
84
|
+
|
|
85
|
+
1 hour
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Task: FunctionModule Class with Execute, Schemas, and normalizeResult()
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `FunctionModule` class that wraps an execute function with explicit TypeBox `inputSchema`/`outputSchema`, metadata properties (description, documentation, tags, version, annotations, metadata, examples), and a `normalizeResult()` utility for standardizing module return values. Also implement `makeAutoId()` for generating valid module IDs from arbitrary strings.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/decorator.ts` -- `FunctionModule` class, `normalizeResult()`, `makeAutoId()`
|
|
10
|
+
- `tests/test-decorator.test.ts` -- Unit tests for FunctionModule, normalizeResult(), makeAutoId()
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Write failing tests (TDD)
|
|
15
|
+
|
|
16
|
+
Create tests for:
|
|
17
|
+
- **FunctionModule wraps execute**: Construct with execute function, moduleId, inputSchema, outputSchema; call `execute()` and verify result
|
|
18
|
+
- **FunctionModule exposes properties**: Verify moduleId, description, documentation, tags, version are accessible
|
|
19
|
+
- **FunctionModule defaults**: Verify sensible defaults (description="Module {id}", documentation=null, tags=null, version="1.0.0", annotations=null, metadata=null, examples=null)
|
|
20
|
+
- **FunctionModule normalizes null return**: Execute function returning null produces `{}`
|
|
21
|
+
- **normalizeResult(null)**: Returns `{}`
|
|
22
|
+
- **normalizeResult(undefined)**: Returns `{}`
|
|
23
|
+
- **normalizeResult(Record)**: Passes through unchanged
|
|
24
|
+
- **normalizeResult(string)**: Returns `{ result: "hello" }`
|
|
25
|
+
- **normalizeResult(number)**: Returns `{ result: 42 }`
|
|
26
|
+
- **normalizeResult(boolean)**: Returns `{ result: true }`
|
|
27
|
+
- **normalizeResult(array)**: Returns `{ result: [1, 2, 3] }`
|
|
28
|
+
- **makeAutoId lowercases**: `makeAutoId('Hello World')` -> `'hello_world'`
|
|
29
|
+
- **makeAutoId preserves dots**: `makeAutoId('my.module.name')` -> `'my.module.name'`
|
|
30
|
+
- **makeAutoId prefixes digit-leading segments**: `makeAutoId('2fast.4you')` -> `'_2fast._4you'`
|
|
31
|
+
- **makeAutoId no-op on valid IDs**: `makeAutoId('valid_id')` -> `'valid_id'`
|
|
32
|
+
|
|
33
|
+
### 2. Implement normalizeResult()
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
export function normalizeResult(result: unknown): Record<string, unknown> {
|
|
37
|
+
if (result === null || result === undefined) return {};
|
|
38
|
+
if (typeof result === 'object' && !Array.isArray(result)) return result as Record<string, unknown>;
|
|
39
|
+
return { result };
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Implement makeAutoId()
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
export function makeAutoId(name: string): string {
|
|
47
|
+
let raw = name.toLowerCase();
|
|
48
|
+
raw = raw.replace(/[^a-z0-9_.]/g, '_');
|
|
49
|
+
const segments = raw.split('.');
|
|
50
|
+
return segments
|
|
51
|
+
.map((s) => (s && s[0] >= '0' && s[0] <= '9' ? '_' + s : s))
|
|
52
|
+
.join('.');
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 4. Implement FunctionModule class
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
export class FunctionModule {
|
|
60
|
+
readonly moduleId: string;
|
|
61
|
+
readonly inputSchema: TSchema;
|
|
62
|
+
readonly outputSchema: TSchema;
|
|
63
|
+
readonly description: string;
|
|
64
|
+
readonly documentation: string | null;
|
|
65
|
+
readonly tags: string[] | null;
|
|
66
|
+
readonly version: string;
|
|
67
|
+
readonly annotations: ModuleAnnotations | null;
|
|
68
|
+
readonly metadata: Record<string, unknown> | null;
|
|
69
|
+
readonly examples: ModuleExample[] | null;
|
|
70
|
+
|
|
71
|
+
private _executeFn: (inputs: Record<string, unknown>, context: Context) =>
|
|
72
|
+
Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
73
|
+
|
|
74
|
+
constructor(options: {
|
|
75
|
+
execute: (inputs: Record<string, unknown>, context: Context) =>
|
|
76
|
+
Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
77
|
+
moduleId: string;
|
|
78
|
+
inputSchema: TSchema;
|
|
79
|
+
outputSchema: TSchema;
|
|
80
|
+
description?: string;
|
|
81
|
+
documentation?: string | null;
|
|
82
|
+
tags?: string[] | null;
|
|
83
|
+
version?: string;
|
|
84
|
+
annotations?: ModuleAnnotations | null;
|
|
85
|
+
metadata?: Record<string, unknown> | null;
|
|
86
|
+
examples?: ModuleExample[] | null;
|
|
87
|
+
}) {
|
|
88
|
+
this.moduleId = options.moduleId;
|
|
89
|
+
this.inputSchema = options.inputSchema;
|
|
90
|
+
this.outputSchema = options.outputSchema;
|
|
91
|
+
this.description = options.description ?? `Module ${options.moduleId}`;
|
|
92
|
+
this.documentation = options.documentation ?? null;
|
|
93
|
+
this.tags = options.tags ?? null;
|
|
94
|
+
this.version = options.version ?? '1.0.0';
|
|
95
|
+
this.annotations = options.annotations ?? null;
|
|
96
|
+
this.metadata = options.metadata ?? null;
|
|
97
|
+
this.examples = options.examples ?? null;
|
|
98
|
+
this._executeFn = options.execute;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async execute(inputs: Record<string, unknown>, context: Context): Promise<Record<string, unknown>> {
|
|
102
|
+
const result = await this._executeFn(inputs, context);
|
|
103
|
+
return normalizeResult(result);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 5. Verify tests pass
|
|
109
|
+
|
|
110
|
+
Run `npx vitest run tests/test-decorator.test.ts` and confirm all FunctionModule, normalizeResult, and makeAutoId tests pass.
|
|
111
|
+
|
|
112
|
+
## Acceptance Criteria
|
|
113
|
+
|
|
114
|
+
- [x] `FunctionModule` constructor accepts options object with execute, moduleId, inputSchema, outputSchema, and optional metadata
|
|
115
|
+
- [x] `FunctionModule.execute()` calls wrapped function and passes result through `normalizeResult()`
|
|
116
|
+
- [x] Default description is `"Module {moduleId}"`, version is `"1.0.0"`, others default to `null`
|
|
117
|
+
- [x] `normalizeResult()` correctly handles null, undefined, Record, string, number, boolean, and array
|
|
118
|
+
- [x] `makeAutoId()` lowercases, replaces invalid characters, preserves dots, prefixes digit-leading segments
|
|
119
|
+
- [x] All fields are typed with `readonly` where appropriate
|
|
120
|
+
|
|
121
|
+
## Dependencies
|
|
122
|
+
|
|
123
|
+
None -- this is the foundational data structure for the decorator-bindings module.
|
|
124
|
+
|
|
125
|
+
## Estimated Time
|
|
126
|
+
|
|
127
|
+
2 hours
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Task: module() Factory Function with Options Object Pattern
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `module()` factory function that creates a `FunctionModule` from an options object. This is the primary ergonomic API for defining apcore modules in TypeScript. Unlike Python's `@module` decorator, this is a plain factory function that returns a `FunctionModule` instance. It supports optional auto-registration with a `Registry` and auto-generates a module ID via `makeAutoId('anonymous')` when no `id` is provided.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/decorator.ts` -- `module()` factory function
|
|
10
|
+
- `tests/test-decorator.test.ts` -- Unit tests for module() factory
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Write failing tests (TDD)
|
|
15
|
+
|
|
16
|
+
Create tests for:
|
|
17
|
+
- **Creates FunctionModule with correct properties**: `module({ id: 'factory.test', inputSchema, outputSchema, execute })` returns `FunctionModule` with correct moduleId and description
|
|
18
|
+
- **Generates auto ID when not provided**: `module({ inputSchema, outputSchema, execute })` creates module with moduleId `'anonymous'`
|
|
19
|
+
- **Passes through optional fields**: documentation, tags, version, metadata are forwarded to FunctionModule
|
|
20
|
+
- **Auto-registers with registry**: When `registry` option is provided, the module is registered via `registry.register()`
|
|
21
|
+
|
|
22
|
+
### 2. Implement module() factory
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
export function module(options: {
|
|
26
|
+
id?: string;
|
|
27
|
+
inputSchema: TSchema;
|
|
28
|
+
outputSchema: TSchema;
|
|
29
|
+
description?: string;
|
|
30
|
+
documentation?: string | null;
|
|
31
|
+
annotations?: ModuleAnnotations | null;
|
|
32
|
+
tags?: string[] | null;
|
|
33
|
+
version?: string;
|
|
34
|
+
metadata?: Record<string, unknown> | null;
|
|
35
|
+
examples?: ModuleExample[] | null;
|
|
36
|
+
execute: (inputs: Record<string, unknown>, context: Context) =>
|
|
37
|
+
Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
38
|
+
registry?: { register(moduleId: string, module: unknown): void } | null;
|
|
39
|
+
}): FunctionModule {
|
|
40
|
+
const moduleId = options.id ?? makeAutoId('anonymous');
|
|
41
|
+
|
|
42
|
+
const fm = new FunctionModule({
|
|
43
|
+
execute: options.execute,
|
|
44
|
+
moduleId,
|
|
45
|
+
inputSchema: options.inputSchema,
|
|
46
|
+
outputSchema: options.outputSchema,
|
|
47
|
+
description: options.description,
|
|
48
|
+
documentation: options.documentation,
|
|
49
|
+
tags: options.tags,
|
|
50
|
+
version: options.version,
|
|
51
|
+
annotations: options.annotations,
|
|
52
|
+
metadata: options.metadata,
|
|
53
|
+
examples: options.examples,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (options.registry) {
|
|
57
|
+
options.registry.register(fm.moduleId, fm);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return fm;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Key design decisions:
|
|
65
|
+
- The `registry` option uses a structural type `{ register(moduleId: string, module: unknown): void }` rather than importing the `Registry` class, enabling loose coupling and easier testing
|
|
66
|
+
- When `id` is omitted, `makeAutoId('anonymous')` produces `'anonymous'` (a valid ID)
|
|
67
|
+
- All optional metadata fields are forwarded to `FunctionModule` without transformation
|
|
68
|
+
|
|
69
|
+
### 3. Verify tests pass
|
|
70
|
+
|
|
71
|
+
Run `npx vitest run tests/test-decorator.test.ts` and confirm all module() factory tests pass.
|
|
72
|
+
|
|
73
|
+
## Acceptance Criteria
|
|
74
|
+
|
|
75
|
+
- [x] `module()` returns a `FunctionModule` instance
|
|
76
|
+
- [x] `module()` forwards all options to `FunctionModule` constructor
|
|
77
|
+
- [x] `module({ id: 'x', ... })` creates module with `moduleId === 'x'`
|
|
78
|
+
- [x] `module({ ... })` without `id` creates module with `moduleId === 'anonymous'`
|
|
79
|
+
- [x] `module({ registry, ... })` auto-registers the module with the provided registry
|
|
80
|
+
- [x] `module({ registry: null, ... })` does not attempt registration
|
|
81
|
+
- [x] `registry` option uses structural typing for loose coupling
|
|
82
|
+
|
|
83
|
+
## Dependencies
|
|
84
|
+
|
|
85
|
+
- `function-module` -- Requires `FunctionModule` class and `makeAutoId()`
|
|
86
|
+
|
|
87
|
+
## Estimated Time
|
|
88
|
+
|
|
89
|
+
2 hours
|