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,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
|
+
}
|