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,261 @@
|
|
|
1
|
+
# Task: Schema Export
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement schema query and export functions that extract module schema information from the registry and serialize it in JSON or YAML formats. Supports strict mode (via `toStrictSchema()` for OpenAI function calling compatibility), compact mode (truncated descriptions, stripped extensions), and LLM export profiles (MCP, OpenAI, Anthropic, Generic) through the `SchemaExporter` class. Functions operate on individual modules or across all registered modules.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/registry/schema-export.ts` -- Schema export function implementations
|
|
10
|
+
- `src/registry/registry.ts` -- `Registry` class (consumed as dependency)
|
|
11
|
+
- `src/schema/exporter.ts` -- `SchemaExporter` class
|
|
12
|
+
- `src/schema/strict.ts` -- `toStrictSchema()`, `stripExtensions()`
|
|
13
|
+
- `src/schema/types.ts` -- `ExportProfile`, `SchemaDefinition`
|
|
14
|
+
- `src/module.ts` -- `ModuleAnnotations`, `ModuleExample`
|
|
15
|
+
- `src/errors.ts` -- `ModuleNotFoundError`
|
|
16
|
+
- `tests/registry/test-schema-export.test.ts` -- Schema export tests
|
|
17
|
+
|
|
18
|
+
## Steps (TDD)
|
|
19
|
+
|
|
20
|
+
### Step 1: Implement getSchema() for single module
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { Registry } from '../../src/registry/registry.js';
|
|
25
|
+
import { getSchema } from '../../src/registry/schema-export.js';
|
|
26
|
+
|
|
27
|
+
describe('getSchema', () => {
|
|
28
|
+
it('should extract schema record from registered module', () => {
|
|
29
|
+
const reg = new Registry();
|
|
30
|
+
const mod = {
|
|
31
|
+
name: 'Adder',
|
|
32
|
+
description: 'Adds two numbers',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
tags: ['math'],
|
|
35
|
+
inputSchema: { type: 'object', properties: { a: { type: 'number' } } },
|
|
36
|
+
outputSchema: { type: 'object', properties: { result: { type: 'number' } } },
|
|
37
|
+
annotations: null,
|
|
38
|
+
examples: [],
|
|
39
|
+
};
|
|
40
|
+
reg.register('math.add', mod);
|
|
41
|
+
const schema = getSchema(reg, 'math.add');
|
|
42
|
+
|
|
43
|
+
expect(schema).not.toBeNull();
|
|
44
|
+
expect(schema!['module_id']).toBe('math.add');
|
|
45
|
+
expect(schema!['name']).toBe('Adder');
|
|
46
|
+
expect(schema!['description']).toBe('Adds two numbers');
|
|
47
|
+
expect(schema!['version']).toBe('1.0.0');
|
|
48
|
+
expect(schema!['tags']).toEqual(['math']);
|
|
49
|
+
expect(schema!['input_schema']).toEqual(mod.inputSchema);
|
|
50
|
+
expect(schema!['output_schema']).toEqual(mod.outputSchema);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return null for non-existent module', () => {
|
|
54
|
+
const reg = new Registry();
|
|
55
|
+
expect(getSchema(reg, 'nonexistent')).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Step 2: Implement exportSchema() with JSON serialization
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { exportSchema } from '../../src/registry/schema-export.js';
|
|
64
|
+
|
|
65
|
+
describe('exportSchema', () => {
|
|
66
|
+
it('should export schema as formatted JSON string', () => {
|
|
67
|
+
const reg = new Registry();
|
|
68
|
+
reg.register('test.mod', {
|
|
69
|
+
description: 'Test',
|
|
70
|
+
inputSchema: { type: 'object' },
|
|
71
|
+
outputSchema: { type: 'object' },
|
|
72
|
+
execute: async () => ({}),
|
|
73
|
+
});
|
|
74
|
+
const json = exportSchema(reg, 'test.mod', 'json');
|
|
75
|
+
const parsed = JSON.parse(json);
|
|
76
|
+
expect(parsed['module_id']).toBe('test.mod');
|
|
77
|
+
expect(parsed['description']).toBe('Test');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should throw ModuleNotFoundError for missing module', () => {
|
|
81
|
+
const reg = new Registry();
|
|
82
|
+
expect(() => exportSchema(reg, 'missing')).toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Step 3: YAML format serialization
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
it('should export schema as YAML string', () => {
|
|
91
|
+
const reg = new Registry();
|
|
92
|
+
reg.register('yaml.mod', {
|
|
93
|
+
description: 'YAML test',
|
|
94
|
+
inputSchema: { type: 'object' },
|
|
95
|
+
outputSchema: { type: 'object' },
|
|
96
|
+
execute: async () => ({}),
|
|
97
|
+
});
|
|
98
|
+
const yamlStr = exportSchema(reg, 'yaml.mod', 'yaml');
|
|
99
|
+
expect(yamlStr).toContain('module_id');
|
|
100
|
+
expect(yamlStr).toContain('yaml.mod');
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Step 4: Strict mode with toStrictSchema()
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
it('should apply strict mode transformations', () => {
|
|
108
|
+
const reg = new Registry();
|
|
109
|
+
reg.register('strict.mod', {
|
|
110
|
+
description: 'Strict test',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: { a: { type: 'number' } },
|
|
114
|
+
},
|
|
115
|
+
outputSchema: { type: 'object' },
|
|
116
|
+
execute: async () => ({}),
|
|
117
|
+
});
|
|
118
|
+
const json = exportSchema(reg, 'strict.mod', 'json', true);
|
|
119
|
+
const parsed = JSON.parse(json);
|
|
120
|
+
// Strict mode adds additionalProperties: false, required arrays
|
|
121
|
+
expect(parsed['input_schema']).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Step 5: Compact mode with truncated descriptions
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
it('should apply compact mode transformations', () => {
|
|
129
|
+
const reg = new Registry();
|
|
130
|
+
reg.register('compact.mod', {
|
|
131
|
+
description: 'First sentence. Second sentence with more details.',
|
|
132
|
+
inputSchema: { type: 'object' },
|
|
133
|
+
outputSchema: { type: 'object' },
|
|
134
|
+
execute: async () => ({}),
|
|
135
|
+
});
|
|
136
|
+
const json = exportSchema(reg, 'compact.mod', 'json', false, true);
|
|
137
|
+
const parsed = JSON.parse(json);
|
|
138
|
+
expect(parsed['description']).toBe('First sentence.');
|
|
139
|
+
expect(parsed['documentation']).toBeUndefined();
|
|
140
|
+
expect(parsed['examples']).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Step 6: LLM export profile support
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
it('should export with LLM profile', () => {
|
|
148
|
+
const reg = new Registry();
|
|
149
|
+
reg.register('profile.mod', {
|
|
150
|
+
description: 'Profile test',
|
|
151
|
+
inputSchema: { type: 'object', properties: {} },
|
|
152
|
+
outputSchema: { type: 'object' },
|
|
153
|
+
execute: async () => ({}),
|
|
154
|
+
});
|
|
155
|
+
const json = exportSchema(reg, 'profile.mod', 'json', false, false, 'generic');
|
|
156
|
+
const parsed = JSON.parse(json);
|
|
157
|
+
expect(parsed).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Step 7: Implement getAllSchemas()
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { getAllSchemas } from '../../src/registry/schema-export.js';
|
|
165
|
+
|
|
166
|
+
describe('getAllSchemas', () => {
|
|
167
|
+
it('should return schema records for all registered modules', () => {
|
|
168
|
+
const reg = new Registry();
|
|
169
|
+
reg.register('mod.a', {
|
|
170
|
+
description: 'A',
|
|
171
|
+
inputSchema: { type: 'object' },
|
|
172
|
+
outputSchema: { type: 'object' },
|
|
173
|
+
execute: async () => ({}),
|
|
174
|
+
});
|
|
175
|
+
reg.register('mod.b', {
|
|
176
|
+
description: 'B',
|
|
177
|
+
inputSchema: { type: 'object' },
|
|
178
|
+
outputSchema: { type: 'object' },
|
|
179
|
+
execute: async () => ({}),
|
|
180
|
+
});
|
|
181
|
+
const schemas = getAllSchemas(reg);
|
|
182
|
+
expect(Object.keys(schemas)).toHaveLength(2);
|
|
183
|
+
expect(schemas['mod.a']['module_id']).toBe('mod.a');
|
|
184
|
+
expect(schemas['mod.b']['module_id']).toBe('mod.b');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should return empty object for empty registry', () => {
|
|
188
|
+
const reg = new Registry();
|
|
189
|
+
expect(getAllSchemas(reg)).toEqual({});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Step 8: Implement exportAllSchemas()
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { exportAllSchemas } from '../../src/registry/schema-export.js';
|
|
198
|
+
|
|
199
|
+
describe('exportAllSchemas', () => {
|
|
200
|
+
it('should export all schemas as JSON string', () => {
|
|
201
|
+
const reg = new Registry();
|
|
202
|
+
reg.register('all.a', {
|
|
203
|
+
description: 'A',
|
|
204
|
+
inputSchema: { type: 'object' },
|
|
205
|
+
outputSchema: { type: 'object' },
|
|
206
|
+
execute: async () => ({}),
|
|
207
|
+
});
|
|
208
|
+
const json = exportAllSchemas(reg);
|
|
209
|
+
const parsed = JSON.parse(json);
|
|
210
|
+
expect(parsed['all.a']).toBeDefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should apply strict mode to all schemas', () => {
|
|
214
|
+
const reg = new Registry();
|
|
215
|
+
reg.register('strict.all', {
|
|
216
|
+
description: 'Strict all',
|
|
217
|
+
inputSchema: { type: 'object', properties: { x: { type: 'string' } } },
|
|
218
|
+
outputSchema: { type: 'object' },
|
|
219
|
+
execute: async () => ({}),
|
|
220
|
+
});
|
|
221
|
+
const json = exportAllSchemas(reg, 'json', true);
|
|
222
|
+
const parsed = JSON.parse(json);
|
|
223
|
+
expect(parsed['strict.all']).toBeDefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should export all schemas as YAML', () => {
|
|
227
|
+
const reg = new Registry();
|
|
228
|
+
reg.register('yaml.all', {
|
|
229
|
+
description: 'YAML',
|
|
230
|
+
inputSchema: { type: 'object' },
|
|
231
|
+
outputSchema: { type: 'object' },
|
|
232
|
+
execute: async () => ({}),
|
|
233
|
+
});
|
|
234
|
+
const yamlStr = exportAllSchemas(reg, 'yaml');
|
|
235
|
+
expect(yamlStr).toContain('yaml.all');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Acceptance Criteria
|
|
241
|
+
|
|
242
|
+
- [x] `getSchema()` extracts module_id, name, description, version, tags, input_schema, output_schema, annotations, examples
|
|
243
|
+
- [x] `getSchema()` returns null for non-existent modules
|
|
244
|
+
- [x] `exportSchema()` serializes to JSON (default) and YAML formats
|
|
245
|
+
- [x] `exportSchema()` throws `ModuleNotFoundError` for missing modules
|
|
246
|
+
- [x] Strict mode applies `toStrictSchema()` to input and output schemas
|
|
247
|
+
- [x] Compact mode truncates description at first sentence boundary and strips extensions/documentation/examples
|
|
248
|
+
- [x] LLM export profiles (MCP, OpenAI, Anthropic, Generic) are delegated to `SchemaExporter`
|
|
249
|
+
- [x] `getAllSchemas()` returns schema records keyed by module ID for all registered modules
|
|
250
|
+
- [x] `exportAllSchemas()` serializes all schemas with optional strict/compact/profile modes
|
|
251
|
+
- [x] Strict and compact modes are mutually exclusive (strict takes precedence)
|
|
252
|
+
- [x] `deepCopy()` prevents mutation of source module data during transformation
|
|
253
|
+
- [x] All tests pass with `vitest`
|
|
254
|
+
|
|
255
|
+
## Dependencies
|
|
256
|
+
|
|
257
|
+
- `registry-core` task (Registry class)
|
|
258
|
+
|
|
259
|
+
## Estimated Time
|
|
260
|
+
|
|
261
|
+
3 hours
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Task: Types
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Define the core TypeScript interfaces for the registry system: `ModuleDescriptor` (complete module definition record), `DiscoveredModule` (scanner output representing a found file), and `DependencyInfo` (parsed dependency entry). These interfaces are consumed by every other registry component and provide the type-safe foundation for the entire module lifecycle.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/registry/types.ts` -- Interface definitions
|
|
10
|
+
- `src/module.ts` -- Imports `ModuleAnnotations` and `ModuleExample` types
|
|
11
|
+
- `tests/registry/test-types.test.ts` -- Type-level and runtime tests
|
|
12
|
+
|
|
13
|
+
## Steps (TDD)
|
|
14
|
+
|
|
15
|
+
### Step 1: Define DiscoveredModule interface
|
|
16
|
+
|
|
17
|
+
Write a test that creates a `DiscoveredModule` object and asserts all fields are accessible:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { describe, it, expect } from 'vitest';
|
|
21
|
+
import type { DiscoveredModule } from '../../src/registry/types.js';
|
|
22
|
+
|
|
23
|
+
describe('DiscoveredModule', () => {
|
|
24
|
+
it('should represent a discovered file with canonical ID', () => {
|
|
25
|
+
const dm: DiscoveredModule = {
|
|
26
|
+
filePath: '/extensions/math/add.ts',
|
|
27
|
+
canonicalId: 'math.add',
|
|
28
|
+
metaPath: '/extensions/math/add_meta.yaml',
|
|
29
|
+
namespace: null,
|
|
30
|
+
};
|
|
31
|
+
expect(dm.filePath).toBe('/extensions/math/add.ts');
|
|
32
|
+
expect(dm.canonicalId).toBe('math.add');
|
|
33
|
+
expect(dm.metaPath).toBe('/extensions/math/add_meta.yaml');
|
|
34
|
+
expect(dm.namespace).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should support namespace for multi-root scanning', () => {
|
|
38
|
+
const dm: DiscoveredModule = {
|
|
39
|
+
filePath: '/plugins/greet.ts',
|
|
40
|
+
canonicalId: 'plugins.greet',
|
|
41
|
+
metaPath: null,
|
|
42
|
+
namespace: 'plugins',
|
|
43
|
+
};
|
|
44
|
+
expect(dm.namespace).toBe('plugins');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Implement the interface with `filePath: string`, `canonicalId: string`, `metaPath: string | null`, `namespace: string | null`.
|
|
50
|
+
|
|
51
|
+
### Step 2: Define DependencyInfo interface
|
|
52
|
+
|
|
53
|
+
Write a test for `DependencyInfo`:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
describe('DependencyInfo', () => {
|
|
57
|
+
it('should represent a module dependency', () => {
|
|
58
|
+
const dep: DependencyInfo = {
|
|
59
|
+
moduleId: 'core.auth',
|
|
60
|
+
version: '2.0.0',
|
|
61
|
+
optional: false,
|
|
62
|
+
};
|
|
63
|
+
expect(dep.moduleId).toBe('core.auth');
|
|
64
|
+
expect(dep.version).toBe('2.0.0');
|
|
65
|
+
expect(dep.optional).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should allow null version for unversioned dependencies', () => {
|
|
69
|
+
const dep: DependencyInfo = {
|
|
70
|
+
moduleId: 'utils.logger',
|
|
71
|
+
version: null,
|
|
72
|
+
optional: true,
|
|
73
|
+
};
|
|
74
|
+
expect(dep.version).toBeNull();
|
|
75
|
+
expect(dep.optional).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Implement with `moduleId: string`, `version: string | null`, `optional: boolean`.
|
|
81
|
+
|
|
82
|
+
### Step 3: Define ModuleDescriptor interface
|
|
83
|
+
|
|
84
|
+
Write a test for `ModuleDescriptor`:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
describe('ModuleDescriptor', () => {
|
|
88
|
+
it('should represent a complete module definition', () => {
|
|
89
|
+
const desc: ModuleDescriptor = {
|
|
90
|
+
moduleId: 'math.add',
|
|
91
|
+
name: 'Add Numbers',
|
|
92
|
+
description: 'Adds two numbers together',
|
|
93
|
+
documentation: 'Detailed usage guide...',
|
|
94
|
+
inputSchema: { type: 'object', properties: { a: { type: 'number' } } },
|
|
95
|
+
outputSchema: { type: 'object', properties: { result: { type: 'number' } } },
|
|
96
|
+
version: '1.0.0',
|
|
97
|
+
tags: ['math', 'arithmetic'],
|
|
98
|
+
annotations: null,
|
|
99
|
+
examples: [],
|
|
100
|
+
metadata: {},
|
|
101
|
+
};
|
|
102
|
+
expect(desc.moduleId).toBe('math.add');
|
|
103
|
+
expect(desc.tags).toContain('math');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Implement with all fields including `annotations: ModuleAnnotations | null` and `examples: ModuleExample[]` imported from `../module.js`.
|
|
109
|
+
|
|
110
|
+
## Acceptance Criteria
|
|
111
|
+
|
|
112
|
+
- [x] `ModuleDescriptor` interface has all 11 fields: moduleId, name, description, documentation, inputSchema, outputSchema, version, tags, annotations, examples, metadata
|
|
113
|
+
- [x] `DiscoveredModule` interface has 4 fields: filePath, canonicalId, metaPath, namespace
|
|
114
|
+
- [x] `DependencyInfo` interface has 3 fields: moduleId, version, optional
|
|
115
|
+
- [x] All interfaces compile under `tsc --noEmit` with strict mode
|
|
116
|
+
- [x] Types are re-exported from `src/registry/index.ts`
|
|
117
|
+
|
|
118
|
+
## Dependencies
|
|
119
|
+
|
|
120
|
+
- `src/module.ts` must define `ModuleAnnotations` and `ModuleExample` types
|
|
121
|
+
|
|
122
|
+
## Estimated Time
|
|
123
|
+
|
|
124
|
+
1 hour
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Task: Validation
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement `validateModule()` which performs duck-type structural validation on a module object. Checks that the module has `inputSchema` (non-null object), `outputSchema` (non-null object), `description` (non-empty string), and `execute` (function). Returns an array of error strings -- empty array means valid. Unlike Python's `issubclass` checks, TypeScript uses duck typing to validate structural conformance.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/registry/validation.ts` -- Validation implementation
|
|
10
|
+
- `tests/registry/test-validation.test.ts` -- Validation tests
|
|
11
|
+
|
|
12
|
+
## Steps (TDD)
|
|
13
|
+
|
|
14
|
+
### Step 1: Valid module returns empty error array
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { validateModule } from '../../src/registry/validation.js';
|
|
19
|
+
|
|
20
|
+
describe('validateModule', () => {
|
|
21
|
+
it('should return empty array for a valid module', () => {
|
|
22
|
+
const validModule = {
|
|
23
|
+
inputSchema: { type: 'object', properties: {} },
|
|
24
|
+
outputSchema: { type: 'object', properties: {} },
|
|
25
|
+
description: 'A valid module',
|
|
26
|
+
execute: async () => ({}),
|
|
27
|
+
};
|
|
28
|
+
const errors = validateModule(validModule);
|
|
29
|
+
expect(errors).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Step 2: Missing inputSchema
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
it('should report missing inputSchema', () => {
|
|
38
|
+
const mod = {
|
|
39
|
+
outputSchema: { type: 'object' },
|
|
40
|
+
description: 'Test',
|
|
41
|
+
execute: async () => ({}),
|
|
42
|
+
};
|
|
43
|
+
const errors = validateModule(mod);
|
|
44
|
+
expect(errors).toHaveLength(1);
|
|
45
|
+
expect(errors[0]).toContain('inputSchema');
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Step 3: Missing outputSchema
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
it('should report missing outputSchema', () => {
|
|
53
|
+
const mod = {
|
|
54
|
+
inputSchema: { type: 'object' },
|
|
55
|
+
description: 'Test',
|
|
56
|
+
execute: async () => ({}),
|
|
57
|
+
};
|
|
58
|
+
const errors = validateModule(mod);
|
|
59
|
+
expect(errors).toHaveLength(1);
|
|
60
|
+
expect(errors[0]).toContain('outputSchema');
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Step 4: Missing or empty description
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
it('should report missing description', () => {
|
|
68
|
+
const mod = {
|
|
69
|
+
inputSchema: { type: 'object' },
|
|
70
|
+
outputSchema: { type: 'object' },
|
|
71
|
+
execute: async () => ({}),
|
|
72
|
+
};
|
|
73
|
+
const errors = validateModule(mod);
|
|
74
|
+
expect(errors.some((e) => e.includes('description'))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should report empty string description', () => {
|
|
78
|
+
const mod = {
|
|
79
|
+
inputSchema: { type: 'object' },
|
|
80
|
+
outputSchema: { type: 'object' },
|
|
81
|
+
description: '',
|
|
82
|
+
execute: async () => ({}),
|
|
83
|
+
};
|
|
84
|
+
const errors = validateModule(mod);
|
|
85
|
+
expect(errors.some((e) => e.includes('description'))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Step 5: Missing execute method
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
it('should report missing execute method', () => {
|
|
93
|
+
const mod = {
|
|
94
|
+
inputSchema: { type: 'object' },
|
|
95
|
+
outputSchema: { type: 'object' },
|
|
96
|
+
description: 'Test module',
|
|
97
|
+
};
|
|
98
|
+
const errors = validateModule(mod);
|
|
99
|
+
expect(errors.some((e) => e.includes('execute'))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Step 6: Multiple validation errors
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
it('should accumulate all validation errors', () => {
|
|
107
|
+
const errors = validateModule({});
|
|
108
|
+
expect(errors.length).toBeGreaterThanOrEqual(3);
|
|
109
|
+
expect(errors.some((e) => e.includes('inputSchema'))).toBe(true);
|
|
110
|
+
expect(errors.some((e) => e.includes('outputSchema'))).toBe(true);
|
|
111
|
+
expect(errors.some((e) => e.includes('description'))).toBe(true);
|
|
112
|
+
expect(errors.some((e) => e.includes('execute'))).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Step 7: Non-object inputSchema/outputSchema
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
it('should reject non-object inputSchema', () => {
|
|
120
|
+
const mod = {
|
|
121
|
+
inputSchema: 'not an object',
|
|
122
|
+
outputSchema: { type: 'object' },
|
|
123
|
+
description: 'Test',
|
|
124
|
+
execute: async () => ({}),
|
|
125
|
+
};
|
|
126
|
+
const errors = validateModule(mod);
|
|
127
|
+
expect(errors.some((e) => e.includes('inputSchema'))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should reject null inputSchema', () => {
|
|
131
|
+
const mod = {
|
|
132
|
+
inputSchema: null,
|
|
133
|
+
outputSchema: { type: 'object' },
|
|
134
|
+
description: 'Test',
|
|
135
|
+
execute: async () => ({}),
|
|
136
|
+
};
|
|
137
|
+
const errors = validateModule(mod);
|
|
138
|
+
expect(errors.some((e) => e.includes('inputSchema'))).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Step 8: Constructor fallback for class-based modules
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
it('should check constructor for schema when instance lacks it', () => {
|
|
146
|
+
class MyModule {
|
|
147
|
+
static inputSchema = { type: 'object' };
|
|
148
|
+
static outputSchema = { type: 'object' };
|
|
149
|
+
description = 'Class-based module';
|
|
150
|
+
execute = async () => ({});
|
|
151
|
+
}
|
|
152
|
+
const instance = new MyModule();
|
|
153
|
+
const errors = validateModule(instance);
|
|
154
|
+
expect(errors).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Acceptance Criteria
|
|
159
|
+
|
|
160
|
+
- [x] Valid module with inputSchema (object), outputSchema (object), description (string), execute (function) returns `[]`
|
|
161
|
+
- [x] Missing `inputSchema` produces descriptive error string
|
|
162
|
+
- [x] Missing `outputSchema` produces descriptive error string
|
|
163
|
+
- [x] Missing or empty `description` produces descriptive error string
|
|
164
|
+
- [x] Missing `execute` method produces descriptive error string
|
|
165
|
+
- [x] Non-object `inputSchema`/`outputSchema` (string, null, number) produces error
|
|
166
|
+
- [x] All errors are accumulated and returned together
|
|
167
|
+
- [x] Constructor properties are checked as fallback for class instances
|
|
168
|
+
- [x] Function accepts `unknown` type parameter for flexibility
|
|
169
|
+
- [x] All tests pass with `vitest`
|
|
170
|
+
|
|
171
|
+
## Dependencies
|
|
172
|
+
|
|
173
|
+
- `types` task (interfaces for context)
|
|
174
|
+
|
|
175
|
+
## Estimated Time
|
|
176
|
+
|
|
177
|
+
1 hour
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Feature: Schema System
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Schema System is the foundational type infrastructure for apcore. It handles schema loading from YAML files, `$ref` resolution across documents, conversion to TypeBox `TSchema` objects, runtime validation via TypeBox `Value.Check()`/`Value.Decode()`, and export to LLM provider formats (MCP, OpenAI, Anthropic, Generic). The system supports three schema resolution strategies (`yaml_first`, `native_first`, `yaml_only`), annotation merging between YAML and code-defined metadata, and strict-mode transformations for OpenAI function calling compatibility.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
### Included
|
|
10
|
+
|
|
11
|
+
- `SchemaDefinition`, `ResolvedSchema`, `SchemaValidationResult`, and `LLMExtensions` interfaces
|
|
12
|
+
- `SchemaStrategy` and `ExportProfile` enums for runtime configuration
|
|
13
|
+
- `SchemaLoader` class with YAML file loading, `jsonSchemaToTypeBox()` conversion, two-level caching (definition + resolved), and strategy-based schema selection
|
|
14
|
+
- `RefResolver` class with `$ref` resolution supporting local pointers, relative file paths, `apcore://` canonical URIs, circular reference detection, and configurable max depth
|
|
15
|
+
- `jsonSchemaToTypeBox()` recursive converter from JSON Schema dictionaries to TypeBox `TSchema` objects
|
|
16
|
+
- `SchemaValidator` class with coercion toggle, `validate()`, `validateInput()`, and `validateOutput()` methods backed by TypeBox `Value.Check()`/`Value.Decode()`/`Value.Errors()`
|
|
17
|
+
- `SchemaExporter` class with four export profiles: MCP (with annotations), OpenAI (strict mode), Anthropic (with examples), Generic (passthrough)
|
|
18
|
+
- `toStrictSchema()`, `stripExtensions()`, `applyLlmDescriptions()` strict-mode utilities
|
|
19
|
+
- `mergeAnnotations()`, `mergeExamples()`, `mergeMetadata()` for YAML/code conflict resolution
|
|
20
|
+
|
|
21
|
+
### Excluded
|
|
22
|
+
|
|
23
|
+
- Executor pipeline integration (consumed by `core-executor`)
|
|
24
|
+
- Registry module discovery (consumed by `registry-system`)
|
|
25
|
+
- Decorator-based schema generation (consumed by `decorator-bindings`)
|
|
26
|
+
- YAML schema file authoring and schema directory management
|
|
27
|
+
|
|
28
|
+
## Technology Stack
|
|
29
|
+
|
|
30
|
+
- **TypeScript 5.5+** with strict mode
|
|
31
|
+
- **@sinclair/typebox >= 0.34.0** for schema representation and runtime validation
|
|
32
|
+
- **js-yaml** for YAML file parsing
|
|
33
|
+
- **Node.js >= 18.0.0** with ES Module support (`node:fs`, `node:path`)
|
|
34
|
+
- **vitest** for unit testing
|
|
35
|
+
|
|
36
|
+
## Task Execution Order
|
|
37
|
+
|
|
38
|
+
| # | Task File | Description | Status |
|
|
39
|
+
|---|-----------|-------------|--------|
|
|
40
|
+
| 1 | [types-and-annotations](./tasks/types-and-annotations.md) | Core interfaces, enums, validation result types, annotation merging | completed |
|
|
41
|
+
| 2 | [loader](./tasks/loader.md) | SchemaLoader with YAML loading, caching, strategy selection | completed |
|
|
42
|
+
| 3 | [ref-resolver](./tasks/ref-resolver.md) | RefResolver with $ref resolution, apcore:// URIs, circular detection | completed |
|
|
43
|
+
| 4 | [typebox-generation](./tasks/typebox-generation.md) | jsonSchemaToTypeBox() recursive JSON Schema to TypeBox conversion | completed |
|
|
44
|
+
| 5 | [validator](./tasks/validator.md) | SchemaValidator with TypeBox validation, coercion toggle, error mapping | completed |
|
|
45
|
+
| 6 | [exporter](./tasks/exporter.md) | SchemaExporter with MCP, OpenAI, Anthropic, Generic export profiles | completed |
|
|
46
|
+
| 7 | [strict-mode](./tasks/strict-mode.md) | toStrictSchema(), stripExtensions(), applyLlmDescriptions() utilities | completed |
|
|
47
|
+
|
|
48
|
+
## Progress
|
|
49
|
+
|
|
50
|
+
| Total | Completed | In Progress | Pending |
|
|
51
|
+
|-------|-----------|-------------|---------|
|
|
52
|
+
| 7 | 7 | 0 | 0 |
|
|
53
|
+
|
|
54
|
+
## Reference Documents
|
|
55
|
+
|
|
56
|
+
- [Schema System Feature Specification](../../features/schema-system.md)
|