Skip to content

Commit b38caa1

Browse files
committed
refactor: Migrate context logging to a dedicated ContextLogger and update related core and test files.
1 parent 78b40af commit b38caa1

File tree

62 files changed

+272
-41
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+272
-41
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions

README.md

Lines changed: 1 addition & 1 deletion

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apcore-js",
3-
"version": "0.8.0",
3+
"version": "0.9.0",
44
"description": "AI-Perceivable Core — schema-driven module development framework",
55
"type": "module",
66
"main": "./dist/index.js",

src/context.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { v4 as uuidv4 } from 'uuid';
33
import type { CancelToken } from './cancel.js';
44
import type { TraceParent } from './trace-context.js';
5+
import { ContextLogger } from './observability/context-logger.js';
56

67
/**
78
* Execution context, identity, and context creation.
@@ -131,14 +132,8 @@ export class Context<T = null> {
131132
);
132133
}
133134

134-
get logger(): { debug: (...args: unknown[]) => void; info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void } {
135-
const prefix = `[apcore:${this.callerId ?? 'unknown'}]`;
136-
return {
137-
debug: (...args: unknown[]) => console.debug(prefix, ...args),
138-
info: (...args: unknown[]) => console.info(prefix, ...args),
139-
warn: (...args: unknown[]) => console.warn(prefix, ...args),
140-
error: (...args: unknown[]) => console.error(prefix, ...args),
141-
};
135+
get logger(): ContextLogger {
136+
return ContextLogger.fromContext(this, this.callerId ?? 'unknown');
142137
}
143138

144139
child(targetModuleId: string): Context<T> {

src/executor.ts

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ import {
1919
ApprovalPendingError,
2020
ApprovalTimeoutError,
2121
InvalidInputError,
22+
ModuleError,
2223
ModuleNotFoundError,
2324
ModuleTimeoutError,
2425
SchemaValidationError,
2526
} from './errors.js';
2627
import { AfterMiddleware, BeforeMiddleware, Middleware } from './middleware/index.js';
2728
import { MiddlewareChainError, MiddlewareManager } from './middleware/manager.js';
2829
import { guardCallChain } from './utils/call-chain.js';
29-
import type { ModuleAnnotations, ValidationResult } from './module.js';
30-
import { DEFAULT_ANNOTATIONS } from './module.js';
30+
import type { ModuleAnnotations, PreflightCheckResult, PreflightResult } from './module.js';
31+
import { DEFAULT_ANNOTATIONS, createPreflightResult } from './module.js';
32+
import { MODULE_ID_PATTERN } from './registry/registry.js';
3133
import type { Registry } from './registry/registry.js';
3234

3335
export const REDACTED_VALUE: string = '***REDACTED***';
@@ -178,7 +180,7 @@ export class Executor {
178180
this._acl = acl;
179181
}
180182

181-
/** Set the approval handler for Step 4.5 gate. */
183+
/** Set the approval handler for Step 5 gate. */
182184
setApprovalHandler(handler: ApprovalHandler): void {
183185
this._approvalHandler = handler;
184186
}
@@ -336,7 +338,7 @@ export class Executor {
336338
const mod = this._lookupModule(moduleId);
337339
this._checkAcl(moduleId, ctx);
338340

339-
// Step 4.5 -- Approval Gate (strips internal keys like _approval_token)
341+
// Step 5 -- Approval Gate (strips internal keys like _approval_token)
340342
effectiveInputs = await this._checkApproval(mod, moduleId, effectiveInputs, ctx);
341343

342344
effectiveInputs = this._validateInputs(mod, effectiveInputs, ctx);
@@ -457,32 +459,98 @@ export class Executor {
457459
}
458460
}
459461

460-
validate(moduleId: string, inputs: Record<string, unknown>): ValidationResult {
462+
/**
463+
* Non-destructive preflight check through Steps 1-6 of the pipeline.
464+
* Returns a PreflightResult that is duck-type compatible with ValidationResult.
465+
*/
466+
validate(
467+
moduleId: string,
468+
inputs?: Record<string, unknown> | null,
469+
context?: Context | null,
470+
): PreflightResult {
471+
const effectiveInputs = inputs ?? {};
472+
const checks: PreflightCheckResult[] = [];
473+
let requiresApproval = false;
474+
475+
// Check 1: module_id format
476+
if (!MODULE_ID_PATTERN.test(moduleId)) {
477+
checks.push({
478+
check: 'module_id', passed: false,
479+
error: { code: 'INVALID_INPUT', message: `Invalid module ID: "${moduleId}"` },
480+
});
481+
return createPreflightResult(checks);
482+
}
483+
checks.push({ check: 'module_id', passed: true });
484+
485+
// Check 2: module lookup
461486
const module = this._registry.get(moduleId);
462487
if (module === null) {
463-
throw new ModuleNotFoundError(moduleId);
488+
checks.push({
489+
check: 'module_lookup', passed: false,
490+
error: { code: 'MODULE_NOT_FOUND', message: `Module not found: ${moduleId}` },
491+
});
492+
return createPreflightResult(checks);
464493
}
465-
494+
checks.push({ check: 'module_lookup', passed: true });
466495
const mod = module as Record<string, unknown>;
467-
const inputSchema = mod['inputSchema'] as TSchema | undefined;
468496

469-
if (inputSchema == null) {
470-
return { valid: true, errors: [] };
497+
// Check 3: call chain safety
498+
const ctx = this._createContext(moduleId, context);
499+
try {
500+
this._checkSafety(moduleId, ctx);
501+
checks.push({ check: 'call_chain', passed: true });
502+
} catch (e) {
503+
const err = e instanceof ModuleError
504+
? { code: e.code, message: e.message }
505+
: { code: 'CALL_CHAIN_ERROR', message: String(e) };
506+
checks.push({ check: 'call_chain', passed: false, error: err });
471507
}
472508

473-
if (Value.Check(inputSchema, inputs)) {
474-
return { valid: true, errors: [] };
509+
// Check 4: ACL
510+
if (this._acl !== null) {
511+
const allowed = this._acl.check(ctx.callerId, moduleId, ctx);
512+
if (!allowed) {
513+
checks.push({
514+
check: 'acl', passed: false,
515+
error: { code: 'ACL_DENIED', message: `Access denied: ${ctx.callerId} -> ${moduleId}` },
516+
});
517+
} else {
518+
checks.push({ check: 'acl', passed: true });
519+
}
520+
} else {
521+
checks.push({ check: 'acl', passed: true });
475522
}
476523

477-
const errors: Array<Record<string, string>> = [];
478-
for (const error of Value.Errors(inputSchema, inputs)) {
479-
errors.push({
480-
field: error.path || '/',
481-
code: String(error.type),
482-
message: error.message,
483-
});
524+
// Check 5: approval detection (report only, no handler invocation)
525+
if (this._needsApproval(mod)) {
526+
requiresApproval = true;
484527
}
485-
return { valid: false, errors };
528+
checks.push({ check: 'approval', passed: true });
529+
530+
// Check 6: input schema validation
531+
const inputSchema = mod['inputSchema'] as TSchema | undefined;
532+
if (inputSchema != null) {
533+
if (Value.Check(inputSchema, effectiveInputs)) {
534+
checks.push({ check: 'schema', passed: true });
535+
} else {
536+
const errors: Array<Record<string, unknown>> = [];
537+
for (const error of Value.Errors(inputSchema, effectiveInputs)) {
538+
errors.push({
539+
field: error.path || '/',
540+
code: String(error.type),
541+
message: error.message,
542+
});
543+
}
544+
checks.push({
545+
check: 'schema', passed: false,
546+
error: { code: 'SCHEMA_VALIDATION_ERROR', errors },
547+
});
548+
}
549+
} else {
550+
checks.push({ check: 'schema', passed: true });
551+
}
552+
553+
return createPreflightResult(checks, requiresApproval);
486554
}
487555

488556
private _checkSafety(moduleId: string, ctx: Context): void {
@@ -568,7 +636,7 @@ export class Executor {
568636
}
569637
}
570638

571-
/** Step 4.5: Approval gate. Returns inputs with internal keys stripped. */
639+
/** Step 5: Approval gate. Returns inputs with internal keys stripped. */
572640
private async _checkApproval(
573641
mod: Record<string, unknown>,
574642
moduleId: string,

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export type { Discoverer, ModuleValidator } from './registry/registry.js';
1111
export { Executor, redactSensitive, REDACTED_VALUE, CTX_GLOBAL_DEADLINE, CTX_TRACING_SPANS } from './executor.js';
1212

1313
// Module types
14-
export { DEFAULT_ANNOTATIONS } from './module.js';
15-
export type { ModuleAnnotations, ModuleExample, ValidationResult, Module } from './module.js';
14+
export { DEFAULT_ANNOTATIONS, createPreflightResult } from './module.js';
15+
export type { ModuleAnnotations, ModuleExample, ValidationResult, PreflightCheckResult, PreflightResult, Module } from './module.js';
1616

1717
// Config
1818
export { Config } from './config.js';
@@ -120,4 +120,4 @@ export { ContextLogger, ObsLoggingMiddleware } from './observability/context-log
120120
export { TraceContext } from './trace-context.js';
121121
export type { TraceParent } from './trace-context.js';
122122

123-
export const VERSION = '0.8.0';
123+
export const VERSION = '0.9.0';

src/module.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ export interface ValidationResult {
3535
errors: Array<Record<string, string>>;
3636
}
3737

38+
export interface PreflightCheckResult {
39+
readonly check: string;
40+
readonly passed: boolean;
41+
readonly error?: Record<string, unknown>;
42+
}
43+
44+
export interface PreflightResult {
45+
readonly valid: boolean;
46+
readonly checks: PreflightCheckResult[];
47+
readonly requiresApproval: boolean;
48+
readonly errors: Array<Record<string, unknown>>;
49+
}
50+
51+
export function createPreflightResult(
52+
checks: PreflightCheckResult[],
53+
requiresApproval: boolean = false,
54+
): PreflightResult {
55+
const valid = checks.every(c => c.passed);
56+
const errors = checks
57+
.filter(c => !c.passed && c.error != null)
58+
.map(c => c.error!);
59+
return { valid, checks, requiresApproval, errors };
60+
}
61+
3862
export interface Module {
3963
inputSchema: TSchema;
4064
outputSchema: TSchema;

src/observability/context-logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class ContextLogger {
4747
this._output = options?.output ?? { write: (s: string) => console.error(s) };
4848
}
4949

50-
static fromContext(context: Context, name: string, options?: {
50+
static fromContext(context: Context<unknown>, name: string, options?: {
5151
format?: string;
5252
level?: string;
5353
redactSensitive?: boolean;

tests/async-task.test.d.ts.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/helpers.d.ts.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)