mcard-js
Version:
MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers
431 lines • 19.1 kB
JavaScript
/**
* CLMRunner - Execute JavaScript logic from CLM specifications
*
* Node.js PTR runtime for interpreting Cubical Logic Models.
*
* Refactored to use modular components from ./clm/
*/
import * as path from 'path';
import * as yaml from 'yaml';
import { RuntimeFactory } from '../runtimes/index.js';
import { PCard } from '../../../model/PCard.js';
import { VCard } from '../../../model/VCard.js';
// Utilities
import { asObject, buildBaseContext, buildExecutionResult, isRecursiveCLM, isMultiRuntime, } from './utils.js';
// Builtins
import { isNetworkBuiltin, isLoaderBuiltin, getHandleBuiltinType, isStaticServerBuiltin, isWebSocketServerBuiltin } from '../core/operations/index.js';
import { executeHandleVersion, executeHandlePrune } from '../core/operations/handle.js';
import { executeStaticServer } from '../core/operations/static-server.js';
import { executeWebSocketServer } from '../core/operations/websocket-server.js';
// Other modules
import { CLMLoader } from './loader.js';
import { executeMultiRuntime } from './multiruntime.js';
export class CLMRunner {
loader;
timeout;
collection;
constructor(basePath = process.cwd(), timeout = 5000, collection) {
this.loader = new CLMLoader(basePath);
this.timeout = timeout;
this.collection = collection;
}
/**
* Run a CLM directly from a file path.
*/
async runFile(clmPath, input) {
const fullPath = path.isAbsolute(clmPath)
? clmPath
: path.resolve(this.loader.basePath, clmPath);
const clm = this.loader.load(fullPath);
const chapterDir = path.dirname(fullPath);
return this.executeCLM(clm, chapterDir, input || {});
}
/**
* Execute a CLM specification with given input.
*
* Implements Petri Net Transition Semantics:
* 1. Constructs PCard (Transition)
* 2. Checks Pre-conditions (Firing Rule)
* 3. Executes Logic
* 4. Produces VerificationVCard (Token)
* 5. Persists to Collection (Place)
*/
async executeCLM(clm, chapterDir, input) {
// 1. Construct PCard (Transition)
// We recreate the YAML content to ensure content-addressability
const clmContent = yaml.stringify(clm);
const pcard = await PCard.create(clmContent);
// Persist PCard if collection available
if (this.collection) {
await this.collection.add(pcard);
}
// 0. GATEKEEPER CHECK (VCard Enforcement)
const inputObj = asObject(input);
const vcardManifest = inputObj.vcard_manifest || {};
// Convert plain object to Map for PCard.canFire
const manifestMap = new Map(Object.entries(vcardManifest));
const gatekeeperStatus = pcard.canFire(manifestMap);
if (!gatekeeperStatus.canFire) {
const errorMsg = `SecurityError: Gatekeeper Rejection. Missing required VCards: ${gatekeeperStatus.missing.join(', ')}`;
// Log error regardless of enforcement
console.error(`[CLMRunner] ${errorMsg}`);
if (inputObj.enforce_gatekeeper) {
throw new Error(errorMsg);
}
else {
console.warn("[CLMRunner] Gatekeeper check failed but enforcement is currently optional (input.enforce_gatekeeper = false). Proceeding with caution.");
}
}
const startTime = Date.now();
const config = clm.clm.concrete || { manifestation: 'Unknown', description: 'Unknown' };
const runtimeName = config.runtime || 'lambda';
// Handle recursive CLM
if (isRecursiveCLM(runtimeName)) {
return this.executeRecursive(runtimeName, config, chapterDir, input, clm);
}
let result;
try {
const builtin = config.builtin;
const operation = config.process || config.action || config.operation;
// Route to appropriate handler
// NOTE: Check specific builtins BEFORE isNetworkBuiltin (which uses prefix matching)
if (isLoaderBuiltin(builtin, operation) || runtimeName === 'loader' || runtimeName === 'collection_loader') {
result = await this.executeLoader(runtimeName, config, clm, chapterDir, input, startTime);
}
else if (isStaticServerBuiltin(builtin, operation)) {
// Static server builtin - extract inner config object
const serverConfig = config.config || {};
const serverResult = await executeStaticServer(serverConfig, asObject(input), chapterDir);
result = buildExecutionResult(serverResult.success, serverResult, startTime, clm);
}
else if (isWebSocketServerBuiltin(builtin, operation)) {
// WebSocket server builtin - extract inner config object
const wsConfig = config.config || {};
const wsResult = await executeWebSocketServer(wsConfig, asObject(input), chapterDir);
result = buildExecutionResult(wsResult.success, wsResult, startTime, clm);
}
else if (isNetworkBuiltin(builtin, operation)) {
result = await this.executeNetwork(config, clm, chapterDir, input, startTime);
}
else {
const handleType = getHandleBuiltinType(builtin, operation);
if (handleType) {
const handleResult = handleType === 'version'
? await executeHandleVersion(input)
: await executeHandlePrune(input);
result = buildExecutionResult(true, handleResult, startTime, clm);
}
else {
// Standard runtime execution
result = await this.executeStandard(runtimeName, config, clm, chapterDir, input, startTime);
}
}
}
catch (err) {
result = buildExecutionResult(false, undefined, startTime, clm, err instanceof Error ? err.message : String(err));
}
// 4. Produce VerificationVCard (Token)
// Only if we have a collection to store it in
if (this.collection) {
try {
// Determine previous hash from input if available (provenance chain)
const inputObj = asObject(input);
let previousVCard;
if (inputObj.previous_hash && typeof inputObj.previous_hash === 'string') {
const prevCard = await this.collection.get(inputObj.previous_hash);
if (prevCard && await VCard.fromMCard(prevCard)) {
previousVCard = await VCard.fromMCard(prevCard);
}
}
const vcard = await VCard.createVerificationVCard(pcard, {
success: result.success,
result: result.result,
error: result.error,
executionTime: result.executionTime
}, previousVCard, result.success);
await this.collection.add(vcard);
// Attach the VCard info to the result for visibility
result.petriNet = {
pcardHash: pcard.hash,
vcardHash: vcard.hash,
handle: vcard.getTokenHandle()
};
}
catch (e) {
// Don't fail the execution if token creation fails, but log it
console.warn(`[CLMRunner] Failed to create VerificationVCard: ${e}`);
}
}
return result;
}
/**
* Execute a CLM across multiple runtimes and verify consensus.
*/
async executeMultiRuntime(clm, chapterDir, input) {
return executeMultiRuntime(clm, chapterDir, input);
}
/**
* Check if a CLM is a multi-runtime CLM.
*/
isMultiRuntime(clm) {
return isMultiRuntime(clm);
}
/**
* Verify CLM output against expected result.
*/
async verifyCLM(clm, chapterDir, input, expected) {
const executionResult = await this.executeCLM(clm, chapterDir, input);
return {
verified: executionResult.success &&
JSON.stringify(executionResult.result) === JSON.stringify(expected),
expected,
actual: executionResult.result,
executionResult,
};
}
/**
* Run all examples from a CLM specification.
*/
async runExamples(clm, chapterDir) {
const examples = clm.examples || [];
const results = [];
for (const example of examples) {
const result = await this.executeCLM(clm, chapterDir, example.input);
results.push({ name: example.name, result });
}
return results;
}
/**
* Summarize example runs.
*/
summarizeExampleRuns(clm, results) {
let passed = 0;
const summaryResults = results.map((res, index) => {
const example = clm.examples?.[index];
const expected = example?.expected_output;
const resultContains = example?.result_contains;
let match = false;
if (res.result.success) {
if (resultContains !== undefined) {
const resultStr = typeof res.result.result === 'string'
? res.result.result
: JSON.stringify(res.result.result);
match = resultStr.includes(String(resultContains));
}
else if (expected === undefined) {
match = true;
}
else {
match = JSON.stringify(res.result.result) === JSON.stringify(expected);
}
}
if (match)
passed += 1;
return {
case: index + 1,
name: res.name,
input: example?.input,
result: res.result.result,
error: res.result.error,
expected: resultContains !== undefined ? `contains: ${resultContains}` : expected,
match,
};
});
return { total: summaryResults.length, passed, results: summaryResults };
}
/**
* Build CLM banner for display.
*/
buildCLMBanner(clm) {
return {
header: [
`--- Executing ${clm.chapter.title} ---`,
`🧊 CLM Cube:`,
` - Abstract (Why): ${clm.clm.abstract?.concept || 'Unknown'}`,
` - Concrete (How): ${clm.clm.concrete?.manifestation || 'Unknown'}`,
` - Balanced (What): ${clm.clm.balanced?.expectation || 'Unknown'}`,
],
};
}
/**
* Build execution report.
*/
buildExecutionReport(clm, execution) {
return {
status: execution.success ? 'success' : 'failure',
result: execution.result,
error: execution.error,
chapter_id: clm.chapter.id,
chapter_title: clm.chapter.title,
};
}
/**
* Build summary report.
*/
buildSummaryReport(clm, summary) {
return {
status: summary.passed === summary.total ? 'success' : 'failure',
result: {
success: summary.passed === summary.total,
total: summary.total,
results: summary.results.map((r) => ({
case: r.case,
name: r.name,
result: r.result,
error: r.error,
})),
},
chapter_id: clm.chapter.id,
chapter_title: clm.chapter.title,
};
}
// ─── Private Execution Methods ───────────────────────────────────────────
async executeLoader(runtimeName, config, clm, chapterDir, input, startTime) {
const targetRuntime = (runtimeName === 'collection_loader') ? 'collection_loader' : 'loader';
const loaderRuntime = RuntimeFactory.getRuntime(targetRuntime);
const loaderContext = buildBaseContext(clm, input);
const result = await loaderRuntime.execute('', loaderContext, config, chapterDir);
return buildExecutionResult(true, result, startTime, clm);
}
async executeNetwork(config, clm, chapterDir, input, startTime) {
const networkRuntime = RuntimeFactory.getRuntime('network', { collection: this.collection });
const networkContext = this.buildNetworkContext(clm, input, chapterDir);
const result = await networkRuntime.execute('', networkContext, config, chapterDir);
return buildExecutionResult(true, result, startTime, clm);
}
async executeStandard(runtimeName, config, clm, chapterDir, input, startTime) {
const runtime = RuntimeFactory.getRuntime(runtimeName, { collection: this.collection });
const codeOrPath = this.resolveCodeOrPath(runtimeName, config, clm, chapterDir, input);
if (!codeOrPath) {
if (runtimeName === 'lambda')
throw new Error(`Lambda runtime requires input expression or term`);
throw new Error(`Execution source not found for runtime ${runtimeName}`);
}
let executionContext = input;
if (runtimeName === 'javascript') {
executionContext = this.buildJavaScriptContext(input, chapterDir);
}
// Lambda runtime needs longer timeout due to async overhead vs Python's sync implementation
// Allow CLM config to override with explicit maxTimeMs, otherwise use 30s for lambda
const lambdaTimeout = config.maxTimeMs || config.max_time_ms || 30000;
// Pass io_effects from CLM config to lambda runtime
const ioEffects = config.io_effects || clm.clm?.concrete?.io_effects;
const effectiveConfig = runtimeName === 'lambda'
? { ...config, maxTimeMs: lambdaTimeout, io_effects: ioEffects }
: config;
let result = await runtime.execute(codeOrPath, executionContext, effectiveConfig, chapterDir);
// Unwrap LambdaRuntimeResult to get the actual value for test comparisons
if (runtimeName === 'lambda' && result && typeof result === 'object') {
const lambdaRes = result;
if (lambdaRes.success !== undefined) {
if (!lambdaRes.success) {
return buildExecutionResult(false, null, startTime, clm, lambdaRes.error);
}
// Prefer prettyPrint for term-returning operations, otherwise use result
result = lambdaRes.prettyPrint || lambdaRes.result;
}
}
return buildExecutionResult(true, result, startTime, clm);
}
async executeRecursive(runtimePath, config, chapterDir, input, originalClm) {
const startTime = Date.now();
const boundary = config.boundary || 'intrinsic';
try {
const metaClmPath = path.resolve(chapterDir, runtimePath);
const metaClm = this.loader.load(metaClmPath);
const metaDir = path.dirname(metaClmPath);
const metaContext = {
...asObject(input),
source_pcard_title: originalClm.chapter.title,
concrete: originalClm.clm.concrete,
abstract: originalClm.clm.abstract,
__input_content__: input
};
let metaResult;
if (boundary === 'extrinsic') {
const isolatedRunner = new CLMRunner(this.loader.basePath, this.timeout, this.collection);
metaResult = await isolatedRunner.executeCLM(metaClm, metaDir, metaContext);
}
else {
metaResult = await this.executeCLM(metaClm, metaDir, metaContext);
}
return {
success: metaResult.success,
result: metaResult.result,
error: metaResult.error,
executionTime: Date.now() - startTime + metaResult.executionTime,
clm: {
chapter: originalClm.chapter.title,
concept: originalClm.clm.abstract?.concept || 'Unknown',
manifestation: originalClm.clm.concrete?.manifestation || 'Unknown',
boundary
}
};
}
catch (e) {
return buildExecutionResult(false, undefined, startTime, originalClm, e instanceof Error ? e.message : String(e), boundary);
}
}
// ─── Private Helpers ─────────────────────────────────────────────────────
resolveCodeOrPath(runtimeName, config, clm, chapterDir, input) {
if (['rust', 'c'].includes(runtimeName)) {
let binaryPath = config.binary_path;
if (!binaryPath && config.runtimes_config) {
const rc = config.runtimes_config.find((r) => r.name === runtimeName);
binaryPath = rc?.binary;
}
return binaryPath || null;
}
if (runtimeName === 'wasm') {
let mod = config.wasm_module;
if (!mod && config.runtimes_config) {
const rc = config.runtimes_config.find((r) => r.name === runtimeName);
mod = rc?.module;
}
return mod || null;
}
if (runtimeName === 'llm')
return "llm-prompt";
if (runtimeName === 'lambda') {
const inputObj = asObject(input);
return inputObj.expression || inputObj.term || "lambda-op";
}
return this.loader.loadLogicFile(clm, chapterDir);
}
buildNetworkContext(clm, input, chapterDir) {
const baseContext = buildBaseContext(clm, input);
return {
...baseContext,
input,
user_input: input,
secrets: process.env,
process,
runCLM: this.buildRunCLM(chapterDir),
};
}
buildJavaScriptContext(input, chapterDir) {
return {
...asObject(input),
console,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
process,
runCLM: async (clmFile, clmInput) => {
const fullPath = path.resolve(chapterDir, clmFile);
const res = await this.runFile(fullPath, clmInput);
if (!res.success)
throw new Error(res.error || 'CLM execution failed');
return res.result;
}
};
}
buildRunCLM(chapterDir) {
return async (clmFile, clmInput) => {
const fullPath = path.resolve(chapterDir, clmFile);
return this.runFile(fullPath, clmInput);
};
}
}
//# sourceMappingURL=runner.js.map