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,157 @@
|
|
|
1
|
+
# Task: Dependencies
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement `resolveDependencies()` using Kahn's topological sort algorithm to determine a valid module load order. The function builds a directed acyclic graph from module dependency lists, processes nodes in zero-in-degree order (sorted for determinism), detects cycles with path extraction, and throws descriptive errors for missing required dependencies.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/registry/dependencies.ts` -- Dependency resolution implementation
|
|
10
|
+
- `src/registry/types.ts` -- `DependencyInfo` interface
|
|
11
|
+
- `src/errors.ts` -- `CircularDependencyError`, `ModuleLoadError`
|
|
12
|
+
- `tests/registry/test-dependencies.test.ts` -- Dependency resolution tests
|
|
13
|
+
|
|
14
|
+
## Steps (TDD)
|
|
15
|
+
|
|
16
|
+
### Step 1: Basic topological sort with no dependencies
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { resolveDependencies } from '../../src/registry/dependencies.js';
|
|
21
|
+
|
|
22
|
+
describe('resolveDependencies', () => {
|
|
23
|
+
it('should return all modules in sorted order when no dependencies exist', () => {
|
|
24
|
+
const modules: Array<[string, []]> = [
|
|
25
|
+
['mod.c', []],
|
|
26
|
+
['mod.a', []],
|
|
27
|
+
['mod.b', []],
|
|
28
|
+
];
|
|
29
|
+
const order = resolveDependencies(modules);
|
|
30
|
+
expect(order).toEqual(['mod.a', 'mod.b', 'mod.c']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return empty array for empty input', () => {
|
|
34
|
+
expect(resolveDependencies([])).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Step 2: Linear dependency chain
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
it('should resolve linear dependency chain', () => {
|
|
43
|
+
const modules: Array<[string, Array<{ moduleId: string; version: string | null; optional: boolean }>]> = [
|
|
44
|
+
['mod.c', [{ moduleId: 'mod.b', version: null, optional: false }]],
|
|
45
|
+
['mod.b', [{ moduleId: 'mod.a', version: null, optional: false }]],
|
|
46
|
+
['mod.a', []],
|
|
47
|
+
];
|
|
48
|
+
const order = resolveDependencies(modules);
|
|
49
|
+
expect(order).toEqual(['mod.a', 'mod.b', 'mod.c']);
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Step 3: Diamond dependency pattern
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
it('should handle diamond dependency pattern', () => {
|
|
57
|
+
const modules: Array<[string, Array<{ moduleId: string; version: string | null; optional: boolean }>]> = [
|
|
58
|
+
['mod.d', [
|
|
59
|
+
{ moduleId: 'mod.b', version: null, optional: false },
|
|
60
|
+
{ moduleId: 'mod.c', version: null, optional: false },
|
|
61
|
+
]],
|
|
62
|
+
['mod.b', [{ moduleId: 'mod.a', version: null, optional: false }]],
|
|
63
|
+
['mod.c', [{ moduleId: 'mod.a', version: null, optional: false }]],
|
|
64
|
+
['mod.a', []],
|
|
65
|
+
];
|
|
66
|
+
const order = resolveDependencies(modules);
|
|
67
|
+
expect(order.indexOf('mod.a')).toBeLessThan(order.indexOf('mod.b'));
|
|
68
|
+
expect(order.indexOf('mod.a')).toBeLessThan(order.indexOf('mod.c'));
|
|
69
|
+
expect(order.indexOf('mod.b')).toBeLessThan(order.indexOf('mod.d'));
|
|
70
|
+
expect(order.indexOf('mod.c')).toBeLessThan(order.indexOf('mod.d'));
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Step 4: Circular dependency detection
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
it('should throw CircularDependencyError for circular dependencies', () => {
|
|
78
|
+
const modules: Array<[string, Array<{ moduleId: string; version: string | null; optional: boolean }>]> = [
|
|
79
|
+
['mod.a', [{ moduleId: 'mod.b', version: null, optional: false }]],
|
|
80
|
+
['mod.b', [{ moduleId: 'mod.a', version: null, optional: false }]],
|
|
81
|
+
];
|
|
82
|
+
expect(() => resolveDependencies(modules)).toThrow(/[Cc]ircular/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should detect three-node cycle', () => {
|
|
86
|
+
const modules: Array<[string, Array<{ moduleId: string; version: string | null; optional: boolean }>]> = [
|
|
87
|
+
['mod.a', [{ moduleId: 'mod.b', version: null, optional: false }]],
|
|
88
|
+
['mod.b', [{ moduleId: 'mod.c', version: null, optional: false }]],
|
|
89
|
+
['mod.c', [{ moduleId: 'mod.a', version: null, optional: false }]],
|
|
90
|
+
];
|
|
91
|
+
expect(() => resolveDependencies(modules)).toThrow(/[Cc]ircular/);
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Step 5: Missing required dependency
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
it('should throw ModuleLoadError for missing required dependency', () => {
|
|
99
|
+
const modules: Array<[string, Array<{ moduleId: string; version: string | null; optional: boolean }>]> = [
|
|
100
|
+
['mod.a', [{ moduleId: 'mod.missing', version: null, optional: false }]],
|
|
101
|
+
];
|
|
102
|
+
expect(() => resolveDependencies(modules)).toThrow(/not found/);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Step 6: Optional missing dependency is silently ignored
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
it('should skip optional dependencies that are not in known IDs', () => {
|
|
110
|
+
const modules: Array<[string, Array<{ moduleId: string; version: string | null; optional: boolean }>]> = [
|
|
111
|
+
['mod.a', [{ moduleId: 'mod.optional', version: null, optional: true }]],
|
|
112
|
+
];
|
|
113
|
+
const order = resolveDependencies(modules);
|
|
114
|
+
expect(order).toEqual(['mod.a']);
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Step 7: Implement extractCycle() for cycle path reporting
|
|
119
|
+
|
|
120
|
+
The `extractCycle()` helper traces from a remaining node through its dependency edges to find and return the cycle path. This cycle path is included in the `CircularDependencyError` for debugging.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
it('should include cycle path in error message', () => {
|
|
124
|
+
const modules: Array<[string, Array<{ moduleId: string; version: string | null; optional: boolean }>]> = [
|
|
125
|
+
['a', [{ moduleId: 'b', version: null, optional: false }]],
|
|
126
|
+
['b', [{ moduleId: 'a', version: null, optional: false }]],
|
|
127
|
+
['c', []],
|
|
128
|
+
];
|
|
129
|
+
try {
|
|
130
|
+
resolveDependencies(modules);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
const msg = (e as Error).message;
|
|
133
|
+
expect(msg).toMatch(/a.*b|b.*a/);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Acceptance Criteria
|
|
139
|
+
|
|
140
|
+
- [x] Independent modules are returned in alphabetically sorted order
|
|
141
|
+
- [x] Linear dependency chains are resolved in correct order
|
|
142
|
+
- [x] Diamond dependency patterns are resolved with all constraints satisfied
|
|
143
|
+
- [x] `CircularDependencyError` is thrown for two-node, three-node, and larger cycles
|
|
144
|
+
- [x] `CircularDependencyError` includes the extracted cycle path
|
|
145
|
+
- [x] `ModuleLoadError` is thrown for missing required dependencies
|
|
146
|
+
- [x] Optional missing dependencies are silently skipped
|
|
147
|
+
- [x] Zero-in-degree nodes and dependents are processed in sorted order for determinism
|
|
148
|
+
- [x] Empty input returns empty array
|
|
149
|
+
- [x] All tests pass with `vitest`
|
|
150
|
+
|
|
151
|
+
## Dependencies
|
|
152
|
+
|
|
153
|
+
- `types` task (DependencyInfo interface)
|
|
154
|
+
|
|
155
|
+
## Estimated Time
|
|
156
|
+
|
|
157
|
+
3 hours
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Task: Entry Point
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement `resolveEntryPoint()` which uses async `import()` to dynamically load a TypeScript/JavaScript module file and resolve the appropriate module object. Supports three resolution strategies: (1) metadata-driven class name override via `entry_point` key, (2) default export auto-detection, (3) single named export auto-detection. Includes `snakeToPascal()` utility and `isModuleClass()` duck-type checker.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/registry/entry-point.ts` -- Entry point resolution implementation
|
|
10
|
+
- `src/registry/validation.ts` -- `validateModule()` (imported for reference)
|
|
11
|
+
- `src/errors.ts` -- `ModuleLoadError`
|
|
12
|
+
- `tests/registry/test-entry-point.test.ts` -- Entry point tests with dynamic module fixtures
|
|
13
|
+
|
|
14
|
+
## Steps (TDD)
|
|
15
|
+
|
|
16
|
+
### Step 1: Implement snakeToPascal() utility
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { snakeToPascal } from '../../src/registry/entry-point.js';
|
|
21
|
+
|
|
22
|
+
describe('snakeToPascal', () => {
|
|
23
|
+
it('should convert snake_case to PascalCase', () => {
|
|
24
|
+
expect(snakeToPascal('hello_world')).toBe('HelloWorld');
|
|
25
|
+
expect(snakeToPascal('my_module_name')).toBe('MyModuleName');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle single word', () => {
|
|
29
|
+
expect(snakeToPascal('hello')).toBe('Hello');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return empty string for empty input', () => {
|
|
33
|
+
expect(snakeToPascal('')).toBe('');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Step 2: Implement isModuleClass() duck-type checker
|
|
39
|
+
|
|
40
|
+
The checker validates that an object has `inputSchema` (object), `outputSchema` (object), `description` (string), and `execute` (function):
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// isModuleClass is private, tested indirectly through resolveEntryPoint
|
|
44
|
+
// but its logic is:
|
|
45
|
+
function isModuleClass(obj: unknown): boolean {
|
|
46
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
47
|
+
const record = obj as Record<string, unknown>;
|
|
48
|
+
return (
|
|
49
|
+
record['inputSchema'] != null &&
|
|
50
|
+
typeof record['inputSchema'] === 'object' &&
|
|
51
|
+
record['outputSchema'] != null &&
|
|
52
|
+
typeof record['outputSchema'] === 'object' &&
|
|
53
|
+
typeof record['description'] === 'string' &&
|
|
54
|
+
typeof record['execute'] === 'function'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Step 3: Resolve default export
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { resolveEntryPoint } from '../../src/registry/entry-point.js';
|
|
63
|
+
|
|
64
|
+
describe('resolveEntryPoint', () => {
|
|
65
|
+
it('should resolve default export that passes isModuleClass', async () => {
|
|
66
|
+
// Given a .ts file with a default export containing inputSchema,
|
|
67
|
+
// outputSchema, description, and execute
|
|
68
|
+
const mod = await resolveEntryPoint('/path/to/valid_default_module.ts');
|
|
69
|
+
expect(mod).toBeDefined();
|
|
70
|
+
const obj = mod as Record<string, unknown>;
|
|
71
|
+
expect(typeof obj['execute']).toBe('function');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Step 4: Resolve single named export
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
it('should resolve single named export when no valid default exists', async () => {
|
|
80
|
+
// Given a .ts file with one named export that passes isModuleClass
|
|
81
|
+
const mod = await resolveEntryPoint('/path/to/named_export_module.ts');
|
|
82
|
+
expect(mod).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Step 5: Metadata entry_point override
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
it('should use metadata entry_point to select specific export', async () => {
|
|
90
|
+
const meta = { entry_point: 'module:MyModule' };
|
|
91
|
+
const mod = await resolveEntryPoint('/path/to/multi_export.ts', meta);
|
|
92
|
+
expect(mod).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Step 6: Throw on import failure
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
it('should throw ModuleLoadError when import fails', async () => {
|
|
100
|
+
await expect(
|
|
101
|
+
resolveEntryPoint('/nonexistent/module.ts'),
|
|
102
|
+
).rejects.toThrow(/Failed to import/);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Step 7: Throw on no module class found
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
it('should throw ModuleLoadError when no Module subclass found', async () => {
|
|
110
|
+
// Given a .ts file with no exports matching isModuleClass
|
|
111
|
+
await expect(
|
|
112
|
+
resolveEntryPoint('/path/to/no_module.ts'),
|
|
113
|
+
).rejects.toThrow(/No Module subclass found/);
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Step 8: Throw on ambiguous multiple candidates
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
it('should throw ModuleLoadError for ambiguous multiple exports', async () => {
|
|
121
|
+
// Given a .ts file with two named exports both matching isModuleClass
|
|
122
|
+
await expect(
|
|
123
|
+
resolveEntryPoint('/path/to/ambiguous_module.ts'),
|
|
124
|
+
).rejects.toThrow(/Ambiguous entry point/);
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Acceptance Criteria
|
|
129
|
+
|
|
130
|
+
- [x] `snakeToPascal()` correctly converts snake_case to PascalCase
|
|
131
|
+
- [x] `isModuleClass()` checks for inputSchema (object), outputSchema (object), description (string), execute (function)
|
|
132
|
+
- [x] Default export is preferred if it passes `isModuleClass()`
|
|
133
|
+
- [x] Single named export is resolved when default export is not a valid module
|
|
134
|
+
- [x] Metadata `entry_point` override selects a specific named export by class name
|
|
135
|
+
- [x] `ModuleLoadError` thrown when async `import()` fails
|
|
136
|
+
- [x] `ModuleLoadError` thrown when no module class is found in exports
|
|
137
|
+
- [x] `ModuleLoadError` thrown when multiple ambiguous candidates exist
|
|
138
|
+
- [x] Function is async, returning `Promise<unknown>`
|
|
139
|
+
- [x] All tests pass with `vitest`
|
|
140
|
+
|
|
141
|
+
## Dependencies
|
|
142
|
+
|
|
143
|
+
- `types` task (interfaces for context)
|
|
144
|
+
- `validation` task (structural validation reference)
|
|
145
|
+
|
|
146
|
+
## Estimated Time
|
|
147
|
+
|
|
148
|
+
2 hours
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Task: Metadata
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement YAML metadata loading and merging functions: `loadMetadata()` for parsing `_meta.yaml` companion files, `parseDependencies()` for converting raw dependency arrays to typed `DependencyInfo[]`, `mergeModuleMetadata()` for resolving code-level vs YAML-level property conflicts, and `loadIdMap()` for loading canonical ID override mappings from a YAML file.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/registry/metadata.ts` -- Metadata function implementations
|
|
10
|
+
- `src/registry/types.ts` -- `DependencyInfo` interface
|
|
11
|
+
- `src/errors.ts` -- `ConfigError`, `ConfigNotFoundError`
|
|
12
|
+
- `tests/registry/test-metadata.test.ts` -- Metadata tests with temp YAML fixtures
|
|
13
|
+
|
|
14
|
+
## Steps (TDD)
|
|
15
|
+
|
|
16
|
+
### Step 1: Implement loadMetadata() for YAML parsing
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
20
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { loadMetadata } from '../../src/registry/metadata.js';
|
|
24
|
+
|
|
25
|
+
describe('loadMetadata', () => {
|
|
26
|
+
let tempDir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
tempDir = mkdtempSync(join(tmpdir(), 'meta-'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should parse valid YAML metadata file', () => {
|
|
37
|
+
const metaPath = join(tempDir, 'mod_meta.yaml');
|
|
38
|
+
writeFileSync(metaPath, 'description: A test module\nversion: "2.0.0"\ntags:\n - math\n');
|
|
39
|
+
const meta = loadMetadata(metaPath);
|
|
40
|
+
expect(meta['description']).toBe('A test module');
|
|
41
|
+
expect(meta['version']).toBe('2.0.0');
|
|
42
|
+
expect(meta['tags']).toEqual(['math']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return empty record for missing file', () => {
|
|
46
|
+
const meta = loadMetadata('/nonexistent/path.yaml');
|
|
47
|
+
expect(meta).toEqual({});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should throw ConfigError for invalid YAML', () => {
|
|
51
|
+
const metaPath = join(tempDir, 'bad_meta.yaml');
|
|
52
|
+
writeFileSync(metaPath, '{ invalid yaml [[[');
|
|
53
|
+
expect(() => loadMetadata(metaPath)).toThrow();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should throw ConfigError for non-mapping YAML', () => {
|
|
57
|
+
const metaPath = join(tempDir, 'list_meta.yaml');
|
|
58
|
+
writeFileSync(metaPath, '- item1\n- item2\n');
|
|
59
|
+
expect(() => loadMetadata(metaPath)).toThrow();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Step 2: Implement parseDependencies()
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { parseDependencies } from '../../src/registry/metadata.js';
|
|
68
|
+
|
|
69
|
+
describe('parseDependencies', () => {
|
|
70
|
+
it('should convert raw dependency array to DependencyInfo[]', () => {
|
|
71
|
+
const raw = [
|
|
72
|
+
{ module_id: 'core.auth', version: '1.0.0', optional: false },
|
|
73
|
+
{ module_id: 'utils.logger', optional: true },
|
|
74
|
+
];
|
|
75
|
+
const deps = parseDependencies(raw);
|
|
76
|
+
expect(deps).toHaveLength(2);
|
|
77
|
+
expect(deps[0]).toEqual({ moduleId: 'core.auth', version: '1.0.0', optional: false });
|
|
78
|
+
expect(deps[1]).toEqual({ moduleId: 'utils.logger', version: null, optional: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should skip entries without module_id', () => {
|
|
82
|
+
const raw = [{ version: '1.0.0' }, { module_id: 'valid' }];
|
|
83
|
+
const deps = parseDependencies(raw as Array<Record<string, unknown>>);
|
|
84
|
+
expect(deps).toHaveLength(1);
|
|
85
|
+
expect(deps[0].moduleId).toBe('valid');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return empty array for empty input', () => {
|
|
89
|
+
expect(parseDependencies([])).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Step 3: Implement mergeModuleMetadata()
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { mergeModuleMetadata } from '../../src/registry/metadata.js';
|
|
98
|
+
|
|
99
|
+
describe('mergeModuleMetadata', () => {
|
|
100
|
+
it('should prefer YAML metadata over code-level properties', () => {
|
|
101
|
+
const moduleObj = {
|
|
102
|
+
description: 'Code description',
|
|
103
|
+
name: 'CodeName',
|
|
104
|
+
version: '1.0.0',
|
|
105
|
+
tags: ['code-tag'],
|
|
106
|
+
};
|
|
107
|
+
const meta = {
|
|
108
|
+
description: 'YAML description',
|
|
109
|
+
name: 'YAMLName',
|
|
110
|
+
version: '2.0.0',
|
|
111
|
+
tags: ['yaml-tag'],
|
|
112
|
+
};
|
|
113
|
+
const merged = mergeModuleMetadata(moduleObj, meta);
|
|
114
|
+
expect(merged['description']).toBe('YAML description');
|
|
115
|
+
expect(merged['name']).toBe('YAMLName');
|
|
116
|
+
expect(merged['version']).toBe('2.0.0');
|
|
117
|
+
expect(merged['tags']).toEqual(['yaml-tag']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should fall back to code-level properties when YAML is empty', () => {
|
|
121
|
+
const moduleObj = {
|
|
122
|
+
description: 'Code desc',
|
|
123
|
+
name: 'CodeName',
|
|
124
|
+
version: '1.5.0',
|
|
125
|
+
tags: ['fallback'],
|
|
126
|
+
};
|
|
127
|
+
const merged = mergeModuleMetadata(moduleObj, {});
|
|
128
|
+
expect(merged['description']).toBe('Code desc');
|
|
129
|
+
expect(merged['name']).toBe('CodeName');
|
|
130
|
+
expect(merged['version']).toBe('1.5.0');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should shallow-merge metadata records', () => {
|
|
134
|
+
const moduleObj = { metadata: { author: 'dev', env: 'prod' } };
|
|
135
|
+
const meta = { metadata: { env: 'staging', team: 'backend' } };
|
|
136
|
+
const merged = mergeModuleMetadata(
|
|
137
|
+
moduleObj as Record<string, unknown>,
|
|
138
|
+
meta as Record<string, unknown>,
|
|
139
|
+
);
|
|
140
|
+
const mergedMeta = merged['metadata'] as Record<string, unknown>;
|
|
141
|
+
expect(mergedMeta['author']).toBe('dev');
|
|
142
|
+
expect(mergedMeta['env']).toBe('staging');
|
|
143
|
+
expect(mergedMeta['team']).toBe('backend');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Step 4: Implement loadIdMap()
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { loadIdMap } from '../../src/registry/metadata.js';
|
|
152
|
+
|
|
153
|
+
describe('loadIdMap', () => {
|
|
154
|
+
it('should load YAML ID map with mappings list', () => {
|
|
155
|
+
const mapPath = join(tempDir, 'id_map.yaml');
|
|
156
|
+
writeFileSync(
|
|
157
|
+
mapPath,
|
|
158
|
+
'mappings:\n - file: math/add.ts\n id: custom.add\n - file: utils/log.ts\n id: custom.log\n',
|
|
159
|
+
);
|
|
160
|
+
const idMap = loadIdMap(mapPath);
|
|
161
|
+
expect(idMap['math/add.ts']['id']).toBe('custom.add');
|
|
162
|
+
expect(idMap['utils/log.ts']['id']).toBe('custom.log');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should throw ConfigNotFoundError for missing file', () => {
|
|
166
|
+
expect(() => loadIdMap('/nonexistent/id_map.yaml')).toThrow();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should throw ConfigError for missing mappings key', () => {
|
|
170
|
+
const mapPath = join(tempDir, 'bad_map.yaml');
|
|
171
|
+
writeFileSync(mapPath, 'other_key: value\n');
|
|
172
|
+
expect(() => loadIdMap(mapPath)).toThrow(/mappings/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Acceptance Criteria
|
|
178
|
+
|
|
179
|
+
- [x] `loadMetadata()` parses valid YAML into a `Record<string, unknown>`
|
|
180
|
+
- [x] `loadMetadata()` returns `{}` for non-existent files
|
|
181
|
+
- [x] `loadMetadata()` throws `ConfigError` for invalid YAML or non-mapping content
|
|
182
|
+
- [x] `parseDependencies()` converts raw arrays to typed `DependencyInfo[]`
|
|
183
|
+
- [x] `parseDependencies()` skips entries without `module_id`
|
|
184
|
+
- [x] `parseDependencies()` defaults `version` to `null` and `optional` to `false`
|
|
185
|
+
- [x] `mergeModuleMetadata()` prefers YAML values over code values for description, name, tags, version, annotations, examples, documentation
|
|
186
|
+
- [x] `mergeModuleMetadata()` shallow-merges metadata records (code spread first, then YAML)
|
|
187
|
+
- [x] `loadIdMap()` parses YAML with `mappings` list into file-to-record map
|
|
188
|
+
- [x] `loadIdMap()` throws `ConfigNotFoundError` for missing files
|
|
189
|
+
- [x] `loadIdMap()` throws `ConfigError` for missing or non-array `mappings` key
|
|
190
|
+
- [x] All tests pass with `vitest`
|
|
191
|
+
|
|
192
|
+
## Dependencies
|
|
193
|
+
|
|
194
|
+
- `types` task (DependencyInfo interface)
|
|
195
|
+
|
|
196
|
+
## Estimated Time
|
|
197
|
+
|
|
198
|
+
2 hours
|