apcore-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.gitmessage +60 -0
  3. package/.pre-commit-config.yaml +28 -0
  4. package/CHANGELOG.md +47 -0
  5. package/CLAUDE.md +68 -0
  6. package/README.md +131 -0
  7. package/apcore-logo.svg +79 -0
  8. package/package.json +37 -0
  9. package/planning/acl-system/overview.md +54 -0
  10. package/planning/acl-system/plan.md +92 -0
  11. package/planning/acl-system/state.json +76 -0
  12. package/planning/acl-system/tasks/acl-core.md +226 -0
  13. package/planning/acl-system/tasks/acl-rule.md +92 -0
  14. package/planning/acl-system/tasks/conditional-rules.md +259 -0
  15. package/planning/acl-system/tasks/pattern-matching.md +152 -0
  16. package/planning/acl-system/tasks/yaml-loading.md +271 -0
  17. package/planning/core-executor/overview.md +53 -0
  18. package/planning/core-executor/plan.md +88 -0
  19. package/planning/core-executor/state.json +76 -0
  20. package/planning/core-executor/tasks/async-support.md +106 -0
  21. package/planning/core-executor/tasks/execution-pipeline.md +113 -0
  22. package/planning/core-executor/tasks/redaction.md +85 -0
  23. package/planning/core-executor/tasks/safety-checks.md +65 -0
  24. package/planning/core-executor/tasks/setup.md +75 -0
  25. package/planning/decorator-bindings/overview.md +62 -0
  26. package/planning/decorator-bindings/plan.md +104 -0
  27. package/planning/decorator-bindings/state.json +87 -0
  28. package/planning/decorator-bindings/tasks/binding-directory.md +79 -0
  29. package/planning/decorator-bindings/tasks/binding-loader.md +148 -0
  30. package/planning/decorator-bindings/tasks/explicit-schemas.md +85 -0
  31. package/planning/decorator-bindings/tasks/function-module.md +127 -0
  32. package/planning/decorator-bindings/tasks/module-factory.md +89 -0
  33. package/planning/decorator-bindings/tasks/schema-modes.md +142 -0
  34. package/planning/middleware-system/overview.md +48 -0
  35. package/planning/middleware-system/plan.md +102 -0
  36. package/planning/middleware-system/state.json +65 -0
  37. package/planning/middleware-system/tasks/adapters.md +170 -0
  38. package/planning/middleware-system/tasks/base.md +115 -0
  39. package/planning/middleware-system/tasks/logging-middleware.md +304 -0
  40. package/planning/middleware-system/tasks/manager.md +313 -0
  41. package/planning/observability/overview.md +53 -0
  42. package/planning/observability/plan.md +119 -0
  43. package/planning/observability/state.json +98 -0
  44. package/planning/observability/tasks/context-logger.md +201 -0
  45. package/planning/observability/tasks/exporters.md +121 -0
  46. package/planning/observability/tasks/metrics-collector.md +162 -0
  47. package/planning/observability/tasks/metrics-middleware.md +141 -0
  48. package/planning/observability/tasks/obs-logging-middleware.md +179 -0
  49. package/planning/observability/tasks/span-model.md +120 -0
  50. package/planning/observability/tasks/tracing-middleware.md +179 -0
  51. package/planning/overview.md +81 -0
  52. package/planning/registry-system/overview.md +57 -0
  53. package/planning/registry-system/plan.md +114 -0
  54. package/planning/registry-system/state.json +109 -0
  55. package/planning/registry-system/tasks/dependencies.md +157 -0
  56. package/planning/registry-system/tasks/entry-point.md +148 -0
  57. package/planning/registry-system/tasks/metadata.md +198 -0
  58. package/planning/registry-system/tasks/registry-core.md +323 -0
  59. package/planning/registry-system/tasks/scanner.md +172 -0
  60. package/planning/registry-system/tasks/schema-export.md +261 -0
  61. package/planning/registry-system/tasks/types.md +124 -0
  62. package/planning/registry-system/tasks/validation.md +177 -0
  63. package/planning/schema-system/overview.md +56 -0
  64. package/planning/schema-system/plan.md +121 -0
  65. package/planning/schema-system/state.json +98 -0
  66. package/planning/schema-system/tasks/exporter.md +153 -0
  67. package/planning/schema-system/tasks/loader.md +106 -0
  68. package/planning/schema-system/tasks/ref-resolver.md +133 -0
  69. package/planning/schema-system/tasks/strict-mode.md +140 -0
  70. package/planning/schema-system/tasks/typebox-generation.md +133 -0
  71. package/planning/schema-system/tasks/types-and-annotations.md +160 -0
  72. package/planning/schema-system/tasks/validator.md +149 -0
  73. package/src/acl.ts +188 -0
  74. package/src/bindings.ts +208 -0
  75. package/src/config.ts +24 -0
  76. package/src/context.ts +75 -0
  77. package/src/decorator.ts +110 -0
  78. package/src/errors.ts +369 -0
  79. package/src/executor.ts +348 -0
  80. package/src/index.ts +81 -0
  81. package/src/middleware/adapters.ts +54 -0
  82. package/src/middleware/base.ts +33 -0
  83. package/src/middleware/index.ts +6 -0
  84. package/src/middleware/logging.ts +103 -0
  85. package/src/middleware/manager.ts +105 -0
  86. package/src/module.ts +41 -0
  87. package/src/observability/context-logger.ts +201 -0
  88. package/src/observability/index.ts +4 -0
  89. package/src/observability/metrics.ts +212 -0
  90. package/src/observability/tracing.ts +187 -0
  91. package/src/registry/dependencies.ts +99 -0
  92. package/src/registry/entry-point.ts +64 -0
  93. package/src/registry/index.ts +8 -0
  94. package/src/registry/metadata.ts +111 -0
  95. package/src/registry/registry.ts +314 -0
  96. package/src/registry/scanner.ts +150 -0
  97. package/src/registry/schema-export.ts +177 -0
  98. package/src/registry/types.ts +32 -0
  99. package/src/registry/validation.ts +38 -0
  100. package/src/schema/annotations.ts +67 -0
  101. package/src/schema/exporter.ts +93 -0
  102. package/src/schema/index.ts +14 -0
  103. package/src/schema/loader.ts +270 -0
  104. package/src/schema/ref-resolver.ts +235 -0
  105. package/src/schema/strict.ts +128 -0
  106. package/src/schema/types.ts +73 -0
  107. package/src/schema/validator.ts +82 -0
  108. package/src/utils/index.ts +1 -0
  109. package/src/utils/pattern.ts +30 -0
  110. package/tests/helpers.ts +30 -0
  111. package/tests/integration/test-acl-safety.test.ts +268 -0
  112. package/tests/integration/test-binding-executor.test.ts +194 -0
  113. package/tests/integration/test-e2e-flow.test.ts +117 -0
  114. package/tests/integration/test-error-propagation.test.ts +259 -0
  115. package/tests/integration/test-middleware-chain.test.ts +120 -0
  116. package/tests/integration/test-observability-integration.test.ts +438 -0
  117. package/tests/observability/test-context-logger.test.ts +123 -0
  118. package/tests/observability/test-metrics.test.ts +89 -0
  119. package/tests/observability/test-tracing.test.ts +131 -0
  120. package/tests/registry/test-dependencies.test.ts +70 -0
  121. package/tests/registry/test-entry-point.test.ts +133 -0
  122. package/tests/registry/test-metadata.test.ts +265 -0
  123. package/tests/registry/test-registry.test.ts +140 -0
  124. package/tests/registry/test-scanner.test.ts +257 -0
  125. package/tests/registry/test-schema-export.test.ts +224 -0
  126. package/tests/registry/test-validation.test.ts +75 -0
  127. package/tests/schema/test-loader.test.ts +97 -0
  128. package/tests/schema/test-ref-resolver.test.ts +105 -0
  129. package/tests/schema/test-strict.test.ts +139 -0
  130. package/tests/schema/test-validator.test.ts +64 -0
  131. package/tests/test-acl.test.ts +206 -0
  132. package/tests/test-bindings.test.ts +227 -0
  133. package/tests/test-config.test.ts +76 -0
  134. package/tests/test-context.test.ts +151 -0
  135. package/tests/test-decorator.test.ts +173 -0
  136. package/tests/test-errors.test.ts +204 -0
  137. package/tests/test-executor.test.ts +252 -0
  138. package/tests/test-middleware-manager.test.ts +185 -0
  139. package/tests/test-middleware.test.ts +86 -0
  140. package/tsconfig.build.json +8 -0
  141. package/tsconfig.json +20 -0
  142. package/vitest.config.ts +18 -0
@@ -0,0 +1,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