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,140 @@
|
|
|
1
|
+
# Task: Strict Mode
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the strict-mode transformation utilities for converting JSON Schemas to OpenAI-compatible strict mode format (Algorithm A23). This includes `toStrictSchema()` for full strict conversion, `stripExtensions()` for removing vendor extensions and defaults, and `applyLlmDescriptions()` for substituting `x-llm-description` into `description` fields.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/schema/strict.ts` -- `toStrictSchema()`, `stripExtensions()`, `applyLlmDescriptions()`, internal `convertToStrict()`
|
|
10
|
+
- `tests/schema/test-strict.test.ts` -- Unit tests
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Implement deepCopy utility
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
function deepCopy<T>(obj: T): T {
|
|
18
|
+
return JSON.parse(JSON.stringify(obj));
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Used internally to ensure all transformations operate on copies, never mutating the original schema.
|
|
23
|
+
|
|
24
|
+
### 2. Implement applyLlmDescriptions()
|
|
25
|
+
|
|
26
|
+
Recursively walk the schema tree. At each object node, if both `x-llm-description` and `description` exist, replace `description` with the value of `x-llm-description`. Recurse into `properties`, `items`, `oneOf`/`anyOf`/`allOf`, and `definitions`/`$defs`.
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
export function applyLlmDescriptions(node: unknown): void {
|
|
30
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node)) return;
|
|
31
|
+
|
|
32
|
+
const obj = node as Record<string, unknown>;
|
|
33
|
+
if ('x-llm-description' in obj && 'description' in obj) {
|
|
34
|
+
obj['description'] = obj['x-llm-description'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ('properties' in obj && typeof obj['properties'] === 'object' && obj['properties'] !== null) {
|
|
38
|
+
for (const prop of Object.values(obj['properties'] as Record<string, unknown>)) {
|
|
39
|
+
applyLlmDescriptions(prop);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ... recurse into items, oneOf, anyOf, allOf, definitions, $defs
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
TDD: Test root-level description replacement. Test nested property description replacement. Test no-op when `x-llm-description` is absent.
|
|
47
|
+
|
|
48
|
+
### 3. Implement stripExtensions()
|
|
49
|
+
|
|
50
|
+
Recursively remove all keys starting with `x-` and the `default` key from every object node. Recurse into all nested objects and arrays.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export function stripExtensions(node: unknown): void {
|
|
54
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node)) return;
|
|
55
|
+
|
|
56
|
+
const obj = node as Record<string, unknown>;
|
|
57
|
+
const keysToRemove = Object.keys(obj).filter(
|
|
58
|
+
(k) => (typeof k === 'string' && k.startsWith('x-')) || k === 'default',
|
|
59
|
+
);
|
|
60
|
+
for (const k of keysToRemove) {
|
|
61
|
+
delete obj[k];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const value of Object.values(obj)) {
|
|
65
|
+
if (typeof value === 'object' && value !== null) {
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
for (const item of value) {
|
|
68
|
+
if (typeof item === 'object' && item !== null) {
|
|
69
|
+
stripExtensions(item);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
stripExtensions(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
TDD: Test `x-sensitive`, `x-custom`, `x-llm-description` are removed. Test `default` is removed. Test nested structures are processed. Test non-extension keys survive.
|
|
81
|
+
|
|
82
|
+
### 4. Implement convertToStrict() (internal)
|
|
83
|
+
|
|
84
|
+
For each object node with `type: "object"` and `properties`:
|
|
85
|
+
1. Set `additionalProperties: false`
|
|
86
|
+
2. Collect existing `required` array into a Set
|
|
87
|
+
3. Find optional properties (not in `required`)
|
|
88
|
+
4. For optional properties with a `type` field: wrap type in array with `"null"` (e.g., `"string"` -> `["string", "null"]`)
|
|
89
|
+
5. For optional properties without a `type` field: wrap in `{ oneOf: [originalSchema, { type: "null" }] }`
|
|
90
|
+
6. Set `required` to sorted array of ALL property names
|
|
91
|
+
|
|
92
|
+
Recurse into `properties`, `items`, `oneOf`/`anyOf`/`allOf`, and `definitions`/`$defs`.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
function convertToStrict(node: unknown): void {
|
|
96
|
+
// ... see implementation in src/schema/strict.ts
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
TDD: Test `additionalProperties: false` is added. Test all properties become required. Test optional properties get null-union wrapping. Test already-required properties keep their original type.
|
|
101
|
+
|
|
102
|
+
### 5. Implement toStrictSchema() composition
|
|
103
|
+
|
|
104
|
+
Compose the three operations: deep-copy, strip extensions, then convert to strict.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
export function toStrictSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
|
108
|
+
const result = deepCopy(schema);
|
|
109
|
+
stripExtensions(result);
|
|
110
|
+
convertToStrict(result);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
TDD: Test that the original schema is not mutated. Test full pipeline: extensions removed, `additionalProperties: false` set, all required, optionals nullable.
|
|
116
|
+
|
|
117
|
+
## Acceptance Criteria
|
|
118
|
+
|
|
119
|
+
- [x] `applyLlmDescriptions()` replaces `description` with `x-llm-description` where both exist
|
|
120
|
+
- [x] `applyLlmDescriptions()` recurses into `properties`, `items`, `oneOf`/`anyOf`/`allOf`, `definitions`/`$defs`
|
|
121
|
+
- [x] `applyLlmDescriptions()` is a no-op when `x-llm-description` is absent
|
|
122
|
+
- [x] `stripExtensions()` removes all `x-` prefixed keys recursively
|
|
123
|
+
- [x] `stripExtensions()` removes `default` keys recursively
|
|
124
|
+
- [x] `stripExtensions()` preserves non-extension keys
|
|
125
|
+
- [x] `stripExtensions()` handles nested objects and arrays
|
|
126
|
+
- [x] `toStrictSchema()` sets `additionalProperties: false` on all object schemas
|
|
127
|
+
- [x] `toStrictSchema()` makes all properties required (sorted alphabetically)
|
|
128
|
+
- [x] `toStrictSchema()` wraps optional type-bearing properties with null type array
|
|
129
|
+
- [x] `toStrictSchema()` wraps optional non-type properties with `oneOf` null union
|
|
130
|
+
- [x] `toStrictSchema()` does not mutate the original schema (returns deep copy)
|
|
131
|
+
- [x] `toStrictSchema()` strips extensions before strict conversion
|
|
132
|
+
- [x] All tests pass with `vitest`
|
|
133
|
+
|
|
134
|
+
## Dependencies
|
|
135
|
+
|
|
136
|
+
- None (standalone utility, no internal dependencies)
|
|
137
|
+
|
|
138
|
+
## Estimated Time
|
|
139
|
+
|
|
140
|
+
2 hours
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Task: TypeBox Generation
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `jsonSchemaToTypeBox()` function that recursively converts a JSON Schema dictionary to a TypeBox `TSchema` object. This is the TypeScript equivalent of the Python `create_model()` approach, leveraging the fact that TypeBox schemas ARE valid JSON Schema.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/schema/loader.ts` -- `jsonSchemaToTypeBox()` function (exported, co-located with SchemaLoader)
|
|
10
|
+
- `tests/schema/test-loader.test.ts` -- Unit tests for `jsonSchemaToTypeBox()`
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Handle object type with properties
|
|
15
|
+
|
|
16
|
+
Convert `{ type: "object", properties: {...}, required: [...] }` to `Type.Object({...})`. Recursively convert each property. Wrap non-required properties with `Type.Optional()`.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
if (schemaType === 'object') {
|
|
20
|
+
const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined;
|
|
21
|
+
const required = new Set((schema['required'] as string[]) ?? []);
|
|
22
|
+
|
|
23
|
+
if (properties) {
|
|
24
|
+
const typeboxProps: Record<string, TSchema> = {};
|
|
25
|
+
for (const [name, propSchema] of Object.entries(properties)) {
|
|
26
|
+
const propType = jsonSchemaToTypeBox(propSchema);
|
|
27
|
+
typeboxProps[name] = required.has(name) ? propType : Type.Optional(propType);
|
|
28
|
+
}
|
|
29
|
+
return Type.Object(typeboxProps);
|
|
30
|
+
}
|
|
31
|
+
return Type.Record(Type.String(), Type.Unknown());
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
TDD: Test object with required and optional properties. Verify `Value.Check()` accepts valid data and rejects missing required fields.
|
|
36
|
+
|
|
37
|
+
### 2. Handle object type without properties
|
|
38
|
+
|
|
39
|
+
Convert `{ type: "object" }` (no properties) to `Type.Record(Type.String(), Type.Unknown())`, accepting any string-keyed object.
|
|
40
|
+
|
|
41
|
+
TDD: Test that `{ any: "value" }` passes validation.
|
|
42
|
+
|
|
43
|
+
### 3. Handle array type
|
|
44
|
+
|
|
45
|
+
Convert `{ type: "array", items: {...} }` to `Type.Array(jsonSchemaToTypeBox(items))`. Without items, use `Type.Array(Type.Unknown())`.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
if (schemaType === 'array') {
|
|
49
|
+
const items = schema['items'] as Record<string, unknown> | undefined;
|
|
50
|
+
return items ? Type.Array(jsonSchemaToTypeBox(items)) : Type.Array(Type.Unknown());
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
TDD: Test typed array (string items), untyped array, valid and invalid data.
|
|
55
|
+
|
|
56
|
+
### 4. Handle string type with constraints
|
|
57
|
+
|
|
58
|
+
Convert `{ type: "string" }` to `Type.String()`. Pass through constraint options: `minLength`, `maxLength`, `pattern`, `format`.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
if (schemaType === 'string') {
|
|
62
|
+
const opts: Record<string, unknown> = {};
|
|
63
|
+
if ('minLength' in schema) opts['minLength'] = schema['minLength'];
|
|
64
|
+
if ('maxLength' in schema) opts['maxLength'] = schema['maxLength'];
|
|
65
|
+
if ('pattern' in schema) opts['pattern'] = schema['pattern'];
|
|
66
|
+
if ('format' in schema) opts['format'] = schema['format'];
|
|
67
|
+
return Type.String(opts);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
TDD: Test plain string, string with `minLength`/`maxLength`, verify constraint enforcement.
|
|
72
|
+
|
|
73
|
+
### 5. Handle integer and number types with constraints
|
|
74
|
+
|
|
75
|
+
Convert `{ type: "integer" }` to `Type.Integer()` and `{ type: "number" }` to `Type.Number()`. Pass through: `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`.
|
|
76
|
+
|
|
77
|
+
TDD: Test integer rejects floats, number accepts both, constraint boundaries work.
|
|
78
|
+
|
|
79
|
+
### 6. Handle boolean and null types
|
|
80
|
+
|
|
81
|
+
Convert `{ type: "boolean" }` to `Type.Boolean()` and `{ type: "null" }` to `Type.Null()`.
|
|
82
|
+
|
|
83
|
+
TDD: Test boolean accepts `true`/`false`, rejects strings. Null accepts `null`, rejects `undefined`.
|
|
84
|
+
|
|
85
|
+
### 7. Handle enum
|
|
86
|
+
|
|
87
|
+
Convert `{ enum: ["a", "b", "c"] }` to `Type.Union(values.map(v => Type.Literal(v)))`.
|
|
88
|
+
|
|
89
|
+
TDD: Test valid enum values accepted, invalid values rejected.
|
|
90
|
+
|
|
91
|
+
### 8. Handle oneOf and anyOf
|
|
92
|
+
|
|
93
|
+
Convert `{ oneOf: [...] }` and `{ anyOf: [...] }` to `Type.Union(schemas.map(s => jsonSchemaToTypeBox(s)))`.
|
|
94
|
+
|
|
95
|
+
TDD: Test union of string and number accepts both types, rejects others.
|
|
96
|
+
|
|
97
|
+
### 9. Handle allOf
|
|
98
|
+
|
|
99
|
+
Convert `{ allOf: [...] }` to `Type.Intersect(schemas.map(s => jsonSchemaToTypeBox(s)))`.
|
|
100
|
+
|
|
101
|
+
TDD: Test intersection of two object schemas requires properties from both.
|
|
102
|
+
|
|
103
|
+
### 10. Handle unknown/unsupported schemas
|
|
104
|
+
|
|
105
|
+
Any schema that does not match a known type falls through to `Type.Unknown()`, which accepts any value.
|
|
106
|
+
|
|
107
|
+
TDD: Test empty schema `{}` accepts any value.
|
|
108
|
+
|
|
109
|
+
## Acceptance Criteria
|
|
110
|
+
|
|
111
|
+
- [x] `object` with properties converts to `Type.Object()` with correct required/optional handling
|
|
112
|
+
- [x] `object` without properties converts to `Type.Record(Type.String(), Type.Unknown())`
|
|
113
|
+
- [x] `array` with items converts to `Type.Array()` with recursive item conversion
|
|
114
|
+
- [x] `array` without items converts to `Type.Array(Type.Unknown())`
|
|
115
|
+
- [x] `string` converts to `Type.String()` with `minLength`/`maxLength`/`pattern`/`format` passthrough
|
|
116
|
+
- [x] `integer` converts to `Type.Integer()` with numeric constraint passthrough
|
|
117
|
+
- [x] `number` converts to `Type.Number()` with numeric constraint passthrough
|
|
118
|
+
- [x] `boolean` converts to `Type.Boolean()`
|
|
119
|
+
- [x] `null` converts to `Type.Null()`
|
|
120
|
+
- [x] `enum` converts to `Type.Union(Type.Literal(...))` for each value
|
|
121
|
+
- [x] `oneOf`/`anyOf` convert to `Type.Union()`
|
|
122
|
+
- [x] `allOf` converts to `Type.Intersect()`
|
|
123
|
+
- [x] Unknown schemas convert to `Type.Unknown()`
|
|
124
|
+
- [x] All generated TypeBox schemas pass `Value.Check()` validation correctly
|
|
125
|
+
- [x] All tests pass with `vitest`
|
|
126
|
+
|
|
127
|
+
## Dependencies
|
|
128
|
+
|
|
129
|
+
- types-and-annotations (for `TSchema` type from TypeBox)
|
|
130
|
+
|
|
131
|
+
## Estimated Time
|
|
132
|
+
|
|
133
|
+
3 hours
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Task: Types and Annotations
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Define the core type interfaces, enums, and annotation merging utilities that form the foundation of the schema system. All other schema tasks depend on these types.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/schema/types.ts` -- `SchemaDefinition`, `ResolvedSchema`, `SchemaStrategy`, `ExportProfile`, `SchemaValidationErrorDetail`, `SchemaValidationResult`, `LLMExtensions`, `validationResultToError()`
|
|
10
|
+
- `src/schema/annotations.ts` -- `mergeAnnotations()`, `mergeExamples()`, `mergeMetadata()`
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Define SchemaStrategy enum
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
export enum SchemaStrategy {
|
|
18
|
+
YamlFirst = 'yaml_first',
|
|
19
|
+
NativeFirst = 'native_first',
|
|
20
|
+
YamlOnly = 'yaml_only',
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
TDD: Write tests asserting enum values match the expected string representations.
|
|
25
|
+
|
|
26
|
+
### 2. Define ExportProfile enum
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
export enum ExportProfile {
|
|
30
|
+
MCP = 'mcp',
|
|
31
|
+
OpenAI = 'openai',
|
|
32
|
+
Anthropic = 'anthropic',
|
|
33
|
+
Generic = 'generic',
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
TDD: Write tests asserting all four profiles are available and have correct string values.
|
|
38
|
+
|
|
39
|
+
### 3. Define SchemaDefinition interface
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export interface SchemaDefinition {
|
|
43
|
+
moduleId: string;
|
|
44
|
+
description: string;
|
|
45
|
+
inputSchema: Record<string, unknown>;
|
|
46
|
+
outputSchema: Record<string, unknown>;
|
|
47
|
+
errorSchema?: Record<string, unknown> | null;
|
|
48
|
+
definitions: Record<string, unknown>;
|
|
49
|
+
version: string;
|
|
50
|
+
documentation?: string | null;
|
|
51
|
+
schemaUrl?: string | null;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
TDD: Create a conforming object and verify all fields are accessible with correct types.
|
|
56
|
+
|
|
57
|
+
### 4. Define ResolvedSchema interface
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import type { TSchema } from '@sinclair/typebox';
|
|
61
|
+
|
|
62
|
+
export interface ResolvedSchema {
|
|
63
|
+
jsonSchema: Record<string, unknown>;
|
|
64
|
+
schema: TSchema;
|
|
65
|
+
moduleId: string;
|
|
66
|
+
direction: string;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
TDD: Create a `ResolvedSchema` with `Type.Object({})` and verify `schema` field is a valid `TSchema`.
|
|
71
|
+
|
|
72
|
+
### 5. Define validation result types
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
export interface SchemaValidationErrorDetail {
|
|
76
|
+
path: string;
|
|
77
|
+
message: string;
|
|
78
|
+
constraint?: string | null;
|
|
79
|
+
expected?: unknown;
|
|
80
|
+
actual?: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SchemaValidationResult {
|
|
84
|
+
valid: boolean;
|
|
85
|
+
errors: SchemaValidationErrorDetail[];
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
TDD: Create valid and invalid results, verify field access.
|
|
90
|
+
|
|
91
|
+
### 6. Implement validationResultToError()
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
export function validationResultToError(result: SchemaValidationResult): SchemaValidationError {
|
|
95
|
+
if (result.valid) {
|
|
96
|
+
throw new Error('Cannot convert valid result to error');
|
|
97
|
+
}
|
|
98
|
+
const errorDicts = result.errors.map((e) => ({
|
|
99
|
+
path: e.path,
|
|
100
|
+
message: e.message,
|
|
101
|
+
constraint: e.constraint ?? null,
|
|
102
|
+
expected: e.expected ?? null,
|
|
103
|
+
actual: e.actual ?? null,
|
|
104
|
+
}));
|
|
105
|
+
return new SchemaValidationError('Schema validation failed', errorDicts);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
TDD: Verify conversion from `SchemaValidationResult` to `SchemaValidationError`, and verify throwing on valid result.
|
|
110
|
+
|
|
111
|
+
### 7. Define LLMExtensions interface
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
export interface LLMExtensions {
|
|
115
|
+
llmDescription?: string | null;
|
|
116
|
+
examples?: unknown[] | null;
|
|
117
|
+
sensitive: boolean;
|
|
118
|
+
constraints?: string | null;
|
|
119
|
+
deprecated?: Record<string, unknown> | null;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 8. Implement mergeAnnotations()
|
|
124
|
+
|
|
125
|
+
Merges YAML-defined and code-defined module annotations with YAML-wins precedence over code, and code-wins precedence over defaults. Operates on the `ANNOTATION_FIELDS` list: `readonly`, `destructive`, `idempotent`, `requiresApproval`, `openWorld`.
|
|
126
|
+
|
|
127
|
+
TDD: Test default-only, code-only, YAML-only, and YAML-overrides-code scenarios.
|
|
128
|
+
|
|
129
|
+
### 9. Implement mergeExamples()
|
|
130
|
+
|
|
131
|
+
YAML examples override code examples entirely. If no YAML examples, fall back to code examples. If neither, return empty array. YAML examples are mapped from raw records to `ModuleExample` objects.
|
|
132
|
+
|
|
133
|
+
TDD: Test YAML-wins, code-fallback, and empty scenarios.
|
|
134
|
+
|
|
135
|
+
### 10. Implement mergeMetadata()
|
|
136
|
+
|
|
137
|
+
Shallow merge with YAML overrides. Start from code metadata (or empty), then `Object.assign()` YAML metadata on top.
|
|
138
|
+
|
|
139
|
+
TDD: Test merge behavior with overlapping keys.
|
|
140
|
+
|
|
141
|
+
## Acceptance Criteria
|
|
142
|
+
|
|
143
|
+
- [x] `SchemaStrategy` enum has `YamlFirst`, `NativeFirst`, `YamlOnly` members
|
|
144
|
+
- [x] `ExportProfile` enum has `MCP`, `OpenAI`, `Anthropic`, `Generic` members
|
|
145
|
+
- [x] `SchemaDefinition` interface includes all required and optional fields
|
|
146
|
+
- [x] `ResolvedSchema` interface includes `jsonSchema`, `schema` (TSchema), `moduleId`, `direction`
|
|
147
|
+
- [x] `SchemaValidationErrorDetail` includes `path`, `message`, optional `constraint`/`expected`/`actual`
|
|
148
|
+
- [x] `validationResultToError()` converts invalid results and throws on valid results
|
|
149
|
+
- [x] `mergeAnnotations()` respects YAML > code > defaults precedence
|
|
150
|
+
- [x] `mergeExamples()` respects YAML-wins-all precedence
|
|
151
|
+
- [x] `mergeMetadata()` performs shallow merge with YAML overrides
|
|
152
|
+
- [x] All types compile with `tsc --noEmit` under strict mode
|
|
153
|
+
|
|
154
|
+
## Dependencies
|
|
155
|
+
|
|
156
|
+
- None (foundation task)
|
|
157
|
+
|
|
158
|
+
## Estimated Time
|
|
159
|
+
|
|
160
|
+
2 hours
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Task: SchemaValidator
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `SchemaValidator` class for runtime validation of data against TypeBox `TSchema` objects. Supports a coercion toggle (`Value.Decode` vs `Value.Check`), detailed error collection via `Value.Errors()`, and separate `validateInput()`/`validateOutput()` entry points that return validated data or throw `SchemaValidationError`.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/schema/validator.ts` -- `SchemaValidator` class
|
|
10
|
+
- `src/schema/types.ts` -- `SchemaValidationResult`, `SchemaValidationErrorDetail`, `validationResultToError()`
|
|
11
|
+
- `src/errors.ts` -- `SchemaValidationError`
|
|
12
|
+
- `tests/schema/test-validator.test.ts` -- Unit tests
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
### 1. Implement constructor with coercion toggle
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
export class SchemaValidator {
|
|
20
|
+
private _coerceTypes: boolean;
|
|
21
|
+
|
|
22
|
+
constructor(coerceTypes: boolean = true) {
|
|
23
|
+
this._coerceTypes = coerceTypes;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
TDD: Verify default `coerceTypes` is `true`. Verify explicit `false` disables coercion.
|
|
29
|
+
|
|
30
|
+
### 2. Implement validate() method
|
|
31
|
+
|
|
32
|
+
When coercion is enabled, attempt `Value.Decode()`. If it succeeds, return `{ valid: true, errors: [] }`. If it throws, collect errors and return `{ valid: false, errors: [...] }`.
|
|
33
|
+
|
|
34
|
+
When coercion is disabled, use `Value.Check()`. If it returns `true`, return valid result. Otherwise, collect errors.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
validate(data: Record<string, unknown>, schema: TSchema): SchemaValidationResult {
|
|
38
|
+
if (this._coerceTypes) {
|
|
39
|
+
try {
|
|
40
|
+
Value.Decode(schema, data);
|
|
41
|
+
return { valid: true, errors: [] };
|
|
42
|
+
} catch {
|
|
43
|
+
return { valid: false, errors: this._collectErrors(schema, data) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Value.Check(schema, data)) {
|
|
48
|
+
return { valid: true, errors: [] };
|
|
49
|
+
}
|
|
50
|
+
return { valid: false, errors: this._collectErrors(schema, data) };
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
TDD: Test valid data returns `{ valid: true }`. Test invalid data returns `{ valid: false }` with non-empty errors array.
|
|
55
|
+
|
|
56
|
+
### 3. Implement _collectErrors() private method
|
|
57
|
+
|
|
58
|
+
Iterate over `Value.Errors(schema, data)` and map each `ValueError` to a `SchemaValidationErrorDetail`.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
private _collectErrors(schema: TSchema, data: unknown): SchemaValidationErrorDetail[] {
|
|
62
|
+
const errors: SchemaValidationErrorDetail[] = [];
|
|
63
|
+
for (const error of Value.Errors(schema, data)) {
|
|
64
|
+
errors.push(this._typeboxErrorToDetail(error));
|
|
65
|
+
}
|
|
66
|
+
return errors;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
TDD: Verify errors have populated `path` and `message` fields.
|
|
71
|
+
|
|
72
|
+
### 4. Implement _typeboxErrorToDetail() mapping
|
|
73
|
+
|
|
74
|
+
Map TypeBox `ValueError` fields to `SchemaValidationErrorDetail`:
|
|
75
|
+
- `error.path` -> `path` (defaults to `'/'` if empty)
|
|
76
|
+
- `error.message` -> `message`
|
|
77
|
+
- `error.type` -> `constraint` (stringified)
|
|
78
|
+
- `error.schema` -> `expected`
|
|
79
|
+
- `error.value` -> `actual`
|
|
80
|
+
|
|
81
|
+
TDD: Verify detail mapping for nested object errors includes correct JSON path.
|
|
82
|
+
|
|
83
|
+
### 5. Implement validateInput() method
|
|
84
|
+
|
|
85
|
+
Call `_validateAndReturn()`. On success, return the (potentially coerced) data. On failure, throw `SchemaValidationError` via `validationResultToError()`.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
validateInput(data: Record<string, unknown>, schema: TSchema): Record<string, unknown> {
|
|
89
|
+
return this._validateAndReturn(data, schema);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
TDD: Test valid input returns data. Test invalid input throws `SchemaValidationError`.
|
|
94
|
+
|
|
95
|
+
### 6. Implement validateOutput() method
|
|
96
|
+
|
|
97
|
+
Same as `validateInput()` -- delegates to `_validateAndReturn()`. Separate method for semantic clarity in the executor pipeline (step 5 vs step 8).
|
|
98
|
+
|
|
99
|
+
TDD: Test valid output returns data. Test invalid output throws `SchemaValidationError`.
|
|
100
|
+
|
|
101
|
+
### 7. Implement _validateAndReturn() private method
|
|
102
|
+
|
|
103
|
+
Shared logic for `validateInput()` and `validateOutput()`. With coercion, use `Value.Decode()` and return the decoded value. Without coercion, use `Value.Check()` and return the original data. On failure, collect errors and throw.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
private _validateAndReturn(data: Record<string, unknown>, schema: TSchema): Record<string, unknown> {
|
|
107
|
+
if (this._coerceTypes) {
|
|
108
|
+
try {
|
|
109
|
+
return Value.Decode(schema, data) as Record<string, unknown>;
|
|
110
|
+
} catch {
|
|
111
|
+
const result: SchemaValidationResult = {
|
|
112
|
+
valid: false,
|
|
113
|
+
errors: this._collectErrors(schema, data),
|
|
114
|
+
};
|
|
115
|
+
throw validationResultToError(result);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (Value.Check(schema, data)) {
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
const result: SchemaValidationResult = {
|
|
122
|
+
valid: false,
|
|
123
|
+
errors: this._collectErrors(schema, data),
|
|
124
|
+
};
|
|
125
|
+
throw validationResultToError(result);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
TDD: Test both coercion and strict paths for valid and invalid data.
|
|
130
|
+
|
|
131
|
+
## Acceptance Criteria
|
|
132
|
+
|
|
133
|
+
- [x] `validate()` returns `{ valid: true, errors: [] }` for conforming data
|
|
134
|
+
- [x] `validate()` returns `{ valid: false, errors: [...] }` for non-conforming data with detailed errors
|
|
135
|
+
- [x] Coercion mode (`coerceTypes: true`) uses `Value.Decode()` for type coercion
|
|
136
|
+
- [x] Strict mode (`coerceTypes: false`) uses `Value.Check()` for exact type matching
|
|
137
|
+
- [x] `validateInput()` returns data on success, throws `SchemaValidationError` on failure
|
|
138
|
+
- [x] `validateOutput()` returns data on success, throws `SchemaValidationError` on failure
|
|
139
|
+
- [x] Error details include `path`, `message`, `constraint`, `expected`, `actual`
|
|
140
|
+
- [x] Nested object errors include correct JSON path (e.g., `/nested/x`)
|
|
141
|
+
- [x] All tests pass with `vitest`
|
|
142
|
+
|
|
143
|
+
## Dependencies
|
|
144
|
+
|
|
145
|
+
- types-and-annotations (for `SchemaValidationResult`, `SchemaValidationErrorDetail`, `validationResultToError()`)
|
|
146
|
+
|
|
147
|
+
## Estimated Time
|
|
148
|
+
|
|
149
|
+
2 hours
|