apcore-js 0.17.1 → 0.19.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 (181) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +14 -10
  3. package/dist/acl-handlers.d.ts +14 -0
  4. package/dist/acl-handlers.d.ts.map +1 -1
  5. package/dist/acl-handlers.js +37 -4
  6. package/dist/acl-handlers.js.map +1 -1
  7. package/dist/acl.d.ts +4 -3
  8. package/dist/acl.d.ts.map +1 -1
  9. package/dist/acl.js +44 -40
  10. package/dist/acl.js.map +1 -1
  11. package/dist/async-task.d.ts +6 -5
  12. package/dist/async-task.d.ts.map +1 -1
  13. package/dist/async-task.js +10 -6
  14. package/dist/async-task.js.map +1 -1
  15. package/dist/bindings.d.ts.map +1 -1
  16. package/dist/bindings.js +113 -11
  17. package/dist/bindings.js.map +1 -1
  18. package/dist/builtin-steps.d.ts +19 -5
  19. package/dist/builtin-steps.d.ts.map +1 -1
  20. package/dist/builtin-steps.js +83 -27
  21. package/dist/builtin-steps.js.map +1 -1
  22. package/dist/client.d.ts +2 -1
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +8 -6
  25. package/dist/client.js.map +1 -1
  26. package/dist/config.d.ts +8 -2
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +44 -6
  29. package/dist/config.js.map +1 -1
  30. package/dist/context.d.ts +34 -7
  31. package/dist/context.d.ts.map +1 -1
  32. package/dist/context.js +97 -40
  33. package/dist/context.js.map +1 -1
  34. package/dist/decorator.d.ts +3 -0
  35. package/dist/decorator.d.ts.map +1 -1
  36. package/dist/decorator.js +17 -1
  37. package/dist/decorator.js.map +1 -1
  38. package/dist/errors.d.ts +65 -16
  39. package/dist/errors.d.ts.map +1 -1
  40. package/dist/errors.js +191 -82
  41. package/dist/errors.js.map +1 -1
  42. package/dist/events/emitter.d.ts +4 -1
  43. package/dist/events/emitter.d.ts.map +1 -1
  44. package/dist/events/emitter.js +26 -16
  45. package/dist/events/emitter.js.map +1 -1
  46. package/dist/executor.d.ts +9 -9
  47. package/dist/executor.d.ts.map +1 -1
  48. package/dist/executor.js +62 -38
  49. package/dist/executor.js.map +1 -1
  50. package/dist/generated/version.d.ts +1 -1
  51. package/dist/generated/version.js +1 -1
  52. package/dist/index.d.ts +21 -7
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +21 -7
  55. package/dist/index.js.map +1 -1
  56. package/dist/middleware/index.d.ts +1 -1
  57. package/dist/middleware/index.d.ts.map +1 -1
  58. package/dist/middleware/index.js +1 -1
  59. package/dist/middleware/index.js.map +1 -1
  60. package/dist/middleware/logging.d.ts +6 -0
  61. package/dist/middleware/logging.d.ts.map +1 -1
  62. package/dist/middleware/logging.js +13 -3
  63. package/dist/middleware/logging.js.map +1 -1
  64. package/dist/middleware/manager.d.ts.map +1 -1
  65. package/dist/middleware/manager.js +1 -0
  66. package/dist/middleware/manager.js.map +1 -1
  67. package/dist/middleware/platform-notify.d.ts +1 -1
  68. package/dist/middleware/platform-notify.d.ts.map +1 -1
  69. package/dist/middleware/platform-notify.js +5 -3
  70. package/dist/middleware/platform-notify.js.map +1 -1
  71. package/dist/middleware/retry.d.ts +16 -7
  72. package/dist/middleware/retry.d.ts.map +1 -1
  73. package/dist/middleware/retry.js +21 -15
  74. package/dist/middleware/retry.js.map +1 -1
  75. package/dist/module.d.ts +8 -2
  76. package/dist/module.d.ts.map +1 -1
  77. package/dist/module.js +10 -3
  78. package/dist/module.js.map +1 -1
  79. package/dist/observability/context-logger.d.ts.map +1 -1
  80. package/dist/observability/context-logger.js +18 -5
  81. package/dist/observability/context-logger.js.map +1 -1
  82. package/dist/observability/metrics-utils.d.ts.map +1 -1
  83. package/dist/observability/metrics-utils.js +3 -5
  84. package/dist/observability/metrics-utils.js.map +1 -1
  85. package/dist/observability/metrics.d.ts +1 -0
  86. package/dist/observability/metrics.d.ts.map +1 -1
  87. package/dist/observability/metrics.js +14 -1
  88. package/dist/observability/metrics.js.map +1 -1
  89. package/dist/observability/tracing.d.ts +2 -0
  90. package/dist/observability/tracing.d.ts.map +1 -1
  91. package/dist/observability/tracing.js +12 -2
  92. package/dist/observability/tracing.js.map +1 -1
  93. package/dist/observability/usage.d.ts.map +1 -1
  94. package/dist/observability/usage.js +10 -1
  95. package/dist/observability/usage.js.map +1 -1
  96. package/dist/pipeline-config.d.ts +13 -9
  97. package/dist/pipeline-config.d.ts.map +1 -1
  98. package/dist/pipeline-config.js +77 -13
  99. package/dist/pipeline-config.js.map +1 -1
  100. package/dist/registry/conflicts.d.ts +29 -0
  101. package/dist/registry/conflicts.d.ts.map +1 -0
  102. package/dist/registry/conflicts.js +61 -0
  103. package/dist/registry/conflicts.js.map +1 -0
  104. package/dist/registry/dependencies.d.ts +1 -1
  105. package/dist/registry/dependencies.d.ts.map +1 -1
  106. package/dist/registry/dependencies.js +69 -20
  107. package/dist/registry/dependencies.js.map +1 -1
  108. package/dist/registry/entry-point.d.ts.map +1 -1
  109. package/dist/registry/entry-point.js.map +1 -1
  110. package/dist/registry/index.d.ts +5 -2
  111. package/dist/registry/index.d.ts.map +1 -1
  112. package/dist/registry/index.js +4 -2
  113. package/dist/registry/index.js.map +1 -1
  114. package/dist/registry/metadata.d.ts.map +1 -1
  115. package/dist/registry/metadata.js +24 -3
  116. package/dist/registry/metadata.js.map +1 -1
  117. package/dist/registry/registry.d.ts +40 -4
  118. package/dist/registry/registry.d.ts.map +1 -1
  119. package/dist/registry/registry.js +222 -53
  120. package/dist/registry/registry.js.map +1 -1
  121. package/dist/registry/scanner.d.ts.map +1 -1
  122. package/dist/registry/scanner.js +6 -0
  123. package/dist/registry/scanner.js.map +1 -1
  124. package/dist/registry/version.d.ts +50 -0
  125. package/dist/registry/version.d.ts.map +1 -0
  126. package/dist/registry/version.js +198 -0
  127. package/dist/registry/version.js.map +1 -0
  128. package/dist/schema/exporter.js +2 -2
  129. package/dist/schema/extractor.d.ts +69 -0
  130. package/dist/schema/extractor.d.ts.map +1 -0
  131. package/dist/schema/extractor.js +142 -0
  132. package/dist/schema/extractor.js.map +1 -0
  133. package/dist/schema/index.d.ts +2 -0
  134. package/dist/schema/index.d.ts.map +1 -1
  135. package/dist/schema/index.js +1 -0
  136. package/dist/schema/index.js.map +1 -1
  137. package/dist/schema/loader.js +7 -7
  138. package/dist/schema/loader.js.map +1 -1
  139. package/dist/schema/ref-resolver.d.ts.map +1 -1
  140. package/dist/schema/ref-resolver.js +10 -1
  141. package/dist/schema/ref-resolver.js.map +1 -1
  142. package/dist/schema/types.d.ts +6 -6
  143. package/dist/schema/types.d.ts.map +1 -1
  144. package/dist/schema/types.js +6 -6
  145. package/dist/schema/types.js.map +1 -1
  146. package/dist/sys-modules/control.d.ts +79 -1
  147. package/dist/sys-modules/control.d.ts.map +1 -1
  148. package/dist/sys-modules/control.js +44 -11
  149. package/dist/sys-modules/control.js.map +1 -1
  150. package/dist/sys-modules/health.d.ts +89 -1
  151. package/dist/sys-modules/health.d.ts.map +1 -1
  152. package/dist/sys-modules/health.js +41 -1
  153. package/dist/sys-modules/health.js.map +1 -1
  154. package/dist/sys-modules/index.d.ts +5 -5
  155. package/dist/sys-modules/index.d.ts.map +1 -1
  156. package/dist/sys-modules/index.js +5 -5
  157. package/dist/sys-modules/index.js.map +1 -1
  158. package/dist/sys-modules/manifest.d.ts +98 -1
  159. package/dist/sys-modules/manifest.d.ts.map +1 -1
  160. package/dist/sys-modules/manifest.js +43 -1
  161. package/dist/sys-modules/manifest.js.map +1 -1
  162. package/dist/sys-modules/registration.d.ts +12 -0
  163. package/dist/sys-modules/registration.d.ts.map +1 -1
  164. package/dist/sys-modules/registration.js +20 -8
  165. package/dist/sys-modules/registration.js.map +1 -1
  166. package/dist/sys-modules/toggle.d.ts +39 -2
  167. package/dist/sys-modules/toggle.d.ts.map +1 -1
  168. package/dist/sys-modules/toggle.js +23 -6
  169. package/dist/sys-modules/toggle.js.map +1 -1
  170. package/dist/sys-modules/usage.d.ts +92 -1
  171. package/dist/sys-modules/usage.d.ts.map +1 -1
  172. package/dist/sys-modules/usage.js +42 -1
  173. package/dist/sys-modules/usage.js.map +1 -1
  174. package/dist/trace-context.d.ts +3 -4
  175. package/dist/trace-context.d.ts.map +1 -1
  176. package/dist/trace-context.js +4 -5
  177. package/dist/trace-context.js.map +1 -1
  178. package/dist/utils/index.d.ts.map +1 -1
  179. package/dist/utils/index.js +2 -1
  180. package/dist/utils/index.js.map +1 -1
  181. package/package.json +9 -5
@@ -17,10 +17,15 @@ export declare const REGISTRY_EVENTS: Readonly<{
17
17
  export declare const MODULE_ID_PATTERN: RegExp;
18
18
  /**
19
19
  * Maximum allowed length for a module ID.
20
+ *
21
+ * Per PROTOCOL_SPEC §2.7 EBNF constraint #1. 192 is filesystem-safe
22
+ * (192 + ".binding.yaml".length = 205 < 255-byte filename limit on
23
+ * ext4/xfs/NTFS/APFS/btrfs) and accommodates Java/.NET deep-namespace
24
+ * FQN-derived IDs. Bumped from 128 in spec 1.6.0-draft (2026-04-08).
20
25
  */
21
- export declare const MAX_MODULE_ID_LENGTH = 128;
26
+ export declare const MAX_MODULE_ID_LENGTH = 192;
22
27
  /**
23
- * Reserved words that cannot appear as any segment of a module ID.
28
+ * Reserved words that cannot appear as the first segment of a module ID.
24
29
  */
25
30
  export declare const RESERVED_WORDS: Set<string>;
26
31
  /**
@@ -48,6 +53,7 @@ export declare class Registry {
48
53
  private _moduleMeta;
49
54
  private _callbacks;
50
55
  private _idMap;
56
+ private _lowercaseMap;
51
57
  private _schemaCache;
52
58
  private _config;
53
59
  private _watchers?;
@@ -79,7 +85,16 @@ export declare class Registry {
79
85
  private _validateAll;
80
86
  private _resolveLoadOrder;
81
87
  private _registerInOrder;
88
+ /**
89
+ * Register a module.
90
+ *
91
+ * Validation order (PROTOCOL_SPEC §2.7, aligned with apcore-python and
92
+ * apcore-rust): empty → pattern → length → reserved (per-segment) →
93
+ * duplicate.
94
+ */
82
95
  register(moduleId: string, module: unknown): void;
96
+ /** Inner registration — no validator, no ID validation. Used by discover() paths that run their own checks. */
97
+ private _registerImpl;
83
98
  unregister(moduleId: string): boolean;
84
99
  get(moduleId: string): unknown | null;
85
100
  has(moduleId: string): boolean;
@@ -93,6 +108,8 @@ export declare class Registry {
93
108
  getDefinition(moduleId: string): ModuleDescriptor | null;
94
109
  describe(moduleId: string): string;
95
110
  on(event: string, callback: EventCallback): void;
111
+ off(event: string, callback: EventCallback): boolean;
112
+ reload(): Promise<number>;
96
113
  private _triggerEvent;
97
114
  watch(): Promise<void>;
98
115
  unwatch(): void;
@@ -100,10 +117,25 @@ export declare class Registry {
100
117
  private _handleFileDeletion;
101
118
  private _pathToModuleId;
102
119
  /**
103
- * Register a module bypassing reserved-word checks.
104
- * Used exclusively by the sys-modules subsystem for system.* IDs.
120
+ * Register a sys/internal module that bypasses **only** the reserved word
121
+ * check. All other PROTOCOL_SPEC §2.7 validations (empty, EBNF pattern,
122
+ * length, duplicate) still apply.
123
+ *
124
+ * Used exclusively by the sys-modules subsystem for `system.*` IDs.
125
+ * Aligned with apcore-python `Registry.register_internal` and apcore-rust
126
+ * `Registry::register_internal`.
105
127
  */
106
128
  registerInternal(moduleId: string, module: unknown): void;
129
+ /**
130
+ * Export the JSON Schema for a registered module.
131
+ *
132
+ * Returns the schema as a plain object (`Record<string, unknown> | null`),
133
+ * matching Python's `dict | None` and Rust's `Option<Value>` return types.
134
+ * Returns `null` if the module is not registered.
135
+ * Use the standalone `exportSchema` function from `schema-export.ts` for
136
+ * serialized (JSON/YAML) output.
137
+ */
138
+ exportSchema(moduleId: string, strict?: boolean): Record<string, unknown> | null;
107
139
  clearCache(): void;
108
140
  /**
109
141
  * Number of in-flight executions per module.
@@ -133,6 +165,10 @@ export declare class Registry {
133
165
  beginDrain(moduleId: string): void;
134
166
  /**
135
167
  * Remove the draining mark and clean up drain state.
168
+ *
169
+ * If any waitDrained waiters are still pending (e.g., refCount briefly
170
+ * hit zero and then a new acquire bumped it back up), resolve them first
171
+ * so they do not wait for their individual timeouts before returning.
136
172
  */
137
173
  endDrain(moduleId: string): void;
138
174
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry/registry.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * Central module registry for discovering, registering, and querying modules.
3
3
  */
4
+ import { getDefault } from '../config.js';
4
5
  import { InvalidInputError, ModuleNotFoundError } from '../errors.js';
6
+ import { detectIdConflicts } from './conflicts.js';
5
7
  import { resolveDependencies } from './dependencies.js';
6
8
  import { resolveEntryPoint } from './entry-point.js';
7
9
  import { mergeModuleMetadata, parseDependencies } from './metadata.js';
10
+ import { getSchema } from './schema-export.js';
11
+ import { toStrictSchema } from '../schema/strict.js';
12
+ import { deepCopy } from '../utils/index.js';
8
13
  import { validateModule } from './validation.js';
9
14
  // ── Lazy-loaded Node.js modules ────────────────────────────────────
10
15
  // These are loaded on first use so that importing Registry in a browser
@@ -52,12 +57,56 @@ export const REGISTRY_EVENTS = Object.freeze({
52
57
  export const MODULE_ID_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/;
53
58
  /**
54
59
  * Maximum allowed length for a module ID.
60
+ *
61
+ * Per PROTOCOL_SPEC §2.7 EBNF constraint #1. 192 is filesystem-safe
62
+ * (192 + ".binding.yaml".length = 205 < 255-byte filename limit on
63
+ * ext4/xfs/NTFS/APFS/btrfs) and accommodates Java/.NET deep-namespace
64
+ * FQN-derived IDs. Bumped from 128 in spec 1.6.0-draft (2026-04-08).
55
65
  */
56
- export const MAX_MODULE_ID_LENGTH = 128;
66
+ export const MAX_MODULE_ID_LENGTH = 192;
57
67
  /**
58
- * Reserved words that cannot appear as any segment of a module ID.
68
+ * Reserved words that cannot appear as the first segment of a module ID.
59
69
  */
60
70
  export const RESERVED_WORDS = new Set(['system', 'internal', 'core', 'apcore', 'plugin', 'schema', 'acl']);
71
+ /**
72
+ * Validate a module ID against PROTOCOL_SPEC §2.7 in canonical order.
73
+ *
74
+ * Order: empty → pattern → length → reserved (first-segment).
75
+ * Duplicate detection is the caller's responsibility (it requires registry
76
+ * state).
77
+ *
78
+ * When `allowReserved` is true the first-segment reserved word check is
79
+ * skipped — used by `Registry.registerInternal` so sys modules can use the
80
+ * `system.*` prefix. All other validations (empty, pattern, length) still
81
+ * apply.
82
+ *
83
+ * Aligned with `apcore-python._validate_module_id` and
84
+ * `apcore::registry::registry::validate_module_id`.
85
+ *
86
+ * @internal
87
+ */
88
+ function validateModuleId(moduleId, allowReserved) {
89
+ // 1. empty check (message byte-aligned with apcore-python and apcore-rust)
90
+ if (!moduleId || typeof moduleId !== 'string') {
91
+ throw new InvalidInputError('module_id must be a non-empty string');
92
+ }
93
+ // 2. EBNF pattern check (message byte-aligned with apcore-python and apcore-rust:
94
+ // single quotes around the offending ID; bare regex source without /…/ delimiters)
95
+ if (!MODULE_ID_PATTERN.test(moduleId)) {
96
+ throw new InvalidInputError(`Invalid module ID: '${moduleId}'. Must match pattern: ${MODULE_ID_PATTERN.source} (lowercase, digits, underscores, dots only; no hyphens)`);
97
+ }
98
+ // 3. length check
99
+ if (moduleId.length > MAX_MODULE_ID_LENGTH) {
100
+ throw new InvalidInputError(`Module ID exceeds maximum length of ${MAX_MODULE_ID_LENGTH}: ${moduleId.length}`);
101
+ }
102
+ // 4. reserved word first-segment check (skipped for registerInternal)
103
+ if (!allowReserved) {
104
+ const firstSegment = moduleId.split('.')[0];
105
+ if (RESERVED_WORDS.has(firstSegment)) {
106
+ throw new InvalidInputError(`Module ID contains reserved word: '${firstSegment}'`);
107
+ }
108
+ }
109
+ }
61
110
  export class Registry {
62
111
  _extensionRoots;
63
112
  _modules = new Map();
@@ -67,6 +116,7 @@ export class Registry {
67
116
  [REGISTRY_EVENTS.UNREGISTER, []],
68
117
  ]);
69
118
  _idMap = {};
119
+ _lowercaseMap = new Map();
70
120
  _schemaCache = new Map();
71
121
  _config;
72
122
  _watchers;
@@ -94,10 +144,10 @@ export class Registry {
94
144
  }
95
145
  else if (config !== null) {
96
146
  const extRoot = config.get('extensions.root');
97
- this._extensionRoots = [{ root: extRoot ?? './extensions' }];
147
+ this._extensionRoots = [{ root: extRoot ?? getDefault('extensions.root') }];
98
148
  }
99
149
  else {
100
- this._extensionRoots = [{ root: './extensions' }];
150
+ this._extensionRoots = [{ root: getDefault('extensions.root') }];
101
151
  }
102
152
  this._config = config;
103
153
  this._idMapPath = options?.idMapPath ?? null;
@@ -124,9 +174,21 @@ export class Registry {
124
174
  async _discoverCustom() {
125
175
  const rootPaths = this._extensionRoots.map((r) => r['root']);
126
176
  const customModules = await this._customDiscoverer.discover(rootPaths);
177
+ if (!Array.isArray(customModules)) {
178
+ console.warn(`[apcore:registry] Custom discoverer returned non-array (${typeof customModules}); expected Array<{moduleId, module}>. Ignoring.`);
179
+ return 0;
180
+ }
127
181
  let count = 0;
128
182
  for (const entry of customModules) {
183
+ if (entry === null || typeof entry !== 'object') {
184
+ console.warn(`[apcore:registry] Malformed entry from custom discoverer (expected object, got ${entry === null ? 'null' : typeof entry}); skipping.`);
185
+ continue;
186
+ }
129
187
  const { moduleId, module: mod } = entry;
188
+ if (typeof moduleId !== 'string' || mod === undefined) {
189
+ console.warn(`[apcore:registry] Malformed entry from custom discoverer (missing 'moduleId' string or 'module'); skipping.`);
190
+ continue;
191
+ }
130
192
  // Apply custom validator if set
131
193
  if (this._customValidator !== null) {
132
194
  const errors = await this._customValidator.validate(mod);
@@ -136,7 +198,7 @@ export class Registry {
136
198
  }
137
199
  }
138
200
  try {
139
- this.register(moduleId, mod);
201
+ this._registerImpl(moduleId, mod);
140
202
  count++;
141
203
  }
142
204
  catch (e) {
@@ -180,7 +242,13 @@ export class Registry {
180
242
  ? dm.filePath.slice(root.length + 1)
181
243
  : null;
182
244
  if (relPath && relPath in this._idMap) {
183
- dm.canonicalId = this._idMap[relPath]['id'];
245
+ const rawId = this._idMap[relPath]['id'];
246
+ if (typeof rawId === 'string' && rawId.length > 0) {
247
+ dm.canonicalId = rawId;
248
+ }
249
+ else {
250
+ console.warn(`[apcore:registry] ID map entry for '${relPath}' has invalid 'id' field (got ${typeof rawId}), skipping override`);
251
+ }
184
252
  break;
185
253
  }
186
254
  }
@@ -229,13 +297,33 @@ export class Registry {
229
297
  }
230
298
  _resolveLoadOrder(validModules, rawMetadata) {
231
299
  const modulesWithDeps = [];
232
- for (const modId of validModules.keys()) {
300
+ const moduleVersions = new Map();
301
+ for (const [modId, cls] of validModules.entries()) {
233
302
  const meta = rawMetadata.get(modId) ?? {};
234
303
  const depsRaw = meta['dependencies'] ?? [];
235
304
  modulesWithDeps.push([modId, depsRaw.length > 0 ? parseDependencies(depsRaw) : []]);
305
+ const yamlVersion = meta['version'];
306
+ const codeVersion = cls?.version;
307
+ const resolvedVersion = (typeof yamlVersion === 'string' && yamlVersion) ||
308
+ (typeof codeVersion === 'string' && codeVersion) ||
309
+ '1.0.0';
310
+ moduleVersions.set(modId, resolvedVersion);
311
+ }
312
+ // Include already-registered modules so inter-batch version constraints
313
+ // resolve against the live registry too.
314
+ for (const [existingId, existingMod] of this._modules.entries()) {
315
+ if (!moduleVersions.has(existingId)) {
316
+ const existingVersion = existingMod?.version;
317
+ if (typeof existingVersion === 'string') {
318
+ moduleVersions.set(existingId, existingVersion);
319
+ }
320
+ }
236
321
  }
237
- const knownIds = new Set(modulesWithDeps.map(([id]) => id));
238
- return resolveDependencies(modulesWithDeps, knownIds);
322
+ const knownIds = new Set([
323
+ ...modulesWithDeps.map(([id]) => id),
324
+ ...this._modules.keys(),
325
+ ]);
326
+ return resolveDependencies(modulesWithDeps, knownIds, moduleVersions);
239
327
  }
240
328
  _registerInOrder(loadOrder, validModules, rawMetadata) {
241
329
  let count = 0;
@@ -245,6 +333,7 @@ export class Registry {
245
333
  const mergedMeta = mergeModuleMetadata(modObj, rawMetadata.get(modId) ?? {});
246
334
  this._modules.set(modId, mod);
247
335
  this._moduleMeta.set(modId, mergedMeta);
336
+ this._lowercaseMap.set(modId.toLowerCase(), modId);
248
337
  if (typeof modObj['onLoad'] === 'function') {
249
338
  try {
250
339
  modObj['onLoad']();
@@ -253,6 +342,7 @@ export class Registry {
253
342
  console.warn(`[apcore:registry] onLoad failed for ${modId}, skipping:`, e);
254
343
  this._modules.delete(modId);
255
344
  this._moduleMeta.delete(modId);
345
+ this._lowercaseMap.delete(modId.toLowerCase());
256
346
  continue;
257
347
  }
258
348
  }
@@ -261,26 +351,40 @@ export class Registry {
261
351
  }
262
352
  return count;
263
353
  }
354
+ /**
355
+ * Register a module.
356
+ *
357
+ * Validation order (PROTOCOL_SPEC §2.7, aligned with apcore-python and
358
+ * apcore-rust): empty → pattern → length → reserved (per-segment) →
359
+ * duplicate.
360
+ */
264
361
  register(moduleId, module) {
265
- if (!moduleId || typeof moduleId !== "string") {
266
- throw new InvalidInputError("Module ID must be a non-empty string");
267
- }
268
- if (!MODULE_ID_PATTERN.test(moduleId)) {
269
- throw new InvalidInputError(`Invalid module ID: "${moduleId}". Must match pattern: ${MODULE_ID_PATTERN} (lowercase, digits, underscores, dots only; no hyphens)`);
270
- }
271
- const parts = moduleId.split('.');
272
- for (const part of parts) {
273
- if (RESERVED_WORDS.has(part)) {
274
- throw new InvalidInputError(`Module ID contains reserved word: '${part}'`);
362
+ validateModuleId(moduleId, false);
363
+ if (this._customValidator !== null) {
364
+ const result = this._customValidator.validate(module);
365
+ if (result instanceof Promise) {
366
+ throw new InvalidInputError(`Custom validator for '${moduleId}' is async use discover() which awaits the validator, or register after awaiting validation manually.`);
367
+ }
368
+ if (result.length > 0) {
369
+ throw new InvalidInputError(`Custom validator rejected module '${moduleId}': ${result.join('; ')}`);
275
370
  }
276
371
  }
277
- if (moduleId.length > MAX_MODULE_ID_LENGTH) {
278
- throw new InvalidInputError(`Module ID exceeds maximum length of ${MAX_MODULE_ID_LENGTH}: ${moduleId.length}`);
279
- }
280
- if (this._modules.has(moduleId)) {
281
- throw new InvalidInputError(`Module already exists: ${moduleId}`);
372
+ this._registerImpl(moduleId, module);
373
+ }
374
+ /** Inner registration — no validator, no ID validation. Used by discover() paths that run their own checks. */
375
+ _registerImpl(moduleId, module) {
376
+ // Algorithm A03: detect ID conflicts (exact duplicate, reserved word, case collision)
377
+ const conflict = detectIdConflicts(moduleId, new Set(this._modules.keys()), RESERVED_WORDS, this._lowercaseMap);
378
+ if (conflict !== null) {
379
+ if (conflict.severity === 'error') {
380
+ throw new InvalidInputError(conflict.message);
381
+ }
382
+ else {
383
+ console.warn(`[apcore:registry] ID conflict: ${conflict.message}`);
384
+ }
282
385
  }
283
386
  this._modules.set(moduleId, module);
387
+ this._lowercaseMap.set(moduleId.toLowerCase(), moduleId);
284
388
  // Populate metadata from the module object
285
389
  const modObj = module;
286
390
  this._moduleMeta.set(moduleId, mergeModuleMetadata(modObj, {}));
@@ -292,6 +396,7 @@ export class Registry {
292
396
  catch (e) {
293
397
  this._modules.delete(moduleId);
294
398
  this._moduleMeta.delete(moduleId);
399
+ this._lowercaseMap.delete(moduleId.toLowerCase());
295
400
  throw e;
296
401
  }
297
402
  }
@@ -304,6 +409,7 @@ export class Registry {
304
409
  this._modules.delete(moduleId);
305
410
  this._moduleMeta.delete(moduleId);
306
411
  this._schemaCache.delete(moduleId);
412
+ this._lowercaseMap.delete(moduleId.toLowerCase());
307
413
  // Call onUnload if available
308
414
  const modObj = module;
309
415
  if (typeof modObj['onUnload'] === 'function') {
@@ -363,21 +469,27 @@ export class Registry {
363
469
  const module = this._modules.get(moduleId);
364
470
  if (module == null)
365
471
  return null;
472
+ // INVARIANT: every registration site (`register`, `registerInternal`,
473
+ // `_discoverDefault`) populates `_moduleMeta` via `mergeModuleMetadata`,
474
+ // so `meta` always contains the full set of canonical keys including
475
+ // an `annotations` slot. Read fields directly from it. The schemas
476
+ // come straight from the module instance because they are not part
477
+ // of the merged metadata payload.
366
478
  const meta = this._moduleMeta.get(moduleId) ?? {};
367
479
  const mod = module;
368
480
  return {
369
481
  moduleId,
370
- name: (meta['name'] ?? mod['name']) ?? null,
371
- description: (meta['description'] ?? mod['description']) ?? '',
372
- documentation: (meta['documentation'] ?? mod['documentation']) ?? null,
482
+ name: meta['name'] ?? null,
483
+ description: meta['description'] ?? '',
484
+ documentation: meta['documentation'] ?? null,
373
485
  inputSchema: mod['inputSchema'] ?? {},
374
486
  outputSchema: mod['outputSchema'] ?? {},
375
- version: (meta['version'] ?? mod['version']) ?? '1.0.0',
376
- tags: meta['tags'] ?? mod['tags'] ?? [],
377
- annotations: mod['annotations'] ?? null,
378
- examples: mod['examples'] ?? [],
487
+ version: meta['version'] ?? '1.0.0',
488
+ tags: meta['tags'] ?? [],
489
+ annotations: meta['annotations'] ?? null,
490
+ examples: meta['examples'] ?? [],
379
491
  metadata: meta['metadata'] ?? {},
380
- sunsetDate: (meta['sunsetDate'] ?? mod['sunsetDate']) ?? null,
492
+ sunsetDate: meta['sunsetDate'] ?? null,
381
493
  };
382
494
  }
383
495
  describe(moduleId) {
@@ -426,6 +538,21 @@ export class Registry {
426
538
  }
427
539
  this._callbacks.get(event).push(callback);
428
540
  }
541
+ off(event, callback) {
542
+ const validEvents = Object.values(REGISTRY_EVENTS);
543
+ if (!validEvents.includes(event)) {
544
+ throw new InvalidInputError(`Invalid event: ${event}. Must be one of: ${validEvents.map((e) => `'${e}'`).join(', ')}`);
545
+ }
546
+ const callbacks = this._callbacks.get(event);
547
+ const idx = callbacks.indexOf(callback);
548
+ if (idx === -1)
549
+ return false;
550
+ callbacks.splice(idx, 1);
551
+ return true;
552
+ }
553
+ async reload() {
554
+ return this.discover();
555
+ }
429
556
  _triggerEvent(event, moduleId, module) {
430
557
  const callbacks = this._callbacks.get(event) ?? [];
431
558
  for (const cb of callbacks) {
@@ -461,24 +588,32 @@ export class Registry {
461
588
  if (now - last < 300)
462
589
  return;
463
590
  this._debounceTimers?.set(fullPath, now);
591
+ const handle = (p) => {
592
+ p.catch((e) => {
593
+ console.warn(`[apcore:registry] Watch handler failed for ${fullPath}:`, e);
594
+ });
595
+ };
464
596
  if (eventType === "rename") {
465
597
  // Could be create or delete
466
598
  try {
467
599
  fs.accessSync(fullPath);
468
- this._handleFileChange(fullPath);
600
+ handle(this._handleFileChange(fullPath));
469
601
  }
470
602
  catch {
471
- this._handleFileDeletion(fullPath);
603
+ handle(this._handleFileDeletion(fullPath));
472
604
  }
473
605
  }
474
606
  else {
475
- this._handleFileChange(fullPath);
607
+ handle(this._handleFileChange(fullPath));
476
608
  }
477
609
  });
478
610
  this._watchers.push(watcher);
479
611
  }
480
- catch {
481
- // Skip directories that don't exist
612
+ catch (e) {
613
+ // Surface real failures (EMFILE, EACCES, Linux kernels < 4.7 without
614
+ // recursive support, etc.). A silently non-functional watch misleads
615
+ // users who expect hot-reload to be active.
616
+ console.warn(`[apcore:registry] fs.watch failed for '${rootPath}' — hot-reload disabled for this root:`, e);
482
617
  }
483
618
  }
484
619
  }
@@ -501,12 +636,17 @@ export class Registry {
501
636
  try {
502
637
  oldModule.onUnload();
503
638
  }
504
- catch { /* ignore */ }
639
+ catch (e) {
640
+ console.warn(`[apcore:registry] onUnload failed for '${moduleId}':`, e);
641
+ }
505
642
  }
506
643
  this.unregister(moduleId);
507
644
  }
508
- // Re-import is complex in ES modules - emit event for user to handle
509
- this._triggerEvent("register", moduleId ?? basename(filePath, extname(filePath)), null);
645
+ // Re-import is complex in ES modules tell consumers that a watched file
646
+ // changed so they can re-import and re-register. The earlier design
647
+ // emitted a 'register' event with a null module, which crashed any
648
+ // consumer that accessed fields on the module argument.
649
+ this._triggerEvent("file_changed", moduleId ?? basename(filePath, extname(filePath)), { filePath });
510
650
  }
511
651
  async _handleFileDeletion(path) {
512
652
  const moduleId = this._pathToModuleId(path);
@@ -516,7 +656,9 @@ export class Registry {
516
656
  try {
517
657
  module.onUnload();
518
658
  }
519
- catch { /* ignore */ }
659
+ catch (e) {
660
+ console.warn(`[apcore:registry] onUnload failed for '${moduleId}':`, e);
661
+ }
520
662
  }
521
663
  this.unregister(moduleId);
522
664
  }
@@ -533,21 +675,18 @@ export class Registry {
533
675
  return null;
534
676
  }
535
677
  /**
536
- * Register a module bypassing reserved-word checks.
537
- * Used exclusively by the sys-modules subsystem for system.* IDs.
678
+ * Register a sys/internal module that bypasses **only** the reserved word
679
+ * check. All other PROTOCOL_SPEC §2.7 validations (empty, EBNF pattern,
680
+ * length, duplicate) still apply.
681
+ *
682
+ * Used exclusively by the sys-modules subsystem for `system.*` IDs.
683
+ * Aligned with apcore-python `Registry.register_internal` and apcore-rust
684
+ * `Registry::register_internal`.
538
685
  */
539
686
  registerInternal(moduleId, module) {
540
- if (!moduleId || typeof moduleId !== "string") {
541
- throw new InvalidInputError("Module ID must be a non-empty string");
542
- }
543
- if (!MODULE_ID_PATTERN.test(moduleId)) {
544
- throw new InvalidInputError(`Invalid module ID: "${moduleId}". Must match pattern: ${MODULE_ID_PATTERN}`);
545
- }
546
- if (moduleId.length > MAX_MODULE_ID_LENGTH) {
547
- throw new InvalidInputError(`Module ID exceeds maximum length of ${MAX_MODULE_ID_LENGTH}: ${moduleId.length}`);
548
- }
687
+ validateModuleId(moduleId, true);
549
688
  if (this._modules.has(moduleId)) {
550
- throw new InvalidInputError(`Module already exists: ${moduleId}`);
689
+ throw new InvalidInputError(`Module ID '${moduleId}' is already registered`);
551
690
  }
552
691
  this._modules.set(moduleId, module);
553
692
  const modObj = module;
@@ -564,6 +703,27 @@ export class Registry {
564
703
  }
565
704
  this._triggerEvent(REGISTRY_EVENTS.REGISTER, moduleId, module);
566
705
  }
706
+ /**
707
+ * Export the JSON Schema for a registered module.
708
+ *
709
+ * Returns the schema as a plain object (`Record<string, unknown> | null`),
710
+ * matching Python's `dict | None` and Rust's `Option<Value>` return types.
711
+ * Returns `null` if the module is not registered.
712
+ * Use the standalone `exportSchema` function from `schema-export.ts` for
713
+ * serialized (JSON/YAML) output.
714
+ */
715
+ exportSchema(moduleId, strict = false) {
716
+ const schema = getSchema(this, moduleId);
717
+ if (schema === null)
718
+ return null;
719
+ if (strict) {
720
+ const result = deepCopy(schema);
721
+ result['input_schema'] = toStrictSchema(result['input_schema']);
722
+ result['output_schema'] = toStrictSchema(result['output_schema']);
723
+ return result;
724
+ }
725
+ return schema;
726
+ }
567
727
  clearCache() {
568
728
  this._schemaCache.clear();
569
729
  }
@@ -629,9 +789,18 @@ export class Registry {
629
789
  }
630
790
  /**
631
791
  * Remove the draining mark and clean up drain state.
792
+ *
793
+ * If any waitDrained waiters are still pending (e.g., refCount briefly
794
+ * hit zero and then a new acquire bumped it back up), resolve them first
795
+ * so they do not wait for their individual timeouts before returning.
632
796
  */
633
797
  endDrain(moduleId) {
634
798
  this._draining.delete(moduleId);
799
+ const resolvers = this._drainResolvers.get(moduleId);
800
+ if (resolvers) {
801
+ for (const resolve of resolvers)
802
+ resolve();
803
+ }
635
804
  this._drainResolvers.delete(moduleId);
636
805
  this._refCounts.delete(moduleId);
637
806
  }