chore: add ion package, codesight wiki, work plans, ascli config
New @boocode/ion package (v0.0.1) for inference optimization network. .codesight/ wiki artifacts for codebase documentation. .omo/ work plans for openspec cleanup and enhanced file panel.
This commit is contained in:
122
packages/ion/src/engine/output-ref.ts
Normal file
122
packages/ion/src/engine/output-ref.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Node output reference resolution for the Ion workflow engine.
|
||||
*
|
||||
* Resolves `$nodeId.field` references in workflow conditions and prompts,
|
||||
* with strict schema-aware validation and descriptive errors.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output reference result
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type OutputRefKind = 'value' | 'empty';
|
||||
|
||||
export interface OutputRefResult {
|
||||
/** Whether the field had a value or was empty. */
|
||||
kind: OutputRefKind;
|
||||
/** The resolved value (empty string for missing optional fields). */
|
||||
value: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OutputRefError
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class OutputRefError extends Error {
|
||||
public readonly nodeId: string;
|
||||
public readonly field: string;
|
||||
|
||||
constructor(nodeId: string, field: string, message: string) {
|
||||
super(`Output reference error for node "${nodeId}".${field}: ${message}`);
|
||||
this.name = 'OutputRefError';
|
||||
this.nodeId = nodeId;
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extract declared field names from an output_format schema.
|
||||
*
|
||||
* The output_format can be:
|
||||
* - A JSON Schema object with `properties` (standard)
|
||||
* - A string describing the format (treated as having no declared fields)
|
||||
* - Undefined (no schema)
|
||||
*
|
||||
* Returns a Set of field names that are declared in the schema.
|
||||
*/
|
||||
export function declaredFieldsFromSchema(
|
||||
outputFormat: Record<string, unknown> | string | undefined,
|
||||
): Set<string> {
|
||||
if (!outputFormat || typeof outputFormat === 'string') {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const properties = outputFormat['properties'];
|
||||
if (properties && typeof properties === 'object' && properties !== null) {
|
||||
return new Set(Object.keys(properties as Record<string, unknown>));
|
||||
}
|
||||
|
||||
return new Set();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Node output resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolve a specific field from a node's output.
|
||||
*
|
||||
* Behavior:
|
||||
* - If the field is present in the output, returns `{ kind: 'value', value }`.
|
||||
* - If the field is declared in the schema but not present in the output,
|
||||
* returns `{ kind: 'empty', value: '' }` (optional field not set).
|
||||
* - If the field is NOT declared in the schema AND not in the output,
|
||||
* throws `OutputRefError` (undeclared reference).
|
||||
* - If the field is NOT declared in the schema but IS in the output,
|
||||
* returns `{ kind: 'value', value }` (dynamic output).
|
||||
*
|
||||
* The `declaredFields` parameter should come from `declaredFieldsFromSchema()`.
|
||||
*/
|
||||
export function resolveNodeOutputField(
|
||||
nodeOutput: Record<string, unknown>,
|
||||
nodeId: string,
|
||||
field: string,
|
||||
declaredFields?: Set<string>,
|
||||
): OutputRefResult {
|
||||
// Check if the field exists in the output.
|
||||
if (field in nodeOutput) {
|
||||
const rawValue = nodeOutput[field];
|
||||
|
||||
// Convert the value to a string.
|
||||
if (rawValue === null || rawValue === undefined) {
|
||||
// Field key exists but value is nullish — treat as empty.
|
||||
return { kind: 'empty', value: '' };
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'string') {
|
||||
return { kind: 'value', value: rawValue };
|
||||
}
|
||||
|
||||
// Non-string values are JSON-serialized.
|
||||
return { kind: 'value', value: JSON.stringify(rawValue) };
|
||||
}
|
||||
|
||||
// Field is not in the output. Check if it was declared in the schema.
|
||||
const isDeclared = declaredFields?.has(field) ?? false;
|
||||
|
||||
if (isDeclared) {
|
||||
// Declared but not present — optional field not set.
|
||||
return { kind: 'empty', value: '' };
|
||||
}
|
||||
|
||||
// Not declared and not present — this is an error.
|
||||
throw new OutputRefError(
|
||||
nodeId,
|
||||
field,
|
||||
`Field "${field}" is not declared in the output schema and is not present in the node output. Available fields: ${Object.keys(nodeOutput).join(', ') || '(none)'}`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user