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