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,121 @@
|
|
|
1
|
+
# Implementation Plan: Schema System
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the complete schema infrastructure for apcore: YAML-based schema loading with `$ref` resolution, recursive conversion from JSON Schema to TypeBox `TSchema` objects, runtime validation with coercion support, annotation merging between YAML and code metadata, strict-mode transformations for LLM provider compatibility, and multi-format export targeting MCP, OpenAI, Anthropic, and Generic consumers.
|
|
6
|
+
|
|
7
|
+
## Architecture Design
|
|
8
|
+
|
|
9
|
+
### Component Structure
|
|
10
|
+
|
|
11
|
+
- **Types and Interfaces** (`schema/types.ts`, ~74 lines) -- Core type definitions for the schema system. Defines `SchemaStrategy` enum (`YamlFirst`, `NativeFirst`, `YamlOnly`), `ExportProfile` enum (`MCP`, `OpenAI`, `Anthropic`, `Generic`), `SchemaDefinition` interface (moduleId, description, inputSchema, outputSchema, errorSchema, definitions, version, documentation, schemaUrl), `ResolvedSchema` interface (jsonSchema, schema TSchema, moduleId, direction), `SchemaValidationErrorDetail` and `SchemaValidationResult` interfaces, `LLMExtensions` interface, and `validationResultToError()` converter.
|
|
12
|
+
|
|
13
|
+
- **Annotations** (`schema/annotations.ts`, ~67 lines) -- Conflict resolution layer for merging YAML-defined and code-defined module metadata. Exposes `mergeAnnotations()` (boolean annotation fields with YAML-wins precedence), `mergeExamples()` (YAML examples override code examples), and `mergeMetadata()` (shallow merge with YAML overrides). Consumes `ModuleAnnotations` and `ModuleExample` types from `module.ts`.
|
|
14
|
+
|
|
15
|
+
- **SchemaLoader** (`schema/loader.ts`, ~271 lines) -- Primary entry point for the schema system. Manages YAML file loading via `js-yaml`, two-level caching (`_schemaCache` for raw definitions, `_modelCache` for resolved TypeBox pairs), strategy-based schema selection (`getSchema()` with `YamlFirst`/`NativeFirst`/`YamlOnly` behavior), and `$ref` resolution delegation to `RefResolver`. Contains `jsonSchemaToTypeBox()` as an exported function for recursive JSON Schema to TypeBox conversion.
|
|
16
|
+
|
|
17
|
+
- **RefResolver** (`schema/ref-resolver.ts`, ~236 lines) -- `$ref` resolution engine following Algorithm A05. Supports local JSON pointers (`#/definitions/Foo`), relative file paths (`../shared.schema.yaml#/definitions/Bar`), `apcore://` canonical URIs (`apcore://module.id/definitions/Type`), and inline sentinel for self-referencing schemas. Implements circular reference detection via visited-set tracking and configurable max depth (default 32). Caches loaded files in `_fileCache`.
|
|
18
|
+
|
|
19
|
+
- **SchemaValidator** (`schema/validator.ts`, ~83 lines) -- Runtime validation using TypeBox `Value.Check()`, `Value.Decode()`, and `Value.Errors()`. Constructor accepts `coerceTypes` boolean (default `true`). When coercion is enabled, uses `Value.Decode()` for type coercion; when disabled, uses strict `Value.Check()`. Exposes `validate()` (returns `SchemaValidationResult`), `validateInput()` and `validateOutput()` (return data or throw `SchemaValidationError`). Error details include JSON path, message, constraint type, expected schema, and actual value.
|
|
20
|
+
|
|
21
|
+
- **SchemaExporter** (`schema/exporter.ts`, ~94 lines) -- Multi-format schema export for LLM provider integration. Dispatches on `ExportProfile` enum to four export methods: `exportMcp()` (MCP tool format with `readOnlyHint`/`destructiveHint`/`idempotentHint`/`openWorldHint` annotations), `exportOpenai()` (OpenAI function calling with strict mode, `additionalProperties: false`, all-required, LLM descriptions applied), `exportAnthropic()` (Anthropic tool use with `input_schema`, LLM descriptions, extensions stripped, optional `input_examples`), `exportGeneric()` (passthrough with raw input/output schemas and definitions).
|
|
22
|
+
|
|
23
|
+
- **Strict Mode** (`schema/strict.ts`, ~129 lines) -- Schema transformation utilities for OpenAI strict-mode compliance (Algorithm A23). `toStrictSchema()` deep-copies, strips extensions, then applies strict conversion. `stripExtensions()` recursively removes all `x-` prefixed keys and `default` values. `applyLlmDescriptions()` replaces `description` with `x-llm-description` where both exist. Internal `convertToStrict()` sets `additionalProperties: false`, promotes all properties to required, and wraps optional properties with `null` union types.
|
|
24
|
+
|
|
25
|
+
- **Barrel Export** (`schema/index.ts`, ~15 lines) -- Re-exports all public types, classes, and functions from the schema module.
|
|
26
|
+
|
|
27
|
+
### Data Flow
|
|
28
|
+
|
|
29
|
+
The schema system processes module schemas through this pipeline:
|
|
30
|
+
|
|
31
|
+
1. **YAML Loading** -- `SchemaLoader.load()` reads `{moduleId}.schema.yaml` from disk, parses with `js-yaml`, extracts required fields (`input_schema`, `output_schema`, `description`), merges `definitions`/`$defs`, and caches the `SchemaDefinition`
|
|
32
|
+
2. **Ref Resolution** -- `SchemaLoader.resolve()` delegates to `RefResolver.resolve()` which deep-copies the schema and recursively walks all nodes, resolving `$ref` pointers in-place with circular detection
|
|
33
|
+
3. **TypeBox Generation** -- `jsonSchemaToTypeBox()` recursively converts the resolved JSON Schema dictionary to TypeBox `TSchema` objects (`Type.Object`, `Type.Array`, `Type.String`, `Type.Integer`, `Type.Number`, `Type.Boolean`, `Type.Null`, `Type.Union`, `Type.Intersect`, `Type.Literal`, `Type.Record`, `Type.Unknown`)
|
|
34
|
+
4. **Strategy Selection** -- `SchemaLoader.getSchema()` applies the configured `SchemaStrategy` to choose between YAML-loaded and native (code-provided) TypeBox schemas, with fallback behavior per strategy
|
|
35
|
+
5. **Validation** -- `SchemaValidator.validate()` checks runtime data against TypeBox schemas using `Value.Check()` (strict) or `Value.Decode()` (coercion), collecting errors via `Value.Errors()`
|
|
36
|
+
6. **Export** -- `SchemaExporter.export()` transforms the `SchemaDefinition` to the target `ExportProfile` format, applying strict-mode transformations (OpenAI), LLM description substitution (OpenAI, Anthropic), extension stripping (Anthropic), and annotation mapping (MCP)
|
|
37
|
+
|
|
38
|
+
### Technical Choices and Rationale
|
|
39
|
+
|
|
40
|
+
- **TypeBox instead of Pydantic**: The Python apcore uses Pydantic for schema validation and `create_model()` for dynamic model generation. In TypeScript, TypeBox provides the same JSON Schema-based validation through `Value.Check()`/`Value.Decode()` with full TypeScript type inference. TypeBox schemas ARE valid JSON Schema objects, which eliminates a separate conversion layer and simplifies the `jsonSchemaToTypeBox()` function to a thin wrapper that produces TypeBox-tagged objects.
|
|
41
|
+
|
|
42
|
+
- **`jsonSchemaToTypeBox()` instead of `create_model()`**: Pydantic's `create_model()` dynamically generates Python classes at runtime. The TypeScript equivalent maps JSON Schema types to TypeBox builder calls (`Type.Object`, `Type.String`, etc.) which return plain objects with a `[Kind]` symbol tag. This is cheaper than class instantiation and integrates naturally with TypeBox's `Value.*` validation functions.
|
|
43
|
+
|
|
44
|
+
- **`js-yaml` for YAML parsing**: Standard YAML parsing library for Node.js. Used with `yaml.load()` for single-document loading. Synchronous file I/O via `readFileSync` is acceptable since schema loading is a startup-time operation.
|
|
45
|
+
|
|
46
|
+
- **Two-level caching in SchemaLoader**: Separate caches for `SchemaDefinition` (raw YAML parse result) and `[ResolvedSchema, ResolvedSchema]` (resolved TypeBox pair) avoid redundant `$ref` resolution and TypeBox conversion on repeated lookups while allowing cache invalidation at each level independently.
|
|
47
|
+
|
|
48
|
+
- **In-place mutation for `$ref` resolution**: The `RefResolver` deep-copies the schema first, then mutates in-place during resolution. This avoids creating intermediate copies at each `$ref` node while preserving the original schema. The visited-set tracking uses a fresh `Set` per `$ref` chain to allow the same definition to be referenced from multiple locations without false circular-detection positives.
|
|
49
|
+
|
|
50
|
+
- **`JSON.parse(JSON.stringify())` for deep copy**: Used in `RefResolver` and `strict.ts` for deep cloning JSON-compatible schema objects. Suitable because schemas are always JSON-serializable (no functions, symbols, or circular references in the input).
|
|
51
|
+
|
|
52
|
+
## Task Breakdown
|
|
53
|
+
|
|
54
|
+
```mermaid
|
|
55
|
+
graph TD
|
|
56
|
+
T1[types-and-annotations] --> T2[loader]
|
|
57
|
+
T1 --> T3[ref-resolver]
|
|
58
|
+
T1 --> T4[typebox-generation]
|
|
59
|
+
T1 --> T5[validator]
|
|
60
|
+
T3 --> T2
|
|
61
|
+
T4 --> T2
|
|
62
|
+
T5 --> T2
|
|
63
|
+
T1 --> T6[exporter]
|
|
64
|
+
T7[strict-mode] --> T6
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Task ID | Title | Estimated Time | Dependencies |
|
|
68
|
+
|---------|-------|---------------|--------------|
|
|
69
|
+
| types-and-annotations | Core interfaces, enums, and annotation merging | 2h | none |
|
|
70
|
+
| loader | SchemaLoader with YAML loading, caching, strategy | 4h | types-and-annotations, ref-resolver, typebox-generation |
|
|
71
|
+
| ref-resolver | RefResolver with $ref resolution and circular detection | 3h | types-and-annotations |
|
|
72
|
+
| typebox-generation | jsonSchemaToTypeBox() recursive converter | 3h | types-and-annotations |
|
|
73
|
+
| validator | SchemaValidator with TypeBox validation and coercion | 2h | types-and-annotations |
|
|
74
|
+
| exporter | SchemaExporter with 4 export profiles | 3h | types-and-annotations, strict-mode |
|
|
75
|
+
| strict-mode | toStrictSchema(), stripExtensions(), applyLlmDescriptions() | 2h | none |
|
|
76
|
+
|
|
77
|
+
## Risks and Considerations
|
|
78
|
+
|
|
79
|
+
- **TypeBox version compatibility**: The `Value.Decode()` API changed between TypeBox 0.32 and 0.34. The implementation targets `>= 0.34.0` where `Value.Decode()` performs coercion and returns the decoded value. Pinning the minimum version is essential.
|
|
80
|
+
|
|
81
|
+
- **JSON Schema coverage gaps**: `jsonSchemaToTypeBox()` handles the most common JSON Schema constructs (`object`, `array`, `string`, `integer`, `number`, `boolean`, `null`, `enum`, `oneOf`, `anyOf`, `allOf`) but does not cover `if/then/else`, `not`, `patternProperties`, or `$dynamicRef`. Unsupported constructs fall through to `Type.Unknown()`.
|
|
82
|
+
|
|
83
|
+
- **Synchronous file I/O**: `SchemaLoader` and `RefResolver` use `readFileSync` for YAML loading. This is acceptable for startup-time schema loading but would block the event loop if called during request handling. Future versions may need async variants.
|
|
84
|
+
|
|
85
|
+
- **Strict-mode `null` wrapping**: `convertToStrict()` wraps optional properties with `[type, "null"]` arrays or `{ oneOf: [schema, { type: "null" }] }`. This matches OpenAI's strict mode requirements but may confuse validators that do not support type arrays.
|
|
86
|
+
|
|
87
|
+
- **`apcore://` URI resolution**: The canonical URI scheme (`apcore://module.id/definitions/Type`) converts dots to path separators and appends `.schema.yaml`. This convention must be documented and consistently used across all schema files.
|
|
88
|
+
|
|
89
|
+
## Acceptance Criteria
|
|
90
|
+
|
|
91
|
+
- [x] `SchemaDefinition`, `ResolvedSchema`, `SchemaStrategy`, `ExportProfile` types are defined and exported
|
|
92
|
+
- [x] `SchemaLoader.load()` reads YAML files, validates required fields, and caches results
|
|
93
|
+
- [x] `SchemaLoader.getSchema()` applies `YamlFirst`/`NativeFirst`/`YamlOnly` strategy correctly
|
|
94
|
+
- [x] `RefResolver.resolve()` handles local pointers, relative files, `apcore://` URIs, and nested `$ref` chains
|
|
95
|
+
- [x] `RefResolver` detects circular references and raises `SchemaCircularRefError`
|
|
96
|
+
- [x] `RefResolver` enforces max depth and raises on exceeded depth
|
|
97
|
+
- [x] `jsonSchemaToTypeBox()` converts all supported JSON Schema types to TypeBox equivalents
|
|
98
|
+
- [x] `jsonSchemaToTypeBox()` passes through constraint options (`minLength`, `maximum`, etc.)
|
|
99
|
+
- [x] `SchemaValidator.validate()` returns `SchemaValidationResult` with detailed error paths
|
|
100
|
+
- [x] `SchemaValidator` supports coercion mode (`Value.Decode`) and strict mode (`Value.Check`)
|
|
101
|
+
- [x] `SchemaExporter` produces correct MCP, OpenAI, Anthropic, and Generic export formats
|
|
102
|
+
- [x] `toStrictSchema()` sets `additionalProperties: false`, makes all properties required, wraps optionals with null
|
|
103
|
+
- [x] `stripExtensions()` removes all `x-` keys and `default` values recursively
|
|
104
|
+
- [x] `applyLlmDescriptions()` replaces `description` with `x-llm-description` where present
|
|
105
|
+
- [x] `mergeAnnotations()`, `mergeExamples()`, `mergeMetadata()` merge YAML/code metadata with correct precedence
|
|
106
|
+
- [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
|
|
107
|
+
|
|
108
|
+
## References
|
|
109
|
+
|
|
110
|
+
- `src/schema/types.ts` -- Type definitions and enums
|
|
111
|
+
- `src/schema/annotations.ts` -- Annotation conflict resolution
|
|
112
|
+
- `src/schema/loader.ts` -- SchemaLoader and jsonSchemaToTypeBox()
|
|
113
|
+
- `src/schema/ref-resolver.ts` -- RefResolver
|
|
114
|
+
- `src/schema/validator.ts` -- SchemaValidator
|
|
115
|
+
- `src/schema/exporter.ts` -- SchemaExporter
|
|
116
|
+
- `src/schema/strict.ts` -- Strict-mode utilities
|
|
117
|
+
- `src/schema/index.ts` -- Barrel export
|
|
118
|
+
- `tests/schema/test-loader.test.ts` -- Loader and TypeBox generation tests
|
|
119
|
+
- `tests/schema/test-validator.test.ts` -- Validator tests
|
|
120
|
+
- `tests/schema/test-ref-resolver.test.ts` -- RefResolver tests
|
|
121
|
+
- `tests/schema/test-strict.test.ts` -- Strict-mode tests
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"feature": "schema-system",
|
|
3
|
+
"created": "2026-02-16T00:00:00Z",
|
|
4
|
+
"updated": "2026-02-16T00:00:00Z",
|
|
5
|
+
"status": "completed",
|
|
6
|
+
"execution_order": [
|
|
7
|
+
"types-and-annotations",
|
|
8
|
+
"loader",
|
|
9
|
+
"ref-resolver",
|
|
10
|
+
"typebox-generation",
|
|
11
|
+
"validator",
|
|
12
|
+
"exporter",
|
|
13
|
+
"strict-mode"
|
|
14
|
+
],
|
|
15
|
+
"progress": {
|
|
16
|
+
"total_tasks": 7,
|
|
17
|
+
"completed": 7,
|
|
18
|
+
"in_progress": 0,
|
|
19
|
+
"pending": 0
|
|
20
|
+
},
|
|
21
|
+
"tasks": [
|
|
22
|
+
{
|
|
23
|
+
"id": "types-and-annotations",
|
|
24
|
+
"file": "tasks/types-and-annotations.md",
|
|
25
|
+
"title": "Core Interfaces, Enums, and Annotation Merging",
|
|
26
|
+
"status": "completed",
|
|
27
|
+
"started_at": "2026-02-16T08:00:00Z",
|
|
28
|
+
"completed_at": "2026-02-16T09:30:00Z",
|
|
29
|
+
"assignee": null,
|
|
30
|
+
"commits": []
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "loader",
|
|
34
|
+
"file": "tasks/loader.md",
|
|
35
|
+
"title": "SchemaLoader with YAML Loading, Caching, and Strategy Selection",
|
|
36
|
+
"status": "completed",
|
|
37
|
+
"started_at": "2026-02-16T09:30:00Z",
|
|
38
|
+
"completed_at": "2026-02-16T12:30:00Z",
|
|
39
|
+
"assignee": null,
|
|
40
|
+
"commits": []
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "ref-resolver",
|
|
44
|
+
"file": "tasks/ref-resolver.md",
|
|
45
|
+
"title": "RefResolver with $ref Resolution and Circular Detection",
|
|
46
|
+
"status": "completed",
|
|
47
|
+
"started_at": "2026-02-16T12:30:00Z",
|
|
48
|
+
"completed_at": "2026-02-16T15:00:00Z",
|
|
49
|
+
"assignee": null,
|
|
50
|
+
"commits": []
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "typebox-generation",
|
|
54
|
+
"file": "tasks/typebox-generation.md",
|
|
55
|
+
"title": "jsonSchemaToTypeBox() Recursive Converter",
|
|
56
|
+
"status": "completed",
|
|
57
|
+
"started_at": "2026-02-16T15:00:00Z",
|
|
58
|
+
"completed_at": "2026-02-16T17:30:00Z",
|
|
59
|
+
"assignee": null,
|
|
60
|
+
"commits": []
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "validator",
|
|
64
|
+
"file": "tasks/validator.md",
|
|
65
|
+
"title": "SchemaValidator with TypeBox Validation and Coercion",
|
|
66
|
+
"status": "completed",
|
|
67
|
+
"started_at": "2026-02-16T17:30:00Z",
|
|
68
|
+
"completed_at": "2026-02-16T19:00:00Z",
|
|
69
|
+
"assignee": null,
|
|
70
|
+
"commits": []
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "exporter",
|
|
74
|
+
"file": "tasks/exporter.md",
|
|
75
|
+
"title": "SchemaExporter with 4 Export Profiles",
|
|
76
|
+
"status": "completed",
|
|
77
|
+
"started_at": "2026-02-16T19:00:00Z",
|
|
78
|
+
"completed_at": "2026-02-16T21:30:00Z",
|
|
79
|
+
"assignee": null,
|
|
80
|
+
"commits": []
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "strict-mode",
|
|
84
|
+
"file": "tasks/strict-mode.md",
|
|
85
|
+
"title": "Strict-Mode Transformations (toStrictSchema, stripExtensions, applyLlmDescriptions)",
|
|
86
|
+
"status": "completed",
|
|
87
|
+
"started_at": "2026-02-16T21:30:00Z",
|
|
88
|
+
"completed_at": "2026-02-16T23:00:00Z",
|
|
89
|
+
"assignee": null,
|
|
90
|
+
"commits": []
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"metadata": {
|
|
94
|
+
"source_doc": "planning/features/schema-system.md",
|
|
95
|
+
"created_by": "code-forge",
|
|
96
|
+
"version": "1.0"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Task: SchemaExporter
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `SchemaExporter` class that converts `SchemaDefinition` objects to platform-specific export formats for four LLM providers: MCP (Model Context Protocol), OpenAI (function calling with strict mode), Anthropic (tool use with examples), and Generic (passthrough for non-LLM consumers).
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/schema/exporter.ts` -- `SchemaExporter` class
|
|
10
|
+
- `src/schema/strict.ts` -- Consumed for `toStrictSchema()`, `applyLlmDescriptions()`, `stripExtensions()`
|
|
11
|
+
- `src/schema/types.ts` -- Consumed for `ExportProfile`, `SchemaDefinition`
|
|
12
|
+
- `src/module.ts` -- Consumed for `ModuleAnnotations`, `ModuleExample`
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
### 1. Implement export() dispatcher
|
|
17
|
+
|
|
18
|
+
Route to the correct export method based on `ExportProfile` enum value.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
export class SchemaExporter {
|
|
22
|
+
export(
|
|
23
|
+
schemaDef: SchemaDefinition,
|
|
24
|
+
profile: ExportProfile,
|
|
25
|
+
annotations?: ModuleAnnotations | null,
|
|
26
|
+
examples?: ModuleExample[] | null,
|
|
27
|
+
name?: string | null,
|
|
28
|
+
): Record<string, unknown> {
|
|
29
|
+
if (profile === ExportProfile.MCP) return this.exportMcp(schemaDef, annotations, name);
|
|
30
|
+
if (profile === ExportProfile.OpenAI) return this.exportOpenai(schemaDef);
|
|
31
|
+
if (profile === ExportProfile.Anthropic) return this.exportAnthropic(schemaDef, examples);
|
|
32
|
+
return this.exportGeneric(schemaDef);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
TDD: Test dispatch routes to correct method for each profile.
|
|
38
|
+
|
|
39
|
+
### 2. Implement exportMcp()
|
|
40
|
+
|
|
41
|
+
Produce MCP tool format with `name`, `description`, `inputSchema`, and `annotations` object mapping module annotations to MCP hint fields.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
exportMcp(
|
|
45
|
+
schemaDef: SchemaDefinition,
|
|
46
|
+
annotations?: ModuleAnnotations | null,
|
|
47
|
+
name?: string | null,
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
return {
|
|
50
|
+
name: name ?? schemaDef.moduleId,
|
|
51
|
+
description: schemaDef.description,
|
|
52
|
+
inputSchema: schemaDef.inputSchema,
|
|
53
|
+
annotations: {
|
|
54
|
+
readOnlyHint: annotations?.readonly ?? false,
|
|
55
|
+
destructiveHint: annotations?.destructive ?? false,
|
|
56
|
+
idempotentHint: annotations?.idempotent ?? false,
|
|
57
|
+
openWorldHint: annotations?.openWorld ?? true,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
TDD: Test MCP output includes correct annotation hints. Test custom `name` override. Test default `openWorldHint` is `true`.
|
|
64
|
+
|
|
65
|
+
### 3. Implement exportOpenai()
|
|
66
|
+
|
|
67
|
+
Produce OpenAI function-calling format with strict mode. Deep-copy input schema, apply LLM descriptions, convert to strict schema, wrap in `{ type: "function", function: { name, description, parameters, strict: true } }`. Module ID dots are replaced with underscores for the function name.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
exportOpenai(schemaDef: SchemaDefinition): Record<string, unknown> {
|
|
71
|
+
const schema = deepCopy(schemaDef.inputSchema);
|
|
72
|
+
applyLlmDescriptions(schema);
|
|
73
|
+
const strictSchema = toStrictSchema(schema);
|
|
74
|
+
return {
|
|
75
|
+
type: 'function',
|
|
76
|
+
function: {
|
|
77
|
+
name: schemaDef.moduleId.replace(/\./g, '_'),
|
|
78
|
+
description: schemaDef.description,
|
|
79
|
+
parameters: strictSchema,
|
|
80
|
+
strict: true,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
TDD: Test function name replaces dots with underscores. Test `strict: true` is set. Test `additionalProperties: false` is present in parameters.
|
|
87
|
+
|
|
88
|
+
### 4. Implement exportAnthropic()
|
|
89
|
+
|
|
90
|
+
Produce Anthropic tool-use format. Deep-copy input schema, apply LLM descriptions, strip extensions. Include `input_examples` from module examples if available.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
exportAnthropic(
|
|
94
|
+
schemaDef: SchemaDefinition,
|
|
95
|
+
examples?: ModuleExample[] | null,
|
|
96
|
+
): Record<string, unknown> {
|
|
97
|
+
const schema = deepCopy(schemaDef.inputSchema);
|
|
98
|
+
applyLlmDescriptions(schema);
|
|
99
|
+
stripExtensions(schema);
|
|
100
|
+
const result: Record<string, unknown> = {
|
|
101
|
+
name: schemaDef.moduleId.replace(/\./g, '_'),
|
|
102
|
+
description: schemaDef.description,
|
|
103
|
+
input_schema: schema,
|
|
104
|
+
};
|
|
105
|
+
if (examples && examples.length > 0) {
|
|
106
|
+
result['input_examples'] = examples.map((ex) => ex.inputs);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
TDD: Test `x-` extensions are removed from output. Test examples are included when provided. Test examples are omitted when empty.
|
|
113
|
+
|
|
114
|
+
### 5. Implement exportGeneric()
|
|
115
|
+
|
|
116
|
+
Passthrough format with all schema data for non-LLM consumers.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
exportGeneric(schemaDef: SchemaDefinition): Record<string, unknown> {
|
|
120
|
+
return {
|
|
121
|
+
module_id: schemaDef.moduleId,
|
|
122
|
+
description: schemaDef.description,
|
|
123
|
+
input_schema: schemaDef.inputSchema,
|
|
124
|
+
output_schema: schemaDef.outputSchema,
|
|
125
|
+
definitions: schemaDef.definitions,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
TDD: Test all fields are present including `output_schema` and `definitions`.
|
|
131
|
+
|
|
132
|
+
## Acceptance Criteria
|
|
133
|
+
|
|
134
|
+
- [x] `export()` dispatches to the correct profile method
|
|
135
|
+
- [x] MCP format includes `name`, `description`, `inputSchema`, and annotation hints
|
|
136
|
+
- [x] MCP format supports custom `name` override via parameter
|
|
137
|
+
- [x] OpenAI format wraps schema in `{ type: "function", function: { ... } }` with `strict: true`
|
|
138
|
+
- [x] OpenAI format applies LLM descriptions and strict-mode transformations
|
|
139
|
+
- [x] OpenAI format replaces dots with underscores in function name
|
|
140
|
+
- [x] Anthropic format applies LLM descriptions and strips extensions
|
|
141
|
+
- [x] Anthropic format includes `input_examples` when examples are provided
|
|
142
|
+
- [x] Generic format passes through all schema data including `output_schema` and `definitions`
|
|
143
|
+
- [x] No export method mutates the original `SchemaDefinition`
|
|
144
|
+
- [x] All tests pass with `vitest`
|
|
145
|
+
|
|
146
|
+
## Dependencies
|
|
147
|
+
|
|
148
|
+
- types-and-annotations (for `ExportProfile`, `SchemaDefinition`)
|
|
149
|
+
- strict-mode (for `toStrictSchema()`, `stripExtensions()`, `applyLlmDescriptions()`)
|
|
150
|
+
|
|
151
|
+
## Estimated Time
|
|
152
|
+
|
|
153
|
+
3 hours
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Task: SchemaLoader
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `SchemaLoader` class as the primary entry point for the schema system. It loads YAML schema files from disk, resolves `$ref` references via `RefResolver`, converts JSON Schema dictionaries to TypeBox `TSchema` objects via `jsonSchemaToTypeBox()`, and supports three schema resolution strategies with two-level caching.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/schema/loader.ts` -- `SchemaLoader` class and `jsonSchemaToTypeBox()` function
|
|
10
|
+
- `src/schema/ref-resolver.ts` -- Consumed for `$ref` resolution
|
|
11
|
+
- `src/schema/types.ts` -- Consumed for `SchemaDefinition`, `ResolvedSchema`, `SchemaStrategy`
|
|
12
|
+
- `tests/schema/test-loader.test.ts` -- Unit tests for `jsonSchemaToTypeBox()` and SchemaLoader
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
### 1. Implement SchemaLoader constructor
|
|
17
|
+
|
|
18
|
+
Accept `Config` and optional `schemasDir`. Resolve the schemas directory from config (`schema.root`, default `./schemas`). Create a `RefResolver` with `schema.max_ref_depth` (default 32). Initialize empty caches.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
export class SchemaLoader {
|
|
22
|
+
private _config: Config;
|
|
23
|
+
private _schemasDir: string;
|
|
24
|
+
private _resolver: RefResolver;
|
|
25
|
+
private _schemaCache: Map<string, SchemaDefinition> = new Map();
|
|
26
|
+
private _modelCache: Map<string, [ResolvedSchema, ResolvedSchema]> = new Map();
|
|
27
|
+
|
|
28
|
+
constructor(config: Config, schemasDir?: string | null) {
|
|
29
|
+
this._config = config;
|
|
30
|
+
this._schemasDir = schemasDir
|
|
31
|
+
? resolve(schemasDir)
|
|
32
|
+
: resolve(config.get('schema.root', './schemas') as string);
|
|
33
|
+
const maxDepth = config.get('schema.max_ref_depth', 32) as number;
|
|
34
|
+
this._resolver = new RefResolver(this._schemasDir, maxDepth);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
TDD: Verify constructor sets `_schemasDir` from explicit param and from config fallback.
|
|
40
|
+
|
|
41
|
+
### 2. Implement load() method
|
|
42
|
+
|
|
43
|
+
Read YAML file at `{schemasDir}/{moduleId.replace('.','/')}.schema.yaml`. Parse with `js-yaml`. Validate required fields (`input_schema`, `output_schema`, `description`). Merge `definitions` and `$defs`. Cache and return `SchemaDefinition`.
|
|
44
|
+
|
|
45
|
+
TDD: Test successful load, missing file (`SchemaNotFoundError`), invalid YAML (`SchemaParseError`), missing required fields (`SchemaParseError`), and cache hit on second call.
|
|
46
|
+
|
|
47
|
+
### 3. Implement resolve() method
|
|
48
|
+
|
|
49
|
+
Delegate to `RefResolver.resolve()` for both input and output schemas. Convert resolved JSON Schema to TypeBox via `jsonSchemaToTypeBox()`. Return `[ResolvedSchema, ResolvedSchema]` tuple.
|
|
50
|
+
|
|
51
|
+
TDD: Test resolution of a schema with `$ref` pointers produces valid TypeBox schemas.
|
|
52
|
+
|
|
53
|
+
### 4. Implement getSchema() with strategy selection
|
|
54
|
+
|
|
55
|
+
Parse the strategy from config string (`schema.strategy`, default `yaml_first`). Convert snake_case to PascalCase for enum lookup. Apply strategy logic:
|
|
56
|
+
- `YamlFirst`: Try YAML, fall back to native if `SchemaNotFoundError` and native schemas provided
|
|
57
|
+
- `NativeFirst`: Use native if provided, else fall back to YAML
|
|
58
|
+
- `YamlOnly`: YAML only, no fallback
|
|
59
|
+
|
|
60
|
+
Cache and return result.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
getSchema(
|
|
64
|
+
moduleId: string,
|
|
65
|
+
nativeInputSchema?: TSchema | null,
|
|
66
|
+
nativeOutputSchema?: TSchema | null,
|
|
67
|
+
): [ResolvedSchema, ResolvedSchema]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
TDD: Test each strategy path, including fallback behavior and `SchemaNotFoundError` propagation.
|
|
71
|
+
|
|
72
|
+
### 5. Implement _wrapNative() helper
|
|
73
|
+
|
|
74
|
+
Wrap native TypeBox schemas into `ResolvedSchema` objects with `jsonSchema` set to the TypeBox schema cast as `Record<string, unknown>`.
|
|
75
|
+
|
|
76
|
+
TDD: Verify wrapped native schemas have correct `direction` and `moduleId`.
|
|
77
|
+
|
|
78
|
+
### 6. Implement clearCache()
|
|
79
|
+
|
|
80
|
+
Clear all three caches: `_schemaCache`, `_modelCache`, and `_resolver.clearCache()`.
|
|
81
|
+
|
|
82
|
+
TDD: Verify cache is cleared and subsequent loads re-read from disk.
|
|
83
|
+
|
|
84
|
+
## Acceptance Criteria
|
|
85
|
+
|
|
86
|
+
- [x] `SchemaLoader.load()` reads and parses YAML schema files correctly
|
|
87
|
+
- [x] `SchemaLoader.load()` throws `SchemaNotFoundError` for missing files
|
|
88
|
+
- [x] `SchemaLoader.load()` throws `SchemaParseError` for invalid YAML or missing required fields
|
|
89
|
+
- [x] `SchemaLoader.load()` caches `SchemaDefinition` on first load
|
|
90
|
+
- [x] `SchemaLoader.resolve()` produces `[ResolvedSchema, ResolvedSchema]` with TypeBox schemas
|
|
91
|
+
- [x] `SchemaLoader.getSchema()` applies `YamlFirst` strategy with native fallback
|
|
92
|
+
- [x] `SchemaLoader.getSchema()` applies `NativeFirst` strategy with YAML fallback
|
|
93
|
+
- [x] `SchemaLoader.getSchema()` applies `YamlOnly` strategy without fallback
|
|
94
|
+
- [x] `SchemaLoader.getSchema()` caches resolved schema pairs
|
|
95
|
+
- [x] `SchemaLoader.clearCache()` invalidates all caches
|
|
96
|
+
- [x] All tests pass with `vitest`
|
|
97
|
+
|
|
98
|
+
## Dependencies
|
|
99
|
+
|
|
100
|
+
- types-and-annotations (for `SchemaDefinition`, `ResolvedSchema`, `SchemaStrategy`)
|
|
101
|
+
- ref-resolver (for `RefResolver`)
|
|
102
|
+
- typebox-generation (for `jsonSchemaToTypeBox()`)
|
|
103
|
+
|
|
104
|
+
## Estimated Time
|
|
105
|
+
|
|
106
|
+
4 hours
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Task: RefResolver
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement the `RefResolver` class for resolving `$ref` pointers in JSON Schema documents. Supports local JSON pointers, relative file paths, `apcore://` canonical URIs, nested `$ref` chains, sibling key merging, circular reference detection, and configurable max depth.
|
|
6
|
+
|
|
7
|
+
## Files Involved
|
|
8
|
+
|
|
9
|
+
- `src/schema/ref-resolver.ts` -- `RefResolver` class
|
|
10
|
+
- `src/errors.ts` -- `SchemaCircularRefError`, `SchemaNotFoundError`, `SchemaParseError`
|
|
11
|
+
- `tests/schema/test-ref-resolver.test.ts` -- Unit tests
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
### 1. Implement constructor and file cache
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
export class RefResolver {
|
|
19
|
+
private _schemasDir: string;
|
|
20
|
+
private _maxDepth: number;
|
|
21
|
+
private _fileCache: Map<string, Record<string, unknown>> = new Map();
|
|
22
|
+
|
|
23
|
+
constructor(schemasDir: string, maxDepth: number = 32) {
|
|
24
|
+
this._schemasDir = resolve(schemasDir);
|
|
25
|
+
this._maxDepth = maxDepth;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
TDD: Verify constructor resolves `schemasDir` to absolute path and stores `maxDepth`.
|
|
31
|
+
|
|
32
|
+
### 2. Implement resolve() entry point
|
|
33
|
+
|
|
34
|
+
Deep-copy the input schema. Set it as the inline sentinel (`__inline__`) in the file cache. Walk all nodes via `_resolveNode()`. Clean up sentinel on completion.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
resolve(schema: Record<string, unknown>, currentFile?: string | null): Record<string, unknown> {
|
|
38
|
+
const result = deepCopy(schema);
|
|
39
|
+
this._fileCache.set(INLINE_SENTINEL, result);
|
|
40
|
+
try {
|
|
41
|
+
this._resolveNode(result, currentFile ?? null, new Set(), 0);
|
|
42
|
+
} finally {
|
|
43
|
+
this._fileCache.delete(INLINE_SENTINEL);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
TDD: Test that schemas without `$ref` pass through unchanged (deep-copied).
|
|
50
|
+
|
|
51
|
+
### 3. Implement _parseRef() for $ref URI parsing
|
|
52
|
+
|
|
53
|
+
Handle four `$ref` formats:
|
|
54
|
+
- Local pointer: `#/definitions/Foo` -- resolve against current file or inline sentinel
|
|
55
|
+
- `apcore://` URI: `apcore://module.id/definitions/Type` -- convert dots to path separators, append `.schema.yaml`
|
|
56
|
+
- File with pointer: `../shared.yaml#/definitions/Bar` -- resolve file relative to current file or schemas dir
|
|
57
|
+
- File only: `../shared.yaml` -- resolve file with empty pointer
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
private _parseRef(refString: string, currentFile: string | null): [string, string]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
TDD: Test each format with expected `[filePath, jsonPointer]` output.
|
|
64
|
+
|
|
65
|
+
### 4. Implement _resolveJsonPointer()
|
|
66
|
+
|
|
67
|
+
Walk the JSON pointer segments (split on `/`, skip empty leading segment, unescape `~1` -> `/` and `~0` -> `~`). Throw `SchemaNotFoundError` if any segment is not found.
|
|
68
|
+
|
|
69
|
+
TDD: Test successful pointer traversal, nested paths, and missing segment error.
|
|
70
|
+
|
|
71
|
+
### 5. Implement resolveRef() for single $ref resolution
|
|
72
|
+
|
|
73
|
+
Check visited set for circular detection. Check depth against `_maxDepth`. Parse the `$ref` string. Load the target file. Resolve the JSON pointer within the document. Deep-copy the target. Merge sibling keys if present. Recursively resolve nested `$ref` in the result.
|
|
74
|
+
|
|
75
|
+
TDD: Test basic `$ref` resolution, sibling key merging, nested `$ref` chains.
|
|
76
|
+
|
|
77
|
+
### 6. Implement _resolveNode() recursive walker
|
|
78
|
+
|
|
79
|
+
Walk objects and arrays. When a `$ref` key is found, extract sibling keys, call `resolveRef()`, then replace the node contents in-place. For non-`$ref` objects and arrays, recurse into children.
|
|
80
|
+
|
|
81
|
+
TDD: Test resolution of schemas with multiple `$ref` at different nesting levels.
|
|
82
|
+
|
|
83
|
+
### 7. Implement circular reference detection
|
|
84
|
+
|
|
85
|
+
The visited set tracks resolved `$ref` strings within a single chain. A fresh `Set` copy is used for each branch of the tree to avoid false positives when the same definition is referenced from multiple independent locations.
|
|
86
|
+
|
|
87
|
+
TDD: Test that `A -> B -> A` circular chain throws `SchemaCircularRefError`. Test that the same definition referenced from two independent properties does NOT throw.
|
|
88
|
+
|
|
89
|
+
### 8. Implement max depth enforcement
|
|
90
|
+
|
|
91
|
+
When `depth >= _maxDepth`, throw `SchemaCircularRefError` with a descriptive message including the ref string and max depth value.
|
|
92
|
+
|
|
93
|
+
TDD: Test with `maxDepth = 2` and a 3-deep chain.
|
|
94
|
+
|
|
95
|
+
### 9. Implement _loadFile() with caching
|
|
96
|
+
|
|
97
|
+
Load YAML files via `readFileSync` and `yaml.load()`. Cache parsed results in `_fileCache`. Handle empty files (return `{}`), non-mapping files (`SchemaParseError`), and missing files (`SchemaNotFoundError`).
|
|
98
|
+
|
|
99
|
+
TDD: Test file loading, caching (second call returns same object), and error cases.
|
|
100
|
+
|
|
101
|
+
### 10. Implement clearCache()
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
clearCache(): void {
|
|
105
|
+
this._fileCache.clear();
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
TDD: Verify cache is cleared.
|
|
110
|
+
|
|
111
|
+
## Acceptance Criteria
|
|
112
|
+
|
|
113
|
+
- [x] Local `#/definitions/Foo` pointers resolve correctly
|
|
114
|
+
- [x] Relative file paths with pointers (`../shared.yaml#/definitions/Bar`) resolve correctly
|
|
115
|
+
- [x] `apcore://module.id/definitions/Type` canonical URIs resolve to correct file paths
|
|
116
|
+
- [x] Nested `$ref` chains (ref pointing to another ref) resolve fully
|
|
117
|
+
- [x] Sibling keys alongside `$ref` are merged into the resolved result
|
|
118
|
+
- [x] Circular references (`A -> B -> A`) throw `SchemaCircularRefError`
|
|
119
|
+
- [x] Max depth exceeded throws `SchemaCircularRefError` with descriptive message
|
|
120
|
+
- [x] Schemas without `$ref` pass through as deep copies
|
|
121
|
+
- [x] Missing files throw `SchemaNotFoundError`
|
|
122
|
+
- [x] Invalid YAML throws `SchemaParseError`
|
|
123
|
+
- [x] File cache prevents redundant disk reads
|
|
124
|
+
- [x] `clearCache()` invalidates all cached files
|
|
125
|
+
- [x] All tests pass with `vitest`
|
|
126
|
+
|
|
127
|
+
## Dependencies
|
|
128
|
+
|
|
129
|
+
- types-and-annotations (for error types from `errors.ts`)
|
|
130
|
+
|
|
131
|
+
## Estimated Time
|
|
132
|
+
|
|
133
|
+
3 hours
|