apcore-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.gitmessage +60 -0
  3. package/.pre-commit-config.yaml +28 -0
  4. package/CHANGELOG.md +47 -0
  5. package/CLAUDE.md +68 -0
  6. package/README.md +131 -0
  7. package/apcore-logo.svg +79 -0
  8. package/package.json +37 -0
  9. package/planning/acl-system/overview.md +54 -0
  10. package/planning/acl-system/plan.md +92 -0
  11. package/planning/acl-system/state.json +76 -0
  12. package/planning/acl-system/tasks/acl-core.md +226 -0
  13. package/planning/acl-system/tasks/acl-rule.md +92 -0
  14. package/planning/acl-system/tasks/conditional-rules.md +259 -0
  15. package/planning/acl-system/tasks/pattern-matching.md +152 -0
  16. package/planning/acl-system/tasks/yaml-loading.md +271 -0
  17. package/planning/core-executor/overview.md +53 -0
  18. package/planning/core-executor/plan.md +88 -0
  19. package/planning/core-executor/state.json +76 -0
  20. package/planning/core-executor/tasks/async-support.md +106 -0
  21. package/planning/core-executor/tasks/execution-pipeline.md +113 -0
  22. package/planning/core-executor/tasks/redaction.md +85 -0
  23. package/planning/core-executor/tasks/safety-checks.md +65 -0
  24. package/planning/core-executor/tasks/setup.md +75 -0
  25. package/planning/decorator-bindings/overview.md +62 -0
  26. package/planning/decorator-bindings/plan.md +104 -0
  27. package/planning/decorator-bindings/state.json +87 -0
  28. package/planning/decorator-bindings/tasks/binding-directory.md +79 -0
  29. package/planning/decorator-bindings/tasks/binding-loader.md +148 -0
  30. package/planning/decorator-bindings/tasks/explicit-schemas.md +85 -0
  31. package/planning/decorator-bindings/tasks/function-module.md +127 -0
  32. package/planning/decorator-bindings/tasks/module-factory.md +89 -0
  33. package/planning/decorator-bindings/tasks/schema-modes.md +142 -0
  34. package/planning/middleware-system/overview.md +48 -0
  35. package/planning/middleware-system/plan.md +102 -0
  36. package/planning/middleware-system/state.json +65 -0
  37. package/planning/middleware-system/tasks/adapters.md +170 -0
  38. package/planning/middleware-system/tasks/base.md +115 -0
  39. package/planning/middleware-system/tasks/logging-middleware.md +304 -0
  40. package/planning/middleware-system/tasks/manager.md +313 -0
  41. package/planning/observability/overview.md +53 -0
  42. package/planning/observability/plan.md +119 -0
  43. package/planning/observability/state.json +98 -0
  44. package/planning/observability/tasks/context-logger.md +201 -0
  45. package/planning/observability/tasks/exporters.md +121 -0
  46. package/planning/observability/tasks/metrics-collector.md +162 -0
  47. package/planning/observability/tasks/metrics-middleware.md +141 -0
  48. package/planning/observability/tasks/obs-logging-middleware.md +179 -0
  49. package/planning/observability/tasks/span-model.md +120 -0
  50. package/planning/observability/tasks/tracing-middleware.md +179 -0
  51. package/planning/overview.md +81 -0
  52. package/planning/registry-system/overview.md +57 -0
  53. package/planning/registry-system/plan.md +114 -0
  54. package/planning/registry-system/state.json +109 -0
  55. package/planning/registry-system/tasks/dependencies.md +157 -0
  56. package/planning/registry-system/tasks/entry-point.md +148 -0
  57. package/planning/registry-system/tasks/metadata.md +198 -0
  58. package/planning/registry-system/tasks/registry-core.md +323 -0
  59. package/planning/registry-system/tasks/scanner.md +172 -0
  60. package/planning/registry-system/tasks/schema-export.md +261 -0
  61. package/planning/registry-system/tasks/types.md +124 -0
  62. package/planning/registry-system/tasks/validation.md +177 -0
  63. package/planning/schema-system/overview.md +56 -0
  64. package/planning/schema-system/plan.md +121 -0
  65. package/planning/schema-system/state.json +98 -0
  66. package/planning/schema-system/tasks/exporter.md +153 -0
  67. package/planning/schema-system/tasks/loader.md +106 -0
  68. package/planning/schema-system/tasks/ref-resolver.md +133 -0
  69. package/planning/schema-system/tasks/strict-mode.md +140 -0
  70. package/planning/schema-system/tasks/typebox-generation.md +133 -0
  71. package/planning/schema-system/tasks/types-and-annotations.md +160 -0
  72. package/planning/schema-system/tasks/validator.md +149 -0
  73. package/src/acl.ts +188 -0
  74. package/src/bindings.ts +208 -0
  75. package/src/config.ts +24 -0
  76. package/src/context.ts +75 -0
  77. package/src/decorator.ts +110 -0
  78. package/src/errors.ts +369 -0
  79. package/src/executor.ts +348 -0
  80. package/src/index.ts +81 -0
  81. package/src/middleware/adapters.ts +54 -0
  82. package/src/middleware/base.ts +33 -0
  83. package/src/middleware/index.ts +6 -0
  84. package/src/middleware/logging.ts +103 -0
  85. package/src/middleware/manager.ts +105 -0
  86. package/src/module.ts +41 -0
  87. package/src/observability/context-logger.ts +201 -0
  88. package/src/observability/index.ts +4 -0
  89. package/src/observability/metrics.ts +212 -0
  90. package/src/observability/tracing.ts +187 -0
  91. package/src/registry/dependencies.ts +99 -0
  92. package/src/registry/entry-point.ts +64 -0
  93. package/src/registry/index.ts +8 -0
  94. package/src/registry/metadata.ts +111 -0
  95. package/src/registry/registry.ts +314 -0
  96. package/src/registry/scanner.ts +150 -0
  97. package/src/registry/schema-export.ts +177 -0
  98. package/src/registry/types.ts +32 -0
  99. package/src/registry/validation.ts +38 -0
  100. package/src/schema/annotations.ts +67 -0
  101. package/src/schema/exporter.ts +93 -0
  102. package/src/schema/index.ts +14 -0
  103. package/src/schema/loader.ts +270 -0
  104. package/src/schema/ref-resolver.ts +235 -0
  105. package/src/schema/strict.ts +128 -0
  106. package/src/schema/types.ts +73 -0
  107. package/src/schema/validator.ts +82 -0
  108. package/src/utils/index.ts +1 -0
  109. package/src/utils/pattern.ts +30 -0
  110. package/tests/helpers.ts +30 -0
  111. package/tests/integration/test-acl-safety.test.ts +268 -0
  112. package/tests/integration/test-binding-executor.test.ts +194 -0
  113. package/tests/integration/test-e2e-flow.test.ts +117 -0
  114. package/tests/integration/test-error-propagation.test.ts +259 -0
  115. package/tests/integration/test-middleware-chain.test.ts +120 -0
  116. package/tests/integration/test-observability-integration.test.ts +438 -0
  117. package/tests/observability/test-context-logger.test.ts +123 -0
  118. package/tests/observability/test-metrics.test.ts +89 -0
  119. package/tests/observability/test-tracing.test.ts +131 -0
  120. package/tests/registry/test-dependencies.test.ts +70 -0
  121. package/tests/registry/test-entry-point.test.ts +133 -0
  122. package/tests/registry/test-metadata.test.ts +265 -0
  123. package/tests/registry/test-registry.test.ts +140 -0
  124. package/tests/registry/test-scanner.test.ts +257 -0
  125. package/tests/registry/test-schema-export.test.ts +224 -0
  126. package/tests/registry/test-validation.test.ts +75 -0
  127. package/tests/schema/test-loader.test.ts +97 -0
  128. package/tests/schema/test-ref-resolver.test.ts +105 -0
  129. package/tests/schema/test-strict.test.ts +139 -0
  130. package/tests/schema/test-validator.test.ts +64 -0
  131. package/tests/test-acl.test.ts +206 -0
  132. package/tests/test-bindings.test.ts +227 -0
  133. package/tests/test-config.test.ts +76 -0
  134. package/tests/test-context.test.ts +151 -0
  135. package/tests/test-decorator.test.ts +173 -0
  136. package/tests/test-errors.test.ts +204 -0
  137. package/tests/test-executor.test.ts +252 -0
  138. package/tests/test-middleware-manager.test.ts +185 -0
  139. package/tests/test-middleware.test.ts +86 -0
  140. package/tsconfig.build.json +8 -0
  141. package/tsconfig.json +20 -0
  142. package/vitest.config.ts +18 -0
package/src/index.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * apcore - Schema-driven module development framework.
3
+ */
4
+
5
+ // Core
6
+ export { Context, createIdentity } from './context.js';
7
+ export type { Identity } from './context.js';
8
+ export { Registry } from './registry/registry.js';
9
+ export { Executor, redactSensitive, REDACTED_VALUE } from './executor.js';
10
+
11
+ // Module types
12
+ export { DEFAULT_ANNOTATIONS } from './module.js';
13
+ export type { ModuleAnnotations, ModuleExample, ValidationResult, Module } from './module.js';
14
+
15
+ // Config
16
+ export { Config } from './config.js';
17
+
18
+ // Errors
19
+ export {
20
+ ModuleError,
21
+ ConfigNotFoundError,
22
+ ConfigError,
23
+ ACLRuleError,
24
+ ACLDeniedError,
25
+ ModuleNotFoundError,
26
+ ModuleTimeoutError,
27
+ SchemaValidationError,
28
+ SchemaNotFoundError,
29
+ SchemaParseError,
30
+ SchemaCircularRefError,
31
+ CallDepthExceededError,
32
+ CircularCallError,
33
+ CallFrequencyExceededError,
34
+ InvalidInputError,
35
+ FuncMissingTypeHintError,
36
+ FuncMissingReturnTypeError,
37
+ BindingInvalidTargetError,
38
+ BindingModuleNotFoundError,
39
+ BindingCallableNotFoundError,
40
+ BindingNotCallableError,
41
+ BindingSchemaMissingError,
42
+ BindingFileInvalidError,
43
+ CircularDependencyError,
44
+ ModuleLoadError,
45
+ } from './errors.js';
46
+
47
+ // ACL
48
+ export { ACL } from './acl.js';
49
+ export type { ACLRule } from './acl.js';
50
+
51
+ // Middleware
52
+ export { Middleware, MiddlewareManager, MiddlewareChainError, BeforeMiddleware, AfterMiddleware, LoggingMiddleware } from './middleware/index.js';
53
+
54
+ // Decorator
55
+ export { module, FunctionModule, normalizeResult, makeAutoId } from './decorator.js';
56
+
57
+ // Bindings
58
+ export { BindingLoader } from './bindings.js';
59
+
60
+ // Utils
61
+ export { matchPattern } from './utils/pattern.js';
62
+
63
+ // Schema
64
+ export { SchemaLoader, jsonSchemaToTypeBox } from './schema/loader.js';
65
+ export { SchemaValidator } from './schema/validator.js';
66
+ export { SchemaExporter } from './schema/exporter.js';
67
+ export { SchemaStrategy, ExportProfile } from './schema/types.js';
68
+ export type { SchemaDefinition, ResolvedSchema, SchemaValidationErrorDetail, SchemaValidationResult } from './schema/types.js';
69
+ export { RefResolver } from './schema/ref-resolver.js';
70
+ export { toStrictSchema, applyLlmDescriptions, stripExtensions } from './schema/strict.js';
71
+
72
+ // Registry types
73
+ export type { ModuleDescriptor, DiscoveredModule, DependencyInfo } from './registry/types.js';
74
+
75
+ // Observability
76
+ export { TracingMiddleware, StdoutExporter, InMemoryExporter, createSpan } from './observability/tracing.js';
77
+ export type { Span, SpanExporter } from './observability/tracing.js';
78
+ export { MetricsCollector, MetricsMiddleware } from './observability/metrics.js';
79
+ export { ContextLogger, ObsLoggingMiddleware } from './observability/context-logger.js';
80
+
81
+ export const VERSION = '0.1.0';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Function adapter classes for the middleware system.
3
+ */
4
+
5
+ import type { Context } from '../context.js';
6
+ import { Middleware } from './base.js';
7
+
8
+ export type BeforeCallback = (
9
+ moduleId: string,
10
+ inputs: Record<string, unknown>,
11
+ context: Context,
12
+ ) => Record<string, unknown> | null;
13
+
14
+ export type AfterCallback = (
15
+ moduleId: string,
16
+ inputs: Record<string, unknown>,
17
+ output: Record<string, unknown>,
18
+ context: Context,
19
+ ) => Record<string, unknown> | null;
20
+
21
+ export class BeforeMiddleware extends Middleware {
22
+ private _callback: BeforeCallback;
23
+
24
+ constructor(callback: BeforeCallback) {
25
+ super();
26
+ this._callback = callback;
27
+ }
28
+
29
+ override before(
30
+ moduleId: string,
31
+ inputs: Record<string, unknown>,
32
+ context: Context,
33
+ ): Record<string, unknown> | null {
34
+ return this._callback(moduleId, inputs, context);
35
+ }
36
+ }
37
+
38
+ export class AfterMiddleware extends Middleware {
39
+ private _callback: AfterCallback;
40
+
41
+ constructor(callback: AfterCallback) {
42
+ super();
43
+ this._callback = callback;
44
+ }
45
+
46
+ override after(
47
+ moduleId: string,
48
+ inputs: Record<string, unknown>,
49
+ output: Record<string, unknown>,
50
+ context: Context,
51
+ ): Record<string, unknown> | null {
52
+ return this._callback(moduleId, inputs, output, context);
53
+ }
54
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Middleware base class for apcore.
3
+ */
4
+
5
+ import type { Context } from '../context.js';
6
+
7
+ export class Middleware {
8
+ before(
9
+ _moduleId: string,
10
+ _inputs: Record<string, unknown>,
11
+ _context: Context,
12
+ ): Record<string, unknown> | null {
13
+ return null;
14
+ }
15
+
16
+ after(
17
+ _moduleId: string,
18
+ _inputs: Record<string, unknown>,
19
+ _output: Record<string, unknown>,
20
+ _context: Context,
21
+ ): Record<string, unknown> | null {
22
+ return null;
23
+ }
24
+
25
+ onError(
26
+ _moduleId: string,
27
+ _inputs: Record<string, unknown>,
28
+ _error: Error,
29
+ _context: Context,
30
+ ): Record<string, unknown> | null {
31
+ return null;
32
+ }
33
+ }
@@ -0,0 +1,6 @@
1
+ export { Middleware } from './base.js';
2
+ export { MiddlewareManager, MiddlewareChainError } from './manager.js';
3
+ export { BeforeMiddleware, AfterMiddleware } from './adapters.js';
4
+ export type { BeforeCallback, AfterCallback } from './adapters.js';
5
+ export { LoggingMiddleware } from './logging.js';
6
+ export type { Logger } from './logging.js';
@@ -0,0 +1,103 @@
1
+ /**
2
+ * LoggingMiddleware for structured module call logging.
3
+ */
4
+
5
+ import type { Context } from '../context.js';
6
+ import { Middleware } from './base.js';
7
+
8
+ export interface Logger {
9
+ info(message: string, extra?: Record<string, unknown>): void;
10
+ error(message: string, extra?: Record<string, unknown>): void;
11
+ }
12
+
13
+ const defaultLogger: Logger = {
14
+ info(message: string, extra?: Record<string, unknown>) {
15
+ console.log(message, extra ?? '');
16
+ },
17
+ error(message: string, extra?: Record<string, unknown>) {
18
+ console.error(message, extra ?? '');
19
+ },
20
+ };
21
+
22
+ export class LoggingMiddleware extends Middleware {
23
+ private _logger: Logger;
24
+ private _logInputs: boolean;
25
+ private _logOutputs: boolean;
26
+ private _logErrors: boolean;
27
+
28
+ constructor(options?: {
29
+ logger?: Logger;
30
+ logInputs?: boolean;
31
+ logOutputs?: boolean;
32
+ logErrors?: boolean;
33
+ }) {
34
+ super();
35
+ this._logger = options?.logger ?? defaultLogger;
36
+ this._logInputs = options?.logInputs ?? true;
37
+ this._logOutputs = options?.logOutputs ?? true;
38
+ this._logErrors = options?.logErrors ?? true;
39
+ }
40
+
41
+ override before(
42
+ moduleId: string,
43
+ inputs: Record<string, unknown>,
44
+ context: Context,
45
+ ): null {
46
+ context.data['_logging_mw_start'] = performance.now();
47
+
48
+ if (this._logInputs) {
49
+ const redacted = context.redactedInputs ?? inputs;
50
+ this._logger.info(`[${context.traceId}] START ${moduleId}`, {
51
+ traceId: context.traceId,
52
+ moduleId,
53
+ callerId: context.callerId,
54
+ inputs: redacted,
55
+ });
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ override after(
62
+ moduleId: string,
63
+ _inputs: Record<string, unknown>,
64
+ output: Record<string, unknown>,
65
+ context: Context,
66
+ ): null {
67
+ const startTime = (context.data['_logging_mw_start'] as number) ?? performance.now();
68
+ const durationMs = performance.now() - startTime;
69
+
70
+ if (this._logOutputs) {
71
+ this._logger.info(
72
+ `[${context.traceId}] END ${moduleId} (${durationMs.toFixed(2)}ms)`,
73
+ {
74
+ traceId: context.traceId,
75
+ moduleId,
76
+ durationMs,
77
+ output,
78
+ },
79
+ );
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ override onError(
86
+ moduleId: string,
87
+ inputs: Record<string, unknown>,
88
+ error: Error,
89
+ context: Context,
90
+ ): null {
91
+ if (this._logErrors) {
92
+ const redacted = context.redactedInputs ?? inputs;
93
+ this._logger.error(`[${context.traceId}] ERROR ${moduleId}: ${error}`, {
94
+ traceId: context.traceId,
95
+ moduleId,
96
+ error: String(error),
97
+ inputs: redacted,
98
+ });
99
+ }
100
+
101
+ return null;
102
+ }
103
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * MiddlewareManager -- onion model execution engine for the middleware pipeline.
3
+ */
4
+
5
+ import type { Context } from '../context.js';
6
+ import { ModuleError } from '../errors.js';
7
+ import { Middleware } from './base.js';
8
+
9
+ export class MiddlewareChainError extends ModuleError {
10
+ readonly original: Error;
11
+ readonly executedMiddlewares: Middleware[];
12
+
13
+ constructor(original: Error, executedMiddlewares: Middleware[]) {
14
+ super('MIDDLEWARE_CHAIN_ERROR', String(original));
15
+ this.name = 'MiddlewareChainError';
16
+ this.original = original;
17
+ this.executedMiddlewares = executedMiddlewares;
18
+ }
19
+ }
20
+
21
+ export class MiddlewareManager {
22
+ private _middlewares: Middleware[] = [];
23
+
24
+ add(middleware: Middleware): void {
25
+ this._middlewares.push(middleware);
26
+ }
27
+
28
+ remove(middleware: Middleware): boolean {
29
+ for (let i = 0; i < this._middlewares.length; i++) {
30
+ if (this._middlewares[i] === middleware) {
31
+ this._middlewares.splice(i, 1);
32
+ return true;
33
+ }
34
+ }
35
+ return false;
36
+ }
37
+
38
+ snapshot(): Middleware[] {
39
+ return [...this._middlewares];
40
+ }
41
+
42
+ executeBefore(
43
+ moduleId: string,
44
+ inputs: Record<string, unknown>,
45
+ context: Context,
46
+ ): [Record<string, unknown>, Middleware[]] {
47
+ let currentInputs = inputs;
48
+ const executedMiddlewares: Middleware[] = [];
49
+ const middlewares = this.snapshot();
50
+
51
+ for (const mw of middlewares) {
52
+ executedMiddlewares.push(mw);
53
+ try {
54
+ const result = mw.before(moduleId, currentInputs, context);
55
+ if (result !== null) {
56
+ currentInputs = result;
57
+ }
58
+ } catch (e) {
59
+ throw new MiddlewareChainError(e as Error, executedMiddlewares);
60
+ }
61
+ }
62
+
63
+ return [currentInputs, executedMiddlewares];
64
+ }
65
+
66
+ executeAfter(
67
+ moduleId: string,
68
+ inputs: Record<string, unknown>,
69
+ output: Record<string, unknown>,
70
+ context: Context,
71
+ ): Record<string, unknown> {
72
+ let currentOutput = output;
73
+ const middlewares = this.snapshot();
74
+
75
+ for (let i = middlewares.length - 1; i >= 0; i--) {
76
+ const result = middlewares[i].after(moduleId, inputs, currentOutput, context);
77
+ if (result !== null) {
78
+ currentOutput = result;
79
+ }
80
+ }
81
+
82
+ return currentOutput;
83
+ }
84
+
85
+ executeOnError(
86
+ moduleId: string,
87
+ inputs: Record<string, unknown>,
88
+ error: Error,
89
+ context: Context,
90
+ executedMiddlewares: Middleware[],
91
+ ): Record<string, unknown> | null {
92
+ for (let i = executedMiddlewares.length - 1; i >= 0; i--) {
93
+ try {
94
+ const result = executedMiddlewares[i].onError(moduleId, inputs, error, context);
95
+ if (result !== null) {
96
+ return result;
97
+ }
98
+ } catch {
99
+ // Swallow errors in onError handlers
100
+ continue;
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ }
package/src/module.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Module interface and related data types.
3
+ */
4
+
5
+ import type { TSchema } from '@sinclair/typebox';
6
+ import type { Context } from './context.js';
7
+
8
+ export interface ModuleAnnotations {
9
+ readonly readonly: boolean;
10
+ readonly destructive: boolean;
11
+ readonly idempotent: boolean;
12
+ readonly requiresApproval: boolean;
13
+ readonly openWorld: boolean;
14
+ }
15
+
16
+ export const DEFAULT_ANNOTATIONS: ModuleAnnotations = Object.freeze({
17
+ readonly: false,
18
+ destructive: false,
19
+ idempotent: false,
20
+ requiresApproval: false,
21
+ openWorld: true,
22
+ });
23
+
24
+ export interface ModuleExample {
25
+ title: string;
26
+ inputs: Record<string, unknown>;
27
+ output: Record<string, unknown>;
28
+ description?: string;
29
+ }
30
+
31
+ export interface ValidationResult {
32
+ valid: boolean;
33
+ errors: Array<Record<string, string>>;
34
+ }
35
+
36
+ export interface Module {
37
+ inputSchema: TSchema;
38
+ outputSchema: TSchema;
39
+ description: string;
40
+ execute(inputs: Record<string, unknown>, context: Context): Promise<Record<string, unknown>> | Record<string, unknown>;
41
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Structured logging: ContextLogger and ObsLoggingMiddleware.
3
+ */
4
+
5
+ import type { Context } from '../context.js';
6
+ import { Middleware } from '../middleware/base.js';
7
+
8
+ const LEVELS: Record<string, number> = {
9
+ trace: 0,
10
+ debug: 10,
11
+ info: 20,
12
+ warn: 30,
13
+ error: 40,
14
+ fatal: 50,
15
+ };
16
+
17
+ const REDACTED = '***REDACTED***';
18
+
19
+ interface WritableOutput {
20
+ write(s: string): void;
21
+ }
22
+
23
+ export class ContextLogger {
24
+ private _name: string;
25
+ private _format: string;
26
+ private _level: string;
27
+ private _levelValue: number;
28
+ private _redactSensitive: boolean;
29
+ private _output: WritableOutput;
30
+ private _traceId: string | null = null;
31
+ private _moduleId: string | null = null;
32
+ private _callerId: string | null = null;
33
+
34
+ constructor(options?: {
35
+ name?: string;
36
+ format?: string;
37
+ level?: string;
38
+ redactSensitive?: boolean;
39
+ output?: WritableOutput;
40
+ }) {
41
+ this._name = options?.name ?? 'apcore';
42
+ this._format = options?.format ?? 'json';
43
+ this._level = options?.level ?? 'info';
44
+ this._levelValue = LEVELS[this._level] ?? 20;
45
+ this._redactSensitive = options?.redactSensitive ?? true;
46
+ this._output = options?.output ?? { write: (s: string) => process.stderr.write(s) };
47
+ }
48
+
49
+ static fromContext(context: Context, name: string, options?: {
50
+ format?: string;
51
+ level?: string;
52
+ redactSensitive?: boolean;
53
+ output?: WritableOutput;
54
+ }): ContextLogger {
55
+ const logger = new ContextLogger({ name, ...options });
56
+ logger._traceId = context.traceId;
57
+ logger._moduleId = context.callChain.length > 0 ? context.callChain[context.callChain.length - 1] : null;
58
+ logger._callerId = context.callerId;
59
+ return logger;
60
+ }
61
+
62
+ private _emit(levelName: string, message: string, extra?: Record<string, unknown> | null): void {
63
+ const levelValue = LEVELS[levelName] ?? 20;
64
+ if (levelValue < this._levelValue) return;
65
+
66
+ let redactedExtra = extra ?? null;
67
+ if (extra != null && this._redactSensitive) {
68
+ redactedExtra = {};
69
+ for (const [k, v] of Object.entries(extra)) {
70
+ (redactedExtra as Record<string, unknown>)[k] = k.startsWith('_secret_') ? REDACTED : v;
71
+ }
72
+ }
73
+
74
+ const now = new Date();
75
+ const entry: Record<string, unknown> = {
76
+ timestamp: now.toISOString(),
77
+ level: levelName,
78
+ message,
79
+ trace_id: this._traceId,
80
+ module_id: this._moduleId,
81
+ caller_id: this._callerId,
82
+ logger: this._name,
83
+ extra: redactedExtra,
84
+ };
85
+
86
+ if (this._format === 'json') {
87
+ this._output.write(JSON.stringify(entry) + '\n');
88
+ } else {
89
+ const ts = now.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
90
+ const lvl = levelName.toUpperCase();
91
+ const trace = this._traceId ?? 'none';
92
+ const mod = this._moduleId ?? 'none';
93
+ let extrasStr = '';
94
+ if (redactedExtra) {
95
+ extrasStr = ' ' + Object.entries(redactedExtra).map(([k, v]) => `${k}=${v}`).join(' ');
96
+ }
97
+ this._output.write(`${ts} [${lvl}] [trace=${trace}] [module=${mod}] ${message}${extrasStr}\n`);
98
+ }
99
+ }
100
+
101
+ trace(message: string, extra?: Record<string, unknown>): void {
102
+ this._emit('trace', message, extra);
103
+ }
104
+
105
+ debug(message: string, extra?: Record<string, unknown>): void {
106
+ this._emit('debug', message, extra);
107
+ }
108
+
109
+ info(message: string, extra?: Record<string, unknown>): void {
110
+ this._emit('info', message, extra);
111
+ }
112
+
113
+ warn(message: string, extra?: Record<string, unknown>): void {
114
+ this._emit('warn', message, extra);
115
+ }
116
+
117
+ error(message: string, extra?: Record<string, unknown>): void {
118
+ this._emit('error', message, extra);
119
+ }
120
+
121
+ fatal(message: string, extra?: Record<string, unknown>): void {
122
+ this._emit('fatal', message, extra);
123
+ }
124
+ }
125
+
126
+ export class ObsLoggingMiddleware extends Middleware {
127
+ private _logger: ContextLogger;
128
+ private _logInputs: boolean;
129
+ private _logOutputs: boolean;
130
+
131
+ constructor(options?: {
132
+ logger?: ContextLogger;
133
+ logInputs?: boolean;
134
+ logOutputs?: boolean;
135
+ }) {
136
+ super();
137
+ this._logger = options?.logger ?? new ContextLogger({ name: 'apcore.obs_logging' });
138
+ this._logInputs = options?.logInputs ?? true;
139
+ this._logOutputs = options?.logOutputs ?? true;
140
+ }
141
+
142
+ override before(
143
+ moduleId: string,
144
+ inputs: Record<string, unknown>,
145
+ context: Context,
146
+ ): null {
147
+ const starts = (context.data['_obs_logging_starts'] as number[]) ?? [];
148
+ starts.push(performance.now());
149
+ context.data['_obs_logging_starts'] = starts;
150
+
151
+ const extra: Record<string, unknown> = {
152
+ module_id: moduleId,
153
+ caller_id: context.callerId,
154
+ };
155
+ if (this._logInputs) {
156
+ extra['inputs'] = context.redactedInputs ?? inputs;
157
+ }
158
+ this._logger.info('Module call started', extra);
159
+ return null;
160
+ }
161
+
162
+ override after(
163
+ moduleId: string,
164
+ _inputs: Record<string, unknown>,
165
+ output: Record<string, unknown>,
166
+ context: Context,
167
+ ): null {
168
+ const starts = context.data['_obs_logging_starts'] as number[];
169
+ const startTime = starts.pop()!;
170
+ const durationMs = performance.now() - startTime;
171
+
172
+ const extra: Record<string, unknown> = {
173
+ module_id: moduleId,
174
+ duration_ms: durationMs,
175
+ };
176
+ if (this._logOutputs) {
177
+ extra['output'] = output;
178
+ }
179
+ this._logger.info('Module call completed', extra);
180
+ return null;
181
+ }
182
+
183
+ override onError(
184
+ moduleId: string,
185
+ _inputs: Record<string, unknown>,
186
+ error: Error,
187
+ context: Context,
188
+ ): null {
189
+ const starts = context.data['_obs_logging_starts'] as number[];
190
+ const startTime = starts.pop()!;
191
+ const durationMs = performance.now() - startTime;
192
+
193
+ this._logger.error('Module call failed', {
194
+ module_id: moduleId,
195
+ duration_ms: durationMs,
196
+ error_type: error.constructor.name,
197
+ error_message: String(error),
198
+ });
199
+ return null;
200
+ }
201
+ }
@@ -0,0 +1,4 @@
1
+ export { TracingMiddleware, StdoutExporter, InMemoryExporter, createSpan } from './tracing.js';
2
+ export type { Span, SpanExporter } from './tracing.js';
3
+ export { MetricsCollector, MetricsMiddleware } from './metrics.js';
4
+ export { ContextLogger, ObsLoggingMiddleware } from './context-logger.js';