apcore-js 0.1.2 → 0.2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2026-02-20
9
+
10
+ ### Added
11
+
12
+ - **Error classes and constants**
13
+ - `ModuleExecuteError` — New error class for module execution failures
14
+ - `InternalError` — New error class for general internal errors
15
+ - `ErrorCodes` — Frozen object with all 26 error code strings for consistent error code usage
16
+ - `ErrorCode` — Type definition for all error codes
17
+ - **Registry constants**
18
+ - `REGISTRY_EVENTS` — Frozen object with standard event names (`register`, `unregister`)
19
+ - `MODULE_ID_PATTERN` — Regex pattern enforcing lowercase/digits/underscores/dots for module IDs (no hyphens allowed to ensure bijective MCP tool name normalization)
20
+ - **Executor methods**
21
+ - `Executor.callAsync()` — Alias for `call()` for compatibility with MCP bridge packages
22
+
23
+ ### Changed
24
+
25
+ - **Module ID validation** — Registry now validates module IDs against `MODULE_ID_PATTERN` on registration, rejecting IDs with hyphens or invalid characters
26
+ - **Event handling** — Registry event validation now uses `REGISTRY_EVENTS` constants instead of hardcoded strings
27
+ - **Test updates** — Updated tests to use underscore-separated module IDs instead of hyphens (e.g., `math.add_ten` instead of `math.addTen`, `ctx_test` instead of `ctx-test`)
28
+
29
+ ### Fixed
30
+
31
+ - **String literals in Registry** — Replaced hardcoded `'register'` and `'unregister'` strings with `REGISTRY_EVENTS.REGISTER` and `REGISTRY_EVENTS.UNREGISTER` constants in event triggers for consistency
32
+
8
33
  ## [0.1.2] - 2026-02-18
9
34
 
10
35
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apcore-js",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "AI-Perceivable Core — schema-driven module development framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -3,7 +3,7 @@
3
3
  ## Overall Progress
4
4
 
5
5
  ```
6
- [████████████████████████████████████████] 42/42 tasks (100%)
6
+ [>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] 42/42 tasks (100%)
7
7
  ```
8
8
 
9
9
  | Status | Count |
package/src/errors.ts CHANGED
@@ -367,3 +367,58 @@ export class ModuleLoadError extends ModuleError {
367
367
  this.name = 'ModuleLoadError';
368
368
  }
369
369
  }
370
+
371
+ export class ModuleExecuteError extends ModuleError {
372
+ constructor(moduleId: string, reason: string, options?: { cause?: Error; traceId?: string }) {
373
+ super(
374
+ 'MODULE_EXECUTE_ERROR',
375
+ `Failed to execute module '${moduleId}': ${reason}`,
376
+ { moduleId, reason },
377
+ options?.cause,
378
+ options?.traceId,
379
+ );
380
+ this.name = 'ModuleExecuteError';
381
+ }
382
+ }
383
+
384
+ export class InternalError extends ModuleError {
385
+ constructor(message: string = 'Internal error', options?: { cause?: Error; traceId?: string }) {
386
+ super('GENERAL_INTERNAL_ERROR', message, {}, options?.cause, options?.traceId);
387
+ this.name = 'InternalError';
388
+ }
389
+ }
390
+
391
+ /**
392
+ * All framework error codes as constants.
393
+ * Use these instead of hardcoding error code strings.
394
+ */
395
+ export const ErrorCodes = Object.freeze({
396
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
397
+ CONFIG_INVALID: "CONFIG_INVALID",
398
+ ACL_RULE_ERROR: "ACL_RULE_ERROR",
399
+ ACL_DENIED: "ACL_DENIED",
400
+ MODULE_NOT_FOUND: "MODULE_NOT_FOUND",
401
+ MODULE_TIMEOUT: "MODULE_TIMEOUT",
402
+ MODULE_LOAD_ERROR: "MODULE_LOAD_ERROR",
403
+ MODULE_EXECUTE_ERROR: "MODULE_EXECUTE_ERROR",
404
+ SCHEMA_VALIDATION_ERROR: "SCHEMA_VALIDATION_ERROR",
405
+ SCHEMA_NOT_FOUND: "SCHEMA_NOT_FOUND",
406
+ SCHEMA_PARSE_ERROR: "SCHEMA_PARSE_ERROR",
407
+ SCHEMA_CIRCULAR_REF: "SCHEMA_CIRCULAR_REF",
408
+ CALL_DEPTH_EXCEEDED: "CALL_DEPTH_EXCEEDED",
409
+ CIRCULAR_CALL: "CIRCULAR_CALL",
410
+ CALL_FREQUENCY_EXCEEDED: "CALL_FREQUENCY_EXCEEDED",
411
+ GENERAL_INVALID_INPUT: "GENERAL_INVALID_INPUT",
412
+ GENERAL_INTERNAL_ERROR: "GENERAL_INTERNAL_ERROR",
413
+ FUNC_MISSING_TYPE_HINT: "FUNC_MISSING_TYPE_HINT",
414
+ FUNC_MISSING_RETURN_TYPE: "FUNC_MISSING_RETURN_TYPE",
415
+ BINDING_INVALID_TARGET: "BINDING_INVALID_TARGET",
416
+ BINDING_MODULE_NOT_FOUND: "BINDING_MODULE_NOT_FOUND",
417
+ BINDING_CALLABLE_NOT_FOUND: "BINDING_CALLABLE_NOT_FOUND",
418
+ BINDING_NOT_CALLABLE: "BINDING_NOT_CALLABLE",
419
+ BINDING_SCHEMA_MISSING: "BINDING_SCHEMA_MISSING",
420
+ BINDING_FILE_INVALID: "BINDING_FILE_INVALID",
421
+ CIRCULAR_DEPENDENCY: "CIRCULAR_DEPENDENCY",
422
+ } as const);
423
+
424
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
package/src/executor.ts CHANGED
@@ -168,6 +168,18 @@ export class Executor {
168
168
  return this._executeWithMiddleware(mod, moduleId, effectiveInputs, ctx);
169
169
  }
170
170
 
171
+ /**
172
+ * Alias for call(). Provided for compatibility with MCP bridge packages
173
+ * that may call callAsync() by convention.
174
+ */
175
+ async callAsync(
176
+ moduleId: string,
177
+ inputs?: Record<string, unknown> | null,
178
+ context?: Context | null,
179
+ ): Promise<Record<string, unknown>> {
180
+ return this.call(moduleId, inputs, context);
181
+ }
182
+
171
183
  private _createContext(moduleId: string, context?: Context | null): Context {
172
184
  if (context == null) {
173
185
  return Context.create(this).child(moduleId);
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // Core
6
6
  export { Context, createIdentity } from './context.js';
7
7
  export type { Identity } from './context.js';
8
- export { Registry } from './registry/registry.js';
8
+ export { Registry, REGISTRY_EVENTS, MODULE_ID_PATTERN } from './registry/registry.js';
9
9
  export { Executor, redactSensitive, REDACTED_VALUE } from './executor.js';
10
10
 
11
11
  // Module types
@@ -42,7 +42,11 @@ export {
42
42
  BindingFileInvalidError,
43
43
  CircularDependencyError,
44
44
  ModuleLoadError,
45
+ ModuleExecuteError,
46
+ InternalError,
47
+ ErrorCodes,
45
48
  } from './errors.js';
49
+ export type { ErrorCode } from './errors.js';
46
50
 
47
51
  // ACL
48
52
  export { ACL } from './acl.js';
@@ -13,6 +13,20 @@ import { scanExtensions, scanMultiRoot } from './scanner.js';
13
13
  import type { DependencyInfo, ModuleDescriptor } from './types.js';
14
14
  import { validateModule } from './validation.js';
15
15
 
16
+ /**
17
+ * Standard registry event names.
18
+ */
19
+ export const REGISTRY_EVENTS = Object.freeze({
20
+ REGISTER: "register",
21
+ UNREGISTER: "unregister",
22
+ } as const);
23
+
24
+ /**
25
+ * Valid module ID pattern. Only lowercase letters, digits, underscores, and dots.
26
+ * Hyphens are prohibited to ensure bijective MCP/OpenAI tool name normalization.
27
+ */
28
+ export const MODULE_ID_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/;
29
+
16
30
  type EventCallback = (moduleId: string, module: unknown) => void;
17
31
 
18
32
  export class Registry {
@@ -20,8 +34,8 @@ export class Registry {
20
34
  private _modules: Map<string, unknown> = new Map();
21
35
  private _moduleMeta: Map<string, Record<string, unknown>> = new Map();
22
36
  private _callbacks: Map<string, EventCallback[]> = new Map([
23
- ['register', []],
24
- ['unregister', []],
37
+ [REGISTRY_EVENTS.REGISTER, []],
38
+ [REGISTRY_EVENTS.UNREGISTER, []],
25
39
  ]);
26
40
  private _idMap: Record<string, Record<string, unknown>> = {};
27
41
  private _schemaCache: Map<string, Record<string, unknown>> = new Map();
@@ -187,15 +201,20 @@ export class Registry {
187
201
  }
188
202
  }
189
203
 
190
- this._triggerEvent('register', modId, mod);
204
+ this._triggerEvent(REGISTRY_EVENTS.REGISTER, modId, mod);
191
205
  count++;
192
206
  }
193
207
  return count;
194
208
  }
195
209
 
196
210
  register(moduleId: string, module: unknown): void {
197
- if (!moduleId) {
198
- throw new InvalidInputError('module_id must be a non-empty string');
211
+ if (!moduleId || typeof moduleId !== "string") {
212
+ throw new InvalidInputError("Module ID must be a non-empty string");
213
+ }
214
+ if (!MODULE_ID_PATTERN.test(moduleId)) {
215
+ throw new InvalidInputError(
216
+ `Invalid module ID: "${moduleId}". Must match pattern: ${MODULE_ID_PATTERN} (lowercase, digits, underscores, dots only; no hyphens)`,
217
+ );
199
218
  }
200
219
 
201
220
  if (this._modules.has(moduleId)) {
@@ -215,7 +234,7 @@ export class Registry {
215
234
  }
216
235
  }
217
236
 
218
- this._triggerEvent('register', moduleId, module);
237
+ this._triggerEvent(REGISTRY_EVENTS.REGISTER, moduleId, module);
219
238
  }
220
239
 
221
240
  unregister(moduleId: string): boolean {
@@ -236,7 +255,7 @@ export class Registry {
236
255
  }
237
256
  }
238
257
 
239
- this._triggerEvent('unregister', moduleId, module);
258
+ this._triggerEvent(REGISTRY_EVENTS.UNREGISTER, moduleId, module);
240
259
  return true;
241
260
  }
242
261
 
@@ -311,8 +330,11 @@ export class Registry {
311
330
  }
312
331
 
313
332
  on(event: string, callback: EventCallback): void {
314
- if (!this._callbacks.has(event)) {
315
- throw new InvalidInputError(`Invalid event: ${event}. Must be 'register' or 'unregister'`);
333
+ const validEvents = Object.values(REGISTRY_EVENTS) as string[];
334
+ if (!validEvents.includes(event)) {
335
+ throw new InvalidInputError(
336
+ `Invalid event: ${event}. Must be one of: ${validEvents.map((e) => `'${e}'`).join(', ')}`,
337
+ );
316
338
  }
317
339
  this._callbacks.get(event)!.push(callback);
318
340
  }
@@ -72,21 +72,21 @@ describe('E2E Flow', () => {
72
72
  });
73
73
  const modB = new FunctionModule({
74
74
  execute: (inputs) => ({ value: (inputs['x'] as number) + 10 }),
75
- moduleId: 'math.addTen',
75
+ moduleId: 'math.add_ten',
76
76
  inputSchema: Type.Object({ x: Type.Number() }),
77
77
  outputSchema: Type.Object({ value: Type.Number() }),
78
78
  description: 'Add ten',
79
79
  });
80
80
  registry.register('math.double', modA);
81
- registry.register('math.addTen', modB);
81
+ registry.register('math.add_ten', modB);
82
82
 
83
83
  const executor = new Executor({ registry });
84
- const ctx = Context.create(executor, createIdentity('test-user'));
84
+ const ctx = Context.create(executor, createIdentity('test_user'));
85
85
 
86
86
  const r1 = await executor.call('math.double', { x: 5 }, ctx);
87
87
  expect(r1['value']).toBe(10);
88
88
 
89
- const r2 = await executor.call('math.addTen', { x: r1['value'] as number }, ctx);
89
+ const r2 = await executor.call('math.add_ten', { x: r1['value'] as number }, ctx);
90
90
  expect(r2['value']).toBe(20);
91
91
  });
92
92
 
@@ -221,16 +221,16 @@ describe('Executor', () => {
221
221
  let capturedCtx: Context | null = null;
222
222
  const mod = new FunctionModule({
223
223
  execute: (_inputs, ctx) => { capturedCtx = ctx; return { ok: true }; },
224
- moduleId: 'ctx-test',
224
+ moduleId: 'ctx_test',
225
225
  inputSchema: Type.Object({}),
226
226
  outputSchema: Type.Object({ ok: Type.Boolean() }),
227
227
  description: 'Context capture',
228
228
  });
229
- registry.register('ctx-test', mod);
229
+ registry.register('ctx_test', mod);
230
230
 
231
231
  const executor = new Executor({ registry });
232
232
  const ctx = Context.create(executor, createIdentity('user1'));
233
- await executor.call('ctx-test', {}, ctx);
233
+ await executor.call('ctx_test', {}, ctx);
234
234
 
235
235
  expect(capturedCtx).not.toBeNull();
236
236
  expect(capturedCtx!.traceId).toBe(ctx.traceId);