apcore-js 0.1.1 → 0.1.2

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.
@@ -63,6 +63,18 @@ export class Registry {
63
63
  }
64
64
 
65
65
  async discover(): Promise<number> {
66
+ const discovered = this._scanRoots();
67
+ this._applyIdMapOverrides(discovered);
68
+
69
+ const rawMetadata = this._loadAllMetadata(discovered);
70
+ const resolvedModules = await this._resolveAllEntryPoints(discovered, rawMetadata);
71
+ const validModules = this._validateAll(resolvedModules);
72
+ const loadOrder = this._resolveLoadOrder(validModules, rawMetadata);
73
+
74
+ return this._registerInOrder(loadOrder, validModules, rawMetadata);
75
+ }
76
+
77
+ private _scanRoots(): import('./types.js').DiscoveredModule[] {
66
78
  let maxDepth = 8;
67
79
  let followSymlinks = false;
68
80
  if (this._config !== null) {
@@ -70,96 +82,105 @@ export class Registry {
70
82
  followSymlinks = (this._config.get('extensions.follow_symlinks', false) as boolean);
71
83
  }
72
84
 
73
- // Step 1: Scan extension roots
74
85
  const hasNamespace = this._extensionRoots.some((r) => 'namespace' in r);
75
- let discovered;
76
86
  if (this._extensionRoots.length > 1 || hasNamespace) {
77
- discovered = scanMultiRoot(this._extensionRoots, maxDepth, followSymlinks);
78
- } else {
79
- const rootPath = this._extensionRoots[0]['root'] as string;
80
- discovered = scanExtensions(rootPath, maxDepth, followSymlinks);
87
+ return scanMultiRoot(this._extensionRoots, maxDepth, followSymlinks);
81
88
  }
89
+ return scanExtensions(this._extensionRoots[0]['root'] as string, maxDepth, followSymlinks);
90
+ }
82
91
 
83
- // Step 2: Apply ID Map overrides
84
- if (Object.keys(this._idMap).length > 0) {
85
- const resolvedRoots = this._extensionRoots.map((r) => resolve(r['root'] as string));
86
- for (const dm of discovered) {
87
- for (const root of resolvedRoots) {
88
- try {
89
- const relPath = dm.filePath.startsWith(root)
90
- ? dm.filePath.slice(root.length + 1)
91
- : null;
92
- if (relPath && relPath in this._idMap) {
93
- dm.canonicalId = this._idMap[relPath]['id'] as string;
94
- break;
95
- }
96
- } catch {
97
- continue;
92
+ private _applyIdMapOverrides(discovered: import('./types.js').DiscoveredModule[]): void {
93
+ if (Object.keys(this._idMap).length === 0) return;
94
+
95
+ const resolvedRoots = this._extensionRoots.map((r) => resolve(r['root'] as string));
96
+ for (const dm of discovered) {
97
+ for (const root of resolvedRoots) {
98
+ try {
99
+ const relPath = dm.filePath.startsWith(root)
100
+ ? dm.filePath.slice(root.length + 1)
101
+ : null;
102
+ if (relPath && relPath in this._idMap) {
103
+ dm.canonicalId = this._idMap[relPath]['id'] as string;
104
+ break;
98
105
  }
106
+ } catch (e) {
107
+ console.warn(`[apcore:registry] Failed to apply ID map for ${dm.canonicalId}:`, e);
108
+ continue;
99
109
  }
100
110
  }
101
111
  }
112
+ }
102
113
 
103
- // Step 3: Load metadata
114
+ private _loadAllMetadata(
115
+ discovered: import('./types.js').DiscoveredModule[],
116
+ ): Map<string, Record<string, unknown>> {
104
117
  const rawMetadata = new Map<string, Record<string, unknown>>();
105
118
  for (const dm of discovered) {
106
- rawMetadata.set(
107
- dm.canonicalId,
108
- dm.metaPath ? loadMetadata(dm.metaPath) : {},
109
- );
119
+ rawMetadata.set(dm.canonicalId, dm.metaPath ? loadMetadata(dm.metaPath) : {});
110
120
  }
121
+ return rawMetadata;
122
+ }
111
123
 
112
- // Step 4: Resolve entry points
124
+ private async _resolveAllEntryPoints(
125
+ discovered: import('./types.js').DiscoveredModule[],
126
+ rawMetadata: Map<string, Record<string, unknown>>,
127
+ ): Promise<Map<string, unknown>> {
113
128
  const resolvedModules = new Map<string, unknown>();
114
129
  for (const dm of discovered) {
115
130
  const meta = rawMetadata.get(dm.canonicalId) ?? {};
116
131
  try {
117
132
  const mod = await resolveEntryPoint(dm.filePath, meta);
118
133
  resolvedModules.set(dm.canonicalId, mod);
119
- } catch {
120
- continue;
134
+ } catch (e) {
135
+ console.warn(`[apcore:registry] Failed to resolve entry point for ${dm.canonicalId}:`, e);
121
136
  }
122
137
  }
138
+ return resolvedModules;
139
+ }
123
140
 
124
- // Step 5: Validate modules
141
+ private _validateAll(resolvedModules: Map<string, unknown>): Map<string, unknown> {
125
142
  const validModules = new Map<string, unknown>();
126
143
  for (const [modId, mod] of resolvedModules) {
127
- const errors = validateModule(mod);
128
- if (errors.length === 0) {
144
+ if (validateModule(mod).length === 0) {
129
145
  validModules.set(modId, mod);
130
146
  }
131
147
  }
148
+ return validModules;
149
+ }
132
150
 
133
- // Step 6: Collect dependencies
151
+ private _resolveLoadOrder(
152
+ validModules: Map<string, unknown>,
153
+ rawMetadata: Map<string, Record<string, unknown>>,
154
+ ): string[] {
134
155
  const modulesWithDeps: Array<[string, DependencyInfo[]]> = [];
135
156
  for (const modId of validModules.keys()) {
136
157
  const meta = rawMetadata.get(modId) ?? {};
137
158
  const depsRaw = (meta['dependencies'] as Array<Record<string, unknown>>) ?? [];
138
- const deps = depsRaw.length > 0 ? parseDependencies(depsRaw) : [];
139
- modulesWithDeps.push([modId, deps]);
159
+ modulesWithDeps.push([modId, depsRaw.length > 0 ? parseDependencies(depsRaw) : []]);
140
160
  }
141
-
142
- // Step 7: Resolve dependency order
143
161
  const knownIds = new Set(modulesWithDeps.map(([id]) => id));
144
- const loadOrder = resolveDependencies(modulesWithDeps, knownIds);
162
+ return resolveDependencies(modulesWithDeps, knownIds);
163
+ }
145
164
 
146
- // Step 8: Register in dependency order
147
- let registeredCount = 0;
165
+ private _registerInOrder(
166
+ loadOrder: string[],
167
+ validModules: Map<string, unknown>,
168
+ rawMetadata: Map<string, Record<string, unknown>>,
169
+ ): number {
170
+ let count = 0;
148
171
  for (const modId of loadOrder) {
149
172
  const mod = validModules.get(modId)!;
150
- const meta = rawMetadata.get(modId) ?? {};
151
-
152
173
  const modObj = mod as Record<string, unknown>;
153
- const mergedMeta = mergeModuleMetadata(modObj, meta);
174
+ const mergedMeta = mergeModuleMetadata(modObj, rawMetadata.get(modId) ?? {});
154
175
 
155
176
  this._modules.set(modId, mod);
156
177
  this._moduleMeta.set(modId, mergedMeta);
157
178
 
158
- // Call onLoad if available
159
179
  if (typeof modObj['onLoad'] === 'function') {
160
180
  try {
161
181
  (modObj['onLoad'] as () => void)();
162
- } catch {
182
+ } catch (e) {
183
+ console.warn(`[apcore:registry] onLoad failed for ${modId}, skipping:`, e);
163
184
  this._modules.delete(modId);
164
185
  this._moduleMeta.delete(modId);
165
186
  continue;
@@ -167,10 +188,9 @@ export class Registry {
167
188
  }
168
189
 
169
190
  this._triggerEvent('register', modId, mod);
170
- registeredCount++;
191
+ count++;
171
192
  }
172
-
173
- return registeredCount;
193
+ return count;
174
194
  }
175
195
 
176
196
  register(moduleId: string, module: unknown): void {
@@ -211,8 +231,8 @@ export class Registry {
211
231
  if (typeof modObj['onUnload'] === 'function') {
212
232
  try {
213
233
  (modObj['onUnload'] as () => void)();
214
- } catch {
215
- // Swallow
234
+ } catch (e) {
235
+ console.warn(`[apcore:registry] onUnload failed for ${moduleId}:`, e);
216
236
  }
217
237
  }
218
238
 
@@ -302,8 +322,8 @@ export class Registry {
302
322
  for (const cb of callbacks) {
303
323
  try {
304
324
  cb(moduleId, module);
305
- } catch {
306
- // Swallow callback errors
325
+ } catch (e) {
326
+ console.warn(`[apcore:registry] Event callback error for '${event}' on ${moduleId}:`, e);
307
327
  }
308
328
  }
309
329
  }
@@ -6,15 +6,12 @@ import type { TSchema } from '@sinclair/typebox';
6
6
  import json from 'js-yaml';
7
7
  import type { ModuleAnnotations, ModuleExample } from '../module.js';
8
8
  import { ModuleNotFoundError } from '../errors.js';
9
+ import { deepCopy } from '../utils/index.js';
9
10
  import { SchemaExporter } from '../schema/exporter.js';
10
11
  import { stripExtensions, toStrictSchema } from '../schema/strict.js';
11
12
  import { ExportProfile, type SchemaDefinition } from '../schema/types.js';
12
13
  import type { Registry } from './registry.js';
13
14
 
14
- function deepCopy<T>(obj: T): T {
15
- return JSON.parse(JSON.stringify(obj));
16
- }
17
-
18
15
  export function getSchema(registry: Registry, moduleId: string): Record<string, unknown> | null {
19
16
  const module = registry.get(moduleId);
20
17
  if (module === null) return null;
@@ -3,13 +3,10 @@
3
3
  */
4
4
 
5
5
  import type { ModuleAnnotations, ModuleExample } from '../module.js';
6
+ import { deepCopy } from '../utils/index.js';
6
7
  import { applyLlmDescriptions, stripExtensions, toStrictSchema } from './strict.js';
7
8
  import { ExportProfile, type SchemaDefinition } from './types.js';
8
9
 
9
- function deepCopy<T>(obj: T): T {
10
- return JSON.parse(JSON.stringify(obj));
11
- }
12
-
13
10
  export class SchemaExporter {
14
11
  export(
15
12
  schemaDef: SchemaDefinition,
@@ -191,80 +191,69 @@ export class SchemaLoader {
191
191
  export function jsonSchemaToTypeBox(schema: Record<string, unknown>): TSchema {
192
192
  const schemaType = schema['type'] as string | undefined;
193
193
 
194
- if (schemaType === 'object') {
195
- const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined;
196
- const required = new Set((schema['required'] as string[]) ?? []);
197
-
198
- if (properties) {
199
- const typeboxProps: Record<string, TSchema> = {};
200
- for (const [name, propSchema] of Object.entries(properties)) {
201
- const propType = jsonSchemaToTypeBox(propSchema);
202
- typeboxProps[name] = required.has(name) ? propType : Type.Optional(propType);
203
- }
204
- return Type.Object(typeboxProps);
205
- }
206
- return Type.Record(Type.String(), Type.Unknown());
207
- }
194
+ if (schemaType === 'object') return convertObjectSchema(schema);
195
+ if (schemaType === 'array') return convertArraySchema(schema);
196
+ if (schemaType === 'string') return convertStringSchema(schema);
197
+ if (schemaType === 'integer') return convertNumericSchema(schema, Type.Integer);
198
+ if (schemaType === 'number') return convertNumericSchema(schema, Type.Number);
199
+ if (schemaType === 'boolean') return Type.Boolean();
200
+ if (schemaType === 'null') return Type.Null();
208
201
 
209
- if (schemaType === 'array') {
210
- const items = schema['items'] as Record<string, unknown> | undefined;
211
- if (items) {
212
- return Type.Array(jsonSchemaToTypeBox(items));
202
+ return convertCombinatorSchema(schema);
203
+ }
204
+
205
+ function convertObjectSchema(schema: Record<string, unknown>): TSchema {
206
+ const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined;
207
+ const required = new Set((schema['required'] as string[]) ?? []);
208
+
209
+ if (properties) {
210
+ const typeboxProps: Record<string, TSchema> = {};
211
+ for (const [name, propSchema] of Object.entries(properties)) {
212
+ const propType = jsonSchemaToTypeBox(propSchema);
213
+ typeboxProps[name] = required.has(name) ? propType : Type.Optional(propType);
213
214
  }
214
- return Type.Array(Type.Unknown());
215
+ return Type.Object(typeboxProps);
215
216
  }
217
+ return Type.Record(Type.String(), Type.Unknown());
218
+ }
216
219
 
217
- if (schemaType === 'string') {
218
- const opts: Record<string, unknown> = {};
219
- if ('minLength' in schema) opts['minLength'] = schema['minLength'];
220
- if ('maxLength' in schema) opts['maxLength'] = schema['maxLength'];
221
- if ('pattern' in schema) opts['pattern'] = schema['pattern'];
222
- if ('format' in schema) opts['format'] = schema['format'];
223
- return Type.String(opts);
224
- }
220
+ function convertArraySchema(schema: Record<string, unknown>): TSchema {
221
+ const items = schema['items'] as Record<string, unknown> | undefined;
222
+ return items ? Type.Array(jsonSchemaToTypeBox(items)) : Type.Array(Type.Unknown());
223
+ }
225
224
 
226
- if (schemaType === 'integer') {
227
- const opts: Record<string, unknown> = {};
228
- if ('minimum' in schema) opts['minimum'] = schema['minimum'];
229
- if ('maximum' in schema) opts['maximum'] = schema['maximum'];
230
- if ('exclusiveMinimum' in schema) opts['exclusiveMinimum'] = schema['exclusiveMinimum'];
231
- if ('exclusiveMaximum' in schema) opts['exclusiveMaximum'] = schema['exclusiveMaximum'];
232
- if ('multipleOf' in schema) opts['multipleOf'] = schema['multipleOf'];
233
- return Type.Integer(opts);
225
+ function convertStringSchema(schema: Record<string, unknown>): TSchema {
226
+ const opts: Record<string, unknown> = {};
227
+ for (const key of ['minLength', 'maxLength', 'pattern', 'format']) {
228
+ if (key in schema) opts[key] = schema[key];
234
229
  }
230
+ return Type.String(opts);
231
+ }
235
232
 
236
- if (schemaType === 'number') {
237
- const opts: Record<string, unknown> = {};
238
- if ('minimum' in schema) opts['minimum'] = schema['minimum'];
239
- if ('maximum' in schema) opts['maximum'] = schema['maximum'];
240
- if ('exclusiveMinimum' in schema) opts['exclusiveMinimum'] = schema['exclusiveMinimum'];
241
- if ('exclusiveMaximum' in schema) opts['exclusiveMaximum'] = schema['exclusiveMaximum'];
242
- if ('multipleOf' in schema) opts['multipleOf'] = schema['multipleOf'];
243
- return Type.Number(opts);
233
+ function convertNumericSchema(
234
+ schema: Record<string, unknown>,
235
+ factory: (opts?: Record<string, unknown>) => TSchema,
236
+ ): TSchema {
237
+ const opts: Record<string, unknown> = {};
238
+ for (const key of ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf']) {
239
+ if (key in schema) opts[key] = schema[key];
244
240
  }
241
+ return factory(opts);
242
+ }
245
243
 
246
- if (schemaType === 'boolean') return Type.Boolean();
247
- if (schemaType === 'null') return Type.Null();
248
-
244
+ function convertCombinatorSchema(schema: Record<string, unknown>): TSchema {
249
245
  if ('enum' in schema) {
250
246
  const values = schema['enum'] as unknown[];
251
247
  return Type.Union(values.map((v) => Type.Literal(v as string | number | boolean)));
252
248
  }
253
-
254
249
  if ('oneOf' in schema) {
255
- const schemas = schema['oneOf'] as Record<string, unknown>[];
256
- return Type.Union(schemas.map((s) => jsonSchemaToTypeBox(s)));
250
+ return Type.Union((schema['oneOf'] as Record<string, unknown>[]).map(jsonSchemaToTypeBox));
257
251
  }
258
-
259
252
  if ('anyOf' in schema) {
260
- const schemas = schema['anyOf'] as Record<string, unknown>[];
261
- return Type.Union(schemas.map((s) => jsonSchemaToTypeBox(s)));
253
+ return Type.Union((schema['anyOf'] as Record<string, unknown>[]).map(jsonSchemaToTypeBox));
262
254
  }
263
-
264
255
  if ('allOf' in schema) {
265
- const schemas = schema['allOf'] as Record<string, unknown>[];
266
- return Type.Intersect(schemas.map((s) => jsonSchemaToTypeBox(s)));
256
+ return Type.Intersect((schema['allOf'] as Record<string, unknown>[]).map(jsonSchemaToTypeBox));
267
257
  }
268
-
269
258
  return Type.Unknown();
270
259
  }
@@ -6,13 +6,10 @@ import { readFileSync, existsSync } from 'node:fs';
6
6
  import { resolve, dirname, join } from 'node:path';
7
7
  import yaml from 'js-yaml';
8
8
  import { SchemaCircularRefError, SchemaNotFoundError, SchemaParseError } from '../errors.js';
9
+ import { deepCopy } from '../utils/index.js';
9
10
 
10
11
  const INLINE_SENTINEL = '__inline__';
11
12
 
12
- function deepCopy<T>(obj: T): T {
13
- return JSON.parse(JSON.stringify(obj));
14
- }
15
-
16
13
  export class RefResolver {
17
14
  private _schemasDir: string;
18
15
  private _maxDepth: number;
@@ -2,9 +2,7 @@
2
2
  * Strict mode conversion for JSON Schemas (Algorithm A23).
3
3
  */
4
4
 
5
- function deepCopy<T>(obj: T): T {
6
- return JSON.parse(JSON.stringify(obj));
7
- }
5
+ import { deepCopy } from '../utils/index.js';
8
6
 
9
7
  export function toStrictSchema(schema: Record<string, unknown>): Record<string, unknown> {
10
8
  const result = deepCopy(schema);
@@ -1 +1,5 @@
1
1
  export { matchPattern } from './pattern.js';
2
+
3
+ export function deepCopy<T>(obj: T): T {
4
+ return JSON.parse(JSON.stringify(obj));
5
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Tests for schema/annotations.ts — annotation conflict resolution.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { mergeAnnotations, mergeExamples, mergeMetadata } from '../../src/schema/annotations.js';
7
+ import { DEFAULT_ANNOTATIONS } from '../../src/module.js';
8
+ import type { ModuleAnnotations, ModuleExample } from '../../src/module.js';
9
+
10
+ describe('mergeAnnotations', () => {
11
+ it('returns defaults when both inputs are null', () => {
12
+ const result = mergeAnnotations(null, null);
13
+ expect(result).toEqual(DEFAULT_ANNOTATIONS);
14
+ });
15
+
16
+ it('returns defaults when both inputs are undefined', () => {
17
+ const result = mergeAnnotations(undefined, undefined);
18
+ expect(result).toEqual(DEFAULT_ANNOTATIONS);
19
+ });
20
+
21
+ it('applies code annotations over defaults', () => {
22
+ const codeAnnotations: ModuleAnnotations = {
23
+ readonly: true,
24
+ destructive: false,
25
+ idempotent: true,
26
+ requiresApproval: false,
27
+ openWorld: false,
28
+ };
29
+ const result = mergeAnnotations(null, codeAnnotations);
30
+ expect(result.readonly).toBe(true);
31
+ expect(result.idempotent).toBe(true);
32
+ expect(result.openWorld).toBe(false);
33
+ });
34
+
35
+ it('yaml annotations override code annotations', () => {
36
+ const codeAnnotations: ModuleAnnotations = {
37
+ readonly: true,
38
+ destructive: false,
39
+ idempotent: false,
40
+ requiresApproval: false,
41
+ openWorld: true,
42
+ };
43
+ const yamlAnnotations = { readonly: false, destructive: true };
44
+ const result = mergeAnnotations(yamlAnnotations, codeAnnotations);
45
+ expect(result.readonly).toBe(false);
46
+ expect(result.destructive).toBe(true);
47
+ expect(result.idempotent).toBe(false);
48
+ });
49
+
50
+ it('ignores unknown yaml keys', () => {
51
+ const yamlAnnotations = { unknownKey: 'value', readonly: true };
52
+ const result = mergeAnnotations(yamlAnnotations, null);
53
+ expect(result.readonly).toBe(true);
54
+ expect((result as unknown as Record<string, unknown>)['unknownKey']).toBeUndefined();
55
+ });
56
+ });
57
+
58
+ describe('mergeExamples', () => {
59
+ it('returns empty array when both inputs are null', () => {
60
+ expect(mergeExamples(null, null)).toEqual([]);
61
+ });
62
+
63
+ it('returns empty array when both inputs are undefined', () => {
64
+ expect(mergeExamples(undefined, undefined)).toEqual([]);
65
+ });
66
+
67
+ it('returns yaml examples when present', () => {
68
+ const yamlExamples = [
69
+ { title: 'Test', inputs: { a: 1 }, output: { b: 2 }, description: 'desc' },
70
+ ];
71
+ const result = mergeExamples(yamlExamples, null);
72
+ expect(result).toHaveLength(1);
73
+ expect(result[0].title).toBe('Test');
74
+ expect(result[0].inputs).toEqual({ a: 1 });
75
+ expect(result[0].output).toEqual({ b: 2 });
76
+ expect(result[0].description).toBe('desc');
77
+ });
78
+
79
+ it('uses code examples when yaml is null', () => {
80
+ const codeExamples: ModuleExample[] = [
81
+ { title: 'Code', inputs: { x: 1 }, output: { y: 2 } },
82
+ ];
83
+ const result = mergeExamples(null, codeExamples);
84
+ expect(result).toEqual(codeExamples);
85
+ });
86
+
87
+ it('yaml examples take precedence over code examples', () => {
88
+ const yamlExamples = [{ title: 'YAML', inputs: {}, output: {} }];
89
+ const codeExamples: ModuleExample[] = [
90
+ { title: 'Code', inputs: {}, output: {} },
91
+ ];
92
+ const result = mergeExamples(yamlExamples, codeExamples);
93
+ expect(result).toHaveLength(1);
94
+ expect(result[0].title).toBe('YAML');
95
+ });
96
+
97
+ it('handles yaml examples with missing optional fields', () => {
98
+ const yamlExamples = [{ title: 'Minimal' }];
99
+ const result = mergeExamples(yamlExamples as Array<Record<string, unknown>>, null);
100
+ expect(result[0].inputs).toEqual({});
101
+ expect(result[0].output).toEqual({});
102
+ expect(result[0].description).toBeUndefined();
103
+ });
104
+ });
105
+
106
+ describe('mergeMetadata', () => {
107
+ it('returns empty object when both inputs are null', () => {
108
+ expect(mergeMetadata(null, null)).toEqual({});
109
+ });
110
+
111
+ it('returns code metadata when yaml is null', () => {
112
+ const code = { key: 'value' };
113
+ expect(mergeMetadata(null, code)).toEqual({ key: 'value' });
114
+ });
115
+
116
+ it('returns yaml metadata when code is null', () => {
117
+ const yaml = { key: 'value' };
118
+ expect(mergeMetadata(yaml, null)).toEqual({ key: 'value' });
119
+ });
120
+
121
+ it('yaml overrides code on conflicting keys', () => {
122
+ const code = { a: 1, b: 2 };
123
+ const yaml = { b: 3, c: 4 };
124
+ const result = mergeMetadata(yaml, code);
125
+ expect(result).toEqual({ a: 1, b: 3, c: 4 });
126
+ });
127
+
128
+ it('does not mutate input objects', () => {
129
+ const code = { a: 1 };
130
+ const yaml = { b: 2 };
131
+ mergeMetadata(yaml, code);
132
+ expect(code).toEqual({ a: 1 });
133
+ expect(yaml).toEqual({ b: 2 });
134
+ });
135
+ });