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,179 @@
1
+ # Task: TracingMiddleware with Sampling Strategies
2
+
3
+ ## Goal
4
+
5
+ Implement `TracingMiddleware` that extends the `Middleware` base class to provide distributed tracing through the executor pipeline. The middleware uses a stack-based approach to manage nested spans via `context.data`, supports 4 sampling strategies (`full`, `proportional`, `error_first`, `off`), and delegates span export to a pluggable `SpanExporter`.
6
+
7
+ ## Files Involved
8
+
9
+ - `src/observability/tracing.ts` -- TracingMiddleware class
10
+ - `src/middleware/base.ts` -- Middleware base class (dependency)
11
+ - `src/context.ts` -- Context class with shared `data` record (dependency)
12
+ - `tests/observability/test-tracing.test.ts` -- Unit tests for TracingMiddleware
13
+
14
+ ## Steps (TDD)
15
+
16
+ ### 1. Write failing tests for TracingMiddleware
17
+
18
+ ```typescript
19
+ describe('TracingMiddleware', () => {
20
+ it('creates and exports spans on success', () => {
21
+ const exporter = new InMemoryExporter();
22
+ const mw = new TracingMiddleware(exporter);
23
+ const ctx = Context.create();
24
+
25
+ mw.before('mod.a', {}, ctx);
26
+ mw.after('mod.a', {}, { result: 'ok' }, ctx);
27
+
28
+ const spans = exporter.getSpans();
29
+ expect(spans).toHaveLength(1);
30
+ expect(spans[0].name).toBe('apcore.module.execute');
31
+ expect(spans[0].status).toBe('ok');
32
+ expect(spans[0].attributes['moduleId']).toBe('mod.a');
33
+ });
34
+
35
+ it('creates error spans', () => {
36
+ const exporter = new InMemoryExporter();
37
+ const mw = new TracingMiddleware(exporter);
38
+ const ctx = Context.create();
39
+
40
+ mw.before('mod.err', {}, ctx);
41
+ mw.onError('mod.err', {}, new Error('fail'), ctx);
42
+
43
+ const spans = exporter.getSpans();
44
+ expect(spans).toHaveLength(1);
45
+ expect(spans[0].status).toBe('error');
46
+ expect(spans[0].attributes['success']).toBe(false);
47
+ });
48
+
49
+ it('supports nested spans with parent chain', () => {
50
+ const exporter = new InMemoryExporter();
51
+ const mw = new TracingMiddleware(exporter);
52
+ const ctx = Context.create();
53
+
54
+ mw.before('mod.outer', {}, ctx);
55
+ mw.before('mod.inner', {}, ctx);
56
+ mw.after('mod.inner', {}, {}, ctx);
57
+ mw.after('mod.outer', {}, {}, ctx);
58
+
59
+ const spans = exporter.getSpans();
60
+ expect(spans).toHaveLength(2);
61
+ expect(spans[0].parentSpanId).toBe(spans[1].spanId);
62
+ });
63
+
64
+ it('off strategy does not export', () => {
65
+ const exporter = new InMemoryExporter();
66
+ const mw = new TracingMiddleware(exporter, 1.0, 'off');
67
+ const ctx = Context.create();
68
+
69
+ mw.before('mod.a', {}, ctx);
70
+ mw.after('mod.a', {}, {}, ctx);
71
+
72
+ expect(exporter.getSpans()).toHaveLength(0);
73
+ });
74
+
75
+ it('error_first exports errors even when not sampled', () => {
76
+ const exporter = new InMemoryExporter();
77
+ const mw = new TracingMiddleware(exporter, 0.0, 'error_first');
78
+ const ctx = Context.create();
79
+
80
+ mw.before('mod.a', {}, ctx);
81
+ mw.onError('mod.a', {}, new Error('fail'), ctx);
82
+
83
+ expect(exporter.getSpans()).toHaveLength(1);
84
+ });
85
+
86
+ it('throws on invalid sampling rate', () => {
87
+ const exporter = new InMemoryExporter();
88
+ expect(() => new TracingMiddleware(exporter, -0.1)).toThrow();
89
+ expect(() => new TracingMiddleware(exporter, 1.5)).toThrow();
90
+ });
91
+
92
+ it('throws on invalid sampling strategy', () => {
93
+ const exporter = new InMemoryExporter();
94
+ expect(() => new TracingMiddleware(exporter, 1.0, 'invalid')).toThrow();
95
+ });
96
+ });
97
+ ```
98
+
99
+ ### 2. Implement sampling strategy validation
100
+
101
+ ```typescript
102
+ const VALID_STRATEGIES = new Set(['full', 'proportional', 'error_first', 'off']);
103
+
104
+ export class TracingMiddleware extends Middleware {
105
+ constructor(
106
+ exporter: SpanExporter,
107
+ samplingRate: number = 1.0,
108
+ samplingStrategy: string = 'full',
109
+ ) {
110
+ super();
111
+ if (samplingRate < 0.0 || samplingRate > 1.0) {
112
+ throw new Error(`sampling_rate must be between 0.0 and 1.0, got ${samplingRate}`);
113
+ }
114
+ if (!VALID_STRATEGIES.has(samplingStrategy)) {
115
+ throw new Error(`sampling_strategy must be one of ${[...VALID_STRATEGIES].join(', ')}, got '${samplingStrategy}'`);
116
+ }
117
+ // ...
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### 3. Implement _shouldSample() with strategy logic
123
+
124
+ The sampling decision is computed once per context and cached at `context.data['_tracing_sampled']`:
125
+
126
+ - **full**: Always sample (`decision = true`)
127
+ - **off**: Never sample (`decision = false`)
128
+ - **proportional**: Sample with probability `samplingRate` (`Math.random() < samplingRate`)
129
+ - **error_first**: Same as proportional for the decision, but errors are always exported regardless
130
+
131
+ ### 4. Implement before() with stack-based span creation
132
+
133
+ ```typescript
134
+ override before(moduleId: string, _inputs: Record<string, unknown>, context: Context): null {
135
+ this._shouldSample(context);
136
+ const spansStack = (context.data['_tracing_spans'] as Span[]) ?? [];
137
+ context.data['_tracing_spans'] = spansStack;
138
+ const parentSpanId = spansStack.length > 0
139
+ ? spansStack[spansStack.length - 1].spanId
140
+ : null;
141
+
142
+ const span = createSpan({
143
+ traceId: context.traceId,
144
+ name: 'apcore.module.execute',
145
+ startTime: performance.now(),
146
+ parentSpanId,
147
+ attributes: { moduleId, method: 'execute', callerId: context.callerId },
148
+ });
149
+ spansStack.push(span);
150
+ return null;
151
+ }
152
+ ```
153
+
154
+ ### 5. Implement after() and onError() with span finalization
155
+
156
+ Both methods pop the top span from the stack, set `endTime`, compute `duration_ms`, set status, and conditionally export. `onError()` additionally checks `error_first` strategy to force export.
157
+
158
+ ### 6. Run tests and verify all pass
159
+
160
+ ## Acceptance Criteria
161
+
162
+ - [x] `TracingMiddleware` extends `Middleware` and accepts `exporter`, `samplingRate`, `samplingStrategy`
163
+ - [x] Constructor validates `samplingRate` in `[0.0, 1.0]` and `samplingStrategy` in valid set
164
+ - [x] 4 strategies implemented: `full` (always), `proportional` (random), `error_first` (random + always-export-errors), `off` (never)
165
+ - [x] Sampling decision computed once per context and cached at `_tracing_sampled`
166
+ - [x] Stack-based span management at `context.data['_tracing_spans']` handles nested calls
167
+ - [x] `parentSpanId` correctly chains from top-of-stack span
168
+ - [x] Span attributes include `moduleId`, `method`, `callerId`, `duration_ms`, `success`
169
+ - [x] Error spans include `error_code` from `error.code` or `error.constructor.name`
170
+ - [x] All tests pass with `vitest`
171
+
172
+ ## Dependencies
173
+
174
+ - **span-model** -- Requires `Span`, `createSpan()`, `SpanExporter`
175
+ - **exporters** -- Requires `InMemoryExporter` for testing
176
+
177
+ ## Estimated Time
178
+
179
+ 3 hours
@@ -0,0 +1,81 @@
1
+ # apcore-typescript - Implementation Overview
2
+
3
+ ## Overall Progress
4
+
5
+ ```
6
+ [████████████████████████████████████████] 42/42 tasks (100%)
7
+ ```
8
+
9
+ | Status | Count |
10
+ |--------|-------|
11
+ | Completed | 7 modules |
12
+ | In Progress | 0 modules |
13
+ | Pending | 0 modules |
14
+
15
+ ## Module Overview
16
+
17
+ | # | Module | Description | Status | Progress |
18
+ |---|--------|-------------|--------|----------|
19
+ | 1 | [core-executor](./core-executor/) | Central orchestration: 10-step execution pipeline with context, safety checks, ACL, and middleware chains | completed | 5/5 |
20
+ | 2 | [schema-system](./schema-system/) | Schema loading, $ref resolution, TypeBox model generation, validation, and LLM provider format export | completed | 7/7 |
21
+ | 3 | [registry-system](./registry-system/) | Module discovery, registration, and querying with 8-step pipeline and topological dependency sort | completed | 8/8 |
22
+ | 4 | [middleware-system](./middleware-system/) | Composable onion-model middleware with before/after/on_error phases | completed | 4/4 |
23
+ | 5 | [acl-system](./acl-system/) | Pattern-based ACL with first-match-wins evaluation and conditional rules | completed | 5/5 |
24
+ | 6 | [observability](./observability/) | Distributed tracing, metrics collection, and structured logging middleware | completed | 7/7 |
25
+ | 7 | [decorator-bindings](./decorator-bindings/) | module() factory for code-first and BindingLoader for YAML-driven module registration | completed | 6/6 |
26
+
27
+ ## Module Dependencies
28
+
29
+ ```mermaid
30
+ graph TD
31
+ CE[core-executor] --> SS[schema-system]
32
+ CE --> MS[middleware-system]
33
+ CE --> ACL[acl-system]
34
+ CE --> RS[registry-system]
35
+ RS --> SS
36
+ RS --> DB[decorator-bindings]
37
+ DB --> SS
38
+ OBS[observability] --> MS
39
+ OBS --> CE
40
+
41
+ style CE fill:#2d6,stroke:#333,color:#fff
42
+ style SS fill:#2d6,stroke:#333,color:#fff
43
+ style RS fill:#2d6,stroke:#333,color:#fff
44
+ style MS fill:#2d6,stroke:#333,color:#fff
45
+ style ACL fill:#2d6,stroke:#333,color:#fff
46
+ style OBS fill:#2d6,stroke:#333,color:#fff
47
+ style DB fill:#2d6,stroke:#333,color:#fff
48
+ ```
49
+
50
+ ## Recommended Implementation Order
51
+
52
+ ### Phase 1: Foundation (Why first: no dependencies on other modules)
53
+
54
+ | Module | Rationale |
55
+ |--------|-----------|
56
+ | [schema-system](./schema-system/) | Core type definitions and validation used by all other modules |
57
+ | [middleware-system](./middleware-system/) | Base middleware infrastructure needed by executor and observability |
58
+ | [acl-system](./acl-system/) | Standalone access control with no internal dependencies |
59
+
60
+ ### Phase 2: Core (Why next: depends only on Phase 1 modules)
61
+
62
+ | Module | Rationale |
63
+ |--------|-----------|
64
+ | [decorator-bindings](./decorator-bindings/) | Depends on schema-system for TypeBox schema generation |
65
+ | [registry-system](./registry-system/) | Depends on schema-system and decorator-bindings for module discovery |
66
+
67
+ ### Phase 3: Orchestration (Why next: integrates all previous modules)
68
+
69
+ | Module | Rationale |
70
+ |--------|-----------|
71
+ | [core-executor](./core-executor/) | Depends on schema-system, middleware-system, acl-system, and registry-system |
72
+
73
+ ### Phase 4: Cross-Cutting (Why last: enhances existing modules without changing behavior)
74
+
75
+ | Module | Rationale |
76
+ |--------|-----------|
77
+ | [observability](./observability/) | Depends on middleware-system and core-executor; adds tracing/metrics/logging |
78
+
79
+ ---
80
+
81
+ *Generated by [code-forge](https://github.com/tercel/code-forge) | Source: [planning/features/](../features/)*
@@ -0,0 +1,57 @@
1
+ # Feature: Registry System
2
+
3
+ ## Overview
4
+
5
+ The Registry System is the module discovery, loading, and querying backbone of apcore. It scans extension directories for `.ts`/`.js` files, loads companion YAML metadata, resolves module entry points via async `import()`, validates structural requirements (inputSchema, outputSchema, description, execute), resolves inter-module dependencies through Kahn's topological sort, and registers modules in dependency order. The `Registry` class exposes query methods (`get`, `has`, `list`, `iter`, `count`, `moduleIds`, `getDefinition`) and event callbacks for register/unregister lifecycle hooks. Schema export functions provide JSON and YAML serialization with optional strict-mode and LLM export profiles.
6
+
7
+ ## Scope
8
+
9
+ ### Included
10
+
11
+ - `ModuleDescriptor`, `DiscoveredModule`, and `DependencyInfo` interfaces for type-safe module representation
12
+ - `EventCallback` type for register/unregister event subscriptions
13
+ - `scanExtensions()` and `scanMultiRoot()` for recursive directory scanning with `.ts`/`.js` filtering, `.d.ts`/test file exclusion, case-collision detection, and configurable symlink following
14
+ - `loadMetadata()`, `mergeModuleMetadata()`, `loadIdMap()`, `parseDependencies()` for YAML metadata loading and code/YAML conflict resolution
15
+ - `resolveDependencies()` implementing Kahn's topological sort with cycle detection and extraction
16
+ - `resolveEntryPoint()` using async `import()` with default-export preference, named-export auto-inference, and metadata-driven entry point override
17
+ - `validateModule()` for duck-type structural validation of inputSchema, outputSchema, description, and execute
18
+ - `Registry` class with 8-step async `discover()` pipeline, manual `register()`/`unregister()`, query accessors, event system, and schema cache management
19
+ - `getSchema()`, `exportSchema()`, `getAllSchemas()`, `exportAllSchemas()` with strict-mode, compact-mode, and LLM export profile support
20
+
21
+ ### Excluded
22
+
23
+ - Schema system internals (consumed via TypeBox `TSchema` and `SchemaExporter`)
24
+ - ACL enforcement (consumed by `core-executor`)
25
+ - Middleware chains (consumed by `core-executor`)
26
+ - YAML schema file authoring and schema directory management
27
+
28
+ ## Technology Stack
29
+
30
+ - **TypeScript 5.5+** with strict mode
31
+ - **@sinclair/typebox >= 0.34.0** for schema representation (`TSchema`)
32
+ - **js-yaml** for YAML metadata and ID map parsing
33
+ - **Node.js >= 18.0.0** with ES Module support (`node:fs`, `node:path`, dynamic `import()`)
34
+ - **vitest** for unit and integration testing
35
+
36
+ ## Task Execution Order
37
+
38
+ | # | Task File | Description | Status |
39
+ |---|-----------|-------------|--------|
40
+ | 1 | [types](./tasks/types.md) | ModuleDescriptor, DiscoveredModule, DependencyInfo interfaces | completed |
41
+ | 2 | [scanner](./tasks/scanner.md) | scanExtensions(), scanMultiRoot() directory scanning | completed |
42
+ | 3 | [metadata](./tasks/metadata.md) | loadMetadata(), mergeModuleMetadata(), loadIdMap(), parseDependencies() | completed |
43
+ | 4 | [dependencies](./tasks/dependencies.md) | resolveDependencies() Kahn's topological sort with cycle detection | completed |
44
+ | 5 | [entry-point](./tasks/entry-point.md) | resolveEntryPoint() with async import() and auto-inference | completed |
45
+ | 6 | [validation](./tasks/validation.md) | validateModule() structural duck-type checks | completed |
46
+ | 7 | [registry-core](./tasks/registry-core.md) | Registry class with 8-step discover() and query methods | completed |
47
+ | 8 | [schema-export](./tasks/schema-export.md) | getSchema(), exportSchema(), getAllSchemas(), exportAllSchemas() | completed |
48
+
49
+ ## Progress
50
+
51
+ | Total | Completed | In Progress | Pending |
52
+ |-------|-----------|-------------|---------|
53
+ | 8 | 8 | 0 | 0 |
54
+
55
+ ## Reference Documents
56
+
57
+ - [Registry System Feature Specification](../../features/registry-system.md)
@@ -0,0 +1,114 @@
1
+ # Implementation Plan: Registry System
2
+
3
+ ## Goal
4
+
5
+ Implement the module discovery and registration backbone for apcore, providing recursive directory scanning for `.ts`/`.js` extension files, YAML metadata loading with code-level merging, dependency resolution via Kahn's topological sort, dynamic module loading via async `import()`, structural validation, and a central `Registry` class that orchestrates the full 8-step discovery pipeline and exposes query, event, and schema export APIs.
6
+
7
+ ## Architecture Design
8
+
9
+ ### Component Structure
10
+
11
+ - **Types** (`registry/types.ts`, ~30 lines) -- Core interfaces: `ModuleDescriptor` (full module definition with schemas, tags, annotations, examples), `DiscoveredModule` (file path, canonical ID, meta path, namespace), and `DependencyInfo` (module ID, version, optional flag). Imports `ModuleAnnotations` and `ModuleExample` from the shared `module.ts` types.
12
+
13
+ - **Scanner** (`registry/scanner.ts`, ~140 lines) -- `scanExtensions()` recursively walks a single directory tree collecting `.ts`/`.js` files, skipping `.d.ts`, `.test.ts`, `.test.js`, `.spec.ts`, `.spec.js`, dot-prefixed entries, underscore-prefixed entries, and `node_modules`/`__pycache__` directories. Builds canonical IDs from relative paths using dot notation. Detects case collisions. Supports configurable `maxDepth` and `followSymlinks`. `scanMultiRoot()` delegates to `scanExtensions()` per root, prepending namespace prefixes and enforcing namespace uniqueness. **Known bug**: symlink detection uses `statSync` instead of `lstatSync`, so `stat.isSymbolicLink()` always returns false after stat follows the link.
14
+
15
+ - **Metadata** (`registry/metadata.ts`, ~105 lines) -- `loadMetadata()` reads and parses a YAML `_meta.yaml` file into a record. `parseDependencies()` converts raw dependency arrays into typed `DependencyInfo[]`. `mergeModuleMetadata()` merges code-level properties with YAML overrides (YAML wins for description, name, tags, version, annotations, examples, documentation; metadata records are shallow-merged). `loadIdMap()` loads a YAML ID map file with a `mappings` list for canonical ID overrides.
16
+
17
+ - **Dependencies** (`registry/dependencies.ts`, ~100 lines) -- `resolveDependencies()` implements Kahn's topological sort. Builds an adjacency graph and in-degree map from module dependency lists. Processes zero-in-degree nodes in sorted order for deterministic output. Throws `ModuleLoadError` for missing required dependencies and `CircularDependencyError` with an extracted cycle path when not all modules can be ordered. `extractCycle()` traces the first reachable cycle from remaining nodes.
18
+
19
+ - **Entry Point** (`registry/entry-point.ts`, ~65 lines) -- `resolveEntryPoint()` uses async `import()` to dynamically load a `.ts`/`.js` file, then resolves the module object. Priority: (1) metadata `entry_point` class name override, (2) default export if it passes `isModuleClass()`, (3) single named export passing `isModuleClass()`. Throws `ModuleLoadError` on import failure, missing class, no candidates, or ambiguous multiple candidates. `snakeToPascal()` utility converts snake_case to PascalCase. `isModuleClass()` duck-types by checking for inputSchema (object), outputSchema (object), description (string), and execute (function).
20
+
21
+ - **Validation** (`registry/validation.ts`, ~40 lines) -- `validateModule()` performs duck-type structural validation. Checks for: inputSchema (must be a non-null object, checked on instance and constructor), outputSchema (same), description (non-empty string), and execute (function). Returns an array of error strings; empty array means valid.
22
+
23
+ - **Registry** (`registry/registry.ts`, ~315 lines) -- Central `Registry` class. Constructor accepts optional `config`, `extensionsDir`, `extensionsDirs` (mutually exclusive), and `idMapPath`. Maintains `_modules` (Map of module ID to module object), `_moduleMeta` (Map of merged metadata), `_callbacks` (Map of event name to callback arrays), `_idMap` (canonical ID overrides), and `_schemaCache`. Exposes: `discover()` (async, returns count), `register()`, `unregister()`, `get()`, `has()`, `list()` (with tag/prefix filtering), `iter()`, `count` (getter), `moduleIds` (getter), `getDefinition()`, `on()`, `clearCache()`. Lifecycle hooks: calls `onLoad()` during registration and `onUnload()` during unregistration.
24
+
25
+ - **Schema Export** (`registry/schema-export.ts`, ~180 lines) -- `getSchema()` extracts a schema record from a registered module. `exportSchema()` serializes with optional strict mode (`toStrictSchema()`), compact mode (truncated descriptions, stripped extensions), or LLM export profile (MCP, OpenAI, Anthropic, Generic). `getAllSchemas()` and `exportAllSchemas()` operate on all registered modules. `serialize()` outputs JSON or YAML format.
26
+
27
+ ### Data Flow
28
+
29
+ The 8-step `discover()` pipeline processes modules in this order:
30
+
31
+ 1. **Scan Extension Roots** -- `scanExtensions()` or `scanMultiRoot()` depending on root configuration, producing `DiscoveredModule[]` with file paths, canonical IDs, and meta paths
32
+ 2. **Apply ID Map Overrides** -- If an ID map was loaded, override canonical IDs for matching relative file paths
33
+ 3. **Load Metadata** -- For each discovered module with a `_meta.yaml` companion, `loadMetadata()` parses the YAML into a metadata record
34
+ 4. **Resolve Entry Points** -- `resolveEntryPoint()` uses async `import()` to load each file and resolve the module object (default export, named export, or metadata-specified class)
35
+ 5. **Validate Modules** -- `validateModule()` checks structural requirements; invalid modules are silently dropped
36
+ 6. **Collect Dependencies** -- `parseDependencies()` extracts `DependencyInfo[]` from each module's metadata
37
+ 7. **Resolve Dependency Order** -- `resolveDependencies()` applies Kahn's topological sort, detecting cycles and missing required dependencies
38
+ 8. **Register in Order** -- Modules are registered in dependency order, merged metadata is stored, `onLoad()` is called, and register event callbacks fire
39
+
40
+ ### Technical Choices and Rationale
41
+
42
+ - **Async `import()` vs Python's `importlib`**: TypeScript/Node.js uses native ES module dynamic `import()` which is inherently async, unlike Python's synchronous `importlib.import_module()`. This makes `discover()` an async method returning `Promise<number>`, whereas the Python equivalent is synchronous.
43
+ - **No thread locking**: Node.js runs on a single-threaded event loop, eliminating the need for Python's `threading.Lock()` around registry mutations. The `_modules` Map is safely accessed without synchronization.
44
+ - **`statSync` instead of `lstatSync`**: The scanner uses `statSync` which follows symlinks before checking `isSymbolicLink()`, making the symlink check always return false. This is a known bug carried from the initial implementation. The fix would be to use `lstatSync` for the symlink check.
45
+ - **TypeBox schemas as JSON Schema**: Module `inputSchema` and `outputSchema` are TypeBox `TSchema` objects, which are already valid JSON Schema. No conversion step is needed for schema export, unlike Python's Pydantic models which require `.model_json_schema()`.
46
+ - **Deterministic sort in Kahn's algorithm**: Zero-in-degree nodes and dependents are processed in sorted order to ensure deterministic load ordering across runs.
47
+ - **Duck-type validation**: Instead of Python's class hierarchy checks (`issubclass`), TypeScript validation uses duck typing -- checking for the presence and types of `inputSchema`, `outputSchema`, `description`, and `execute` properties.
48
+
49
+ ## Task Breakdown
50
+
51
+ ```mermaid
52
+ graph TD
53
+ T1[types] --> T2[scanner]
54
+ T1 --> T3[metadata]
55
+ T1 --> T4[dependencies]
56
+ T1 --> T5[entry-point]
57
+ T1 --> T6[validation]
58
+ T2 --> T7[registry-core]
59
+ T3 --> T7
60
+ T4 --> T7
61
+ T5 --> T7
62
+ T6 --> T7
63
+ T7 --> T8[schema-export]
64
+ ```
65
+
66
+ | Task ID | Title | Estimated Time | Dependencies |
67
+ |---------|-------|---------------|--------------|
68
+ | types | ModuleDescriptor, DiscoveredModule, DependencyInfo interfaces | 1h | none |
69
+ | scanner | scanExtensions(), scanMultiRoot() directory scanning | 3h | types |
70
+ | metadata | loadMetadata(), mergeModuleMetadata(), loadIdMap(), parseDependencies() | 2h | types |
71
+ | dependencies | resolveDependencies() Kahn's topological sort | 3h | types |
72
+ | entry-point | resolveEntryPoint() with async import() | 2h | types |
73
+ | validation | validateModule() structural checks | 1h | types |
74
+ | registry-core | Registry class with 8-step discover() and query methods | 5h | scanner, metadata, dependencies, entry-point, validation |
75
+ | schema-export | getSchema(), exportSchema(), getAllSchemas(), exportAllSchemas() | 3h | registry-core |
76
+
77
+ ## Risks and Considerations
78
+
79
+ - **Symlink detection bug**: `statSync` follows symlinks before `isSymbolicLink()` is called, so the symlink guard in the scanner never triggers. Symlink cycles could cause infinite recursion up to `maxDepth`. The fix is to use `lstatSync` for the initial stat call.
80
+ - **Silent module drop on import failure**: If `resolveEntryPoint()` throws during `discover()`, the module is silently skipped with `continue`. This could hide genuine configuration or code errors. Consider adding a logging callback or accumulating warnings.
81
+ - **Case-insensitive filesystem collisions**: The scanner detects case collisions in canonical IDs but does not prevent registration. On case-insensitive filesystems (macOS default), this could lead to unexpected behavior.
82
+ - **`onLoad` failure during discover**: If a module's `onLoad()` throws during step 8, the module is removed from the registry but no error is propagated. This is intentional for resilience but could mask setup failures.
83
+ - **Schema cache invalidation**: `clearCache()` only clears the schema cache; it does not affect `_modules` or `_moduleMeta`. Re-running `discover()` will add modules but not remove previously registered ones.
84
+
85
+ ## Acceptance Criteria
86
+
87
+ - [x] `ModuleDescriptor`, `DiscoveredModule`, `DependencyInfo` interfaces compile with strict TypeScript
88
+ - [x] `scanExtensions()` discovers `.ts`/`.js` files, skips `.d.ts`, test files, dot/underscore prefixed, and `node_modules`
89
+ - [x] `scanMultiRoot()` enforces unique namespaces and prepends namespace to canonical IDs
90
+ - [x] `loadMetadata()` parses YAML `_meta.yaml` files; returns `{}` for missing files
91
+ - [x] `mergeModuleMetadata()` merges code and YAML metadata with YAML taking precedence
92
+ - [x] `loadIdMap()` loads YAML ID map with `mappings` list; throws on invalid format
93
+ - [x] `parseDependencies()` converts raw dependency arrays to typed `DependencyInfo[]`
94
+ - [x] `resolveDependencies()` produces correct topological order and detects cycles
95
+ - [x] `resolveEntryPoint()` resolves default exports, single named exports, and metadata-specified classes
96
+ - [x] `validateModule()` returns empty array for valid modules and descriptive errors for invalid ones
97
+ - [x] `Registry.discover()` executes all 8 pipeline steps and returns registered count
98
+ - [x] `Registry.register()`/`unregister()` manage modules with lifecycle hooks and event callbacks
99
+ - [x] `Registry.get()`, `has()`, `list()`, `iter()`, `count`, `moduleIds` provide correct query results
100
+ - [x] `getSchema()`/`exportSchema()` produce correct JSON/YAML with strict and compact modes
101
+ - [x] `getAllSchemas()`/`exportAllSchemas()` operate across all registered modules
102
+ - [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
103
+
104
+ ## References
105
+
106
+ - `src/registry/types.ts` -- Core type interfaces
107
+ - `src/registry/scanner.ts` -- Directory scanning
108
+ - `src/registry/metadata.ts` -- Metadata loading and merging
109
+ - `src/registry/dependencies.ts` -- Dependency resolution
110
+ - `src/registry/entry-point.ts` -- Dynamic module loading
111
+ - `src/registry/validation.ts` -- Module structural validation
112
+ - `src/registry/registry.ts` -- Central Registry class
113
+ - `src/registry/schema-export.ts` -- Schema query and export
114
+ - `src/registry/index.ts` -- Public API re-exports
@@ -0,0 +1,109 @@
1
+ {
2
+ "feature": "registry-system",
3
+ "created": "2026-02-16T00:00:00Z",
4
+ "updated": "2026-02-16T00:00:00Z",
5
+ "status": "completed",
6
+ "execution_order": [
7
+ "types",
8
+ "scanner",
9
+ "metadata",
10
+ "dependencies",
11
+ "entry-point",
12
+ "validation",
13
+ "registry-core",
14
+ "schema-export"
15
+ ],
16
+ "progress": {
17
+ "total_tasks": 8,
18
+ "completed": 8,
19
+ "in_progress": 0,
20
+ "pending": 0
21
+ },
22
+ "tasks": [
23
+ {
24
+ "id": "types",
25
+ "file": "tasks/types.md",
26
+ "title": "ModuleDescriptor, DiscoveredModule, DependencyInfo Interfaces",
27
+ "status": "completed",
28
+ "started_at": "2026-02-16T08:00:00Z",
29
+ "completed_at": "2026-02-16T09:00:00Z",
30
+ "assignee": null,
31
+ "commits": []
32
+ },
33
+ {
34
+ "id": "scanner",
35
+ "file": "tasks/scanner.md",
36
+ "title": "Directory Scanning with scanExtensions() and scanMultiRoot()",
37
+ "status": "completed",
38
+ "started_at": "2026-02-16T09:00:00Z",
39
+ "completed_at": "2026-02-16T12:00:00Z",
40
+ "assignee": null,
41
+ "commits": []
42
+ },
43
+ {
44
+ "id": "metadata",
45
+ "file": "tasks/metadata.md",
46
+ "title": "Metadata Loading, Merging, ID Map, and Dependency Parsing",
47
+ "status": "completed",
48
+ "started_at": "2026-02-16T12:00:00Z",
49
+ "completed_at": "2026-02-16T14:00:00Z",
50
+ "assignee": null,
51
+ "commits": []
52
+ },
53
+ {
54
+ "id": "dependencies",
55
+ "file": "tasks/dependencies.md",
56
+ "title": "Kahn's Topological Sort with Cycle Detection",
57
+ "status": "completed",
58
+ "started_at": "2026-02-16T14:00:00Z",
59
+ "completed_at": "2026-02-16T17:00:00Z",
60
+ "assignee": null,
61
+ "commits": []
62
+ },
63
+ {
64
+ "id": "entry-point",
65
+ "file": "tasks/entry-point.md",
66
+ "title": "Dynamic Module Loading via async import()",
67
+ "status": "completed",
68
+ "started_at": "2026-02-16T17:00:00Z",
69
+ "completed_at": "2026-02-16T19:00:00Z",
70
+ "assignee": null,
71
+ "commits": []
72
+ },
73
+ {
74
+ "id": "validation",
75
+ "file": "tasks/validation.md",
76
+ "title": "Module Structural Validation",
77
+ "status": "completed",
78
+ "started_at": "2026-02-16T19:00:00Z",
79
+ "completed_at": "2026-02-16T20:00:00Z",
80
+ "assignee": null,
81
+ "commits": []
82
+ },
83
+ {
84
+ "id": "registry-core",
85
+ "file": "tasks/registry-core.md",
86
+ "title": "Registry Class with 8-Step Discover Pipeline",
87
+ "status": "completed",
88
+ "started_at": "2026-02-16T20:00:00Z",
89
+ "completed_at": "2026-02-17T01:00:00Z",
90
+ "assignee": null,
91
+ "commits": []
92
+ },
93
+ {
94
+ "id": "schema-export",
95
+ "file": "tasks/schema-export.md",
96
+ "title": "Schema Query and Export Functions",
97
+ "status": "completed",
98
+ "started_at": "2026-02-17T01:00:00Z",
99
+ "completed_at": "2026-02-17T04:00:00Z",
100
+ "assignee": null,
101
+ "commits": []
102
+ }
103
+ ],
104
+ "metadata": {
105
+ "source_doc": "planning/features/registry-system.md",
106
+ "created_by": "code-forge",
107
+ "version": "1.0"
108
+ }
109
+ }