feat: add ABM pipeline contracts and validation
This commit is contained in:
@@ -3,7 +3,11 @@ import { Link, useLocation } from 'react-router-dom';
|
|||||||
import { LayoutGrid, Home, Edit3, Activity, ChevronDown, Globe } from 'lucide-react';
|
import { LayoutGrid, Home, Edit3, Activity, ChevronDown, Globe } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
|
||||||
const ModuleSwitcher: React.FC = () => {
|
interface ModuleSwitcherProps {
|
||||||
|
variant?: 'light' | 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModuleSwitcher: React.FC<ModuleSwitcherProps> = ({ variant = 'light' }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@@ -20,11 +24,15 @@ const ModuleSwitcher: React.FC = () => {
|
|||||||
<div className="relative z-[100]">
|
<div className="relative z-[100]">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="flex items-center gap-2 px-3 py-1.5 bg-white hover:bg-slate-50 rounded-xl border border-slate-200 shadow-sm transition-all text-slate-800 active:scale-95"
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-xl border shadow-sm transition-all active:scale-95 ${
|
||||||
|
variant === 'dark'
|
||||||
|
? 'bg-blue-600 hover:bg-blue-500 border-blue-500 text-white shadow-blue-950/30'
|
||||||
|
: 'bg-white hover:bg-slate-50 border-slate-200 text-slate-800'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<LayoutGrid className="w-4 h-4 text-blue-600" />
|
<LayoutGrid className={`w-4 h-4 ${variant === 'dark' ? 'text-white' : 'text-blue-600'}`} />
|
||||||
<span className="text-sm font-black tracking-tight">{currentModule.name}</span>
|
<span className="text-sm font-black tracking-tight">{currentModule.name}</span>
|
||||||
<ChevronDown className={`w-3 h-3 text-slate-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
<ChevronDown className={`w-3 h-3 transition-transform ${variant === 'dark' ? 'text-blue-100' : 'text-slate-400'} ${isOpen ? 'rotate-180' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|||||||
+1012
-103
File diff suppressed because it is too large
Load Diff
+101
-1
@@ -1,5 +1,16 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createDefaultWorkflow, getBehaviorSectionsForNode, getNodeConnectionAnchor, getParametersForNode } from './workflowModel';
|
import {
|
||||||
|
buildRuntimeConfig,
|
||||||
|
createDefaultWorkflow,
|
||||||
|
ensureValidationChecklist,
|
||||||
|
getBehaviorSectionsForNode,
|
||||||
|
getInputOutputsForEdge,
|
||||||
|
getInputOutputsForNode,
|
||||||
|
getNodeConnectionAnchor,
|
||||||
|
getNodeStatusFromValidation,
|
||||||
|
getParametersForNode,
|
||||||
|
getValidationsForNode,
|
||||||
|
} from './workflowModel';
|
||||||
|
|
||||||
const baseWorkflow = createDefaultWorkflow();
|
const baseWorkflow = createDefaultWorkflow();
|
||||||
|
|
||||||
@@ -10,6 +21,44 @@ assert.deepEqual(
|
|||||||
|
|
||||||
assert.equal(baseWorkflow.edges.length, 1);
|
assert.equal(baseWorkflow.edges.length, 1);
|
||||||
|
|
||||||
|
assert.equal(baseWorkflow.schedules.length, 3);
|
||||||
|
assert.equal(baseWorkflow.schedules[0].id, 'S001');
|
||||||
|
assert.equal(baseWorkflow.schedules[0].actorAgent, 'Environment');
|
||||||
|
|
||||||
|
assert.equal(baseWorkflow.associatedData.length, 3);
|
||||||
|
assert.equal(baseWorkflow.associatedData[0].sourceTable, 'model_initial_state');
|
||||||
|
|
||||||
|
assert.equal(baseWorkflow.inputOutputs.length, 3);
|
||||||
|
assert.equal(baseWorkflow.inputOutputs[0].snapshotPolicy, 'run_start');
|
||||||
|
|
||||||
|
assert.equal(baseWorkflow.validations.length, 3);
|
||||||
|
assert.equal(baseWorkflow.validations[0].severity, 'error');
|
||||||
|
|
||||||
|
const runtimeConfig = buildRuntimeConfig(baseWorkflow);
|
||||||
|
|
||||||
|
assert.equal(runtimeConfig.runtime.version, 'workflow-runtime-v1');
|
||||||
|
assert.equal(runtimeConfig.runtime.ioContracts.length, 3);
|
||||||
|
assert.deepEqual(runtimeConfig.runtime.ioContracts[0], {
|
||||||
|
id: 'IO001',
|
||||||
|
direction: 'input',
|
||||||
|
name: 'self.position',
|
||||||
|
dataType: 'Coordinate',
|
||||||
|
schema: '{ x: number, y: number }',
|
||||||
|
source: {
|
||||||
|
component: 'Grid Space',
|
||||||
|
table: 'space_cell_map',
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
component: 'Example Agent',
|
||||||
|
table: '-',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
defaultValue: null,
|
||||||
|
snapshotPolicy: 'run_start',
|
||||||
|
});
|
||||||
|
assert.equal(runtimeConfig.runtime.schedule[0].id, 'S001');
|
||||||
|
assert.equal(runtimeConfig.runtime.validation[0].severity, 'error');
|
||||||
|
|
||||||
const gridWorkflow = createDefaultWorkflow('grid');
|
const gridWorkflow = createDefaultWorkflow('grid');
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -19,6 +68,57 @@ assert.deepEqual(
|
|||||||
|
|
||||||
assert.equal(gridWorkflow.edges.length, 2);
|
assert.equal(gridWorkflow.edges.length, 2);
|
||||||
|
|
||||||
|
const spaceToAgentContracts = getInputOutputsForEdge(gridWorkflow, 'e2');
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
spaceToAgentContracts.map((row) => row.ioName),
|
||||||
|
['self.position'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const agentContracts = getInputOutputsForNode(gridWorkflow, 'example-agent');
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
agentContracts.map((row) => row.ioName),
|
||||||
|
['self.position', 'neighbor.race', 'self.happiness'],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(getNodeStatusFromValidation(gridWorkflow, 'grid-space'), 'error');
|
||||||
|
assert.equal(getNodeStatusFromValidation(gridWorkflow, 'example-agent'), 'ready');
|
||||||
|
|
||||||
|
const gridValidations = getValidationsForNode(gridWorkflow, 'grid-space');
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
gridValidations.map((row) => row.id),
|
||||||
|
['V001'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const generatedWorkflow = ensureValidationChecklist({
|
||||||
|
...gridWorkflow,
|
||||||
|
nodes: [
|
||||||
|
...gridWorkflow.nodes,
|
||||||
|
{
|
||||||
|
id: 'ai-output',
|
||||||
|
title: 'AI Output',
|
||||||
|
subtitle: 'Generated report output',
|
||||||
|
kind: 'output',
|
||||||
|
x: 360,
|
||||||
|
y: 120,
|
||||||
|
status: 'ready',
|
||||||
|
inputs: [{ id: 'report', label: 'report input', type: 'REPORT' }],
|
||||||
|
outputs: [],
|
||||||
|
config: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
validations: gridWorkflow.validations,
|
||||||
|
});
|
||||||
|
|
||||||
|
const generatedNodeValidations = getValidationsForNode(generatedWorkflow, 'ai-output');
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
generatedNodeValidations.map((row) => row.method),
|
||||||
|
['node_config_present', 'io_contract_coverage'],
|
||||||
|
);
|
||||||
|
|
||||||
const parameterRows = getParametersForNode(gridWorkflow, 'example-agent');
|
const parameterRows = getParametersForNode(gridWorkflow, 'example-agent');
|
||||||
|
|
||||||
assert.equal(parameterRows.length, 3);
|
assert.equal(parameterRows.length, 3);
|
||||||
|
|||||||
@@ -67,11 +67,82 @@ export interface BehaviorSection {
|
|||||||
items: Array<{ label: string; source: string; value: string }>;
|
items: Array<{ label: string; source: string; value: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScheduleRow {
|
||||||
|
id: string;
|
||||||
|
order: string;
|
||||||
|
phase: string;
|
||||||
|
actorAgent: string;
|
||||||
|
targetBehavior: string;
|
||||||
|
trigger: string;
|
||||||
|
executionMode: string;
|
||||||
|
timeUnit: string;
|
||||||
|
condition: string;
|
||||||
|
repeatRule: string;
|
||||||
|
dependsOn: string;
|
||||||
|
writesTo: string;
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssociatedDataRow {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
sourceType: string;
|
||||||
|
sourceName: string;
|
||||||
|
sourceTable: string;
|
||||||
|
sourceColumn: string;
|
||||||
|
ownerAgent: string;
|
||||||
|
relatedVariable: string;
|
||||||
|
usedByBehavior: string;
|
||||||
|
updateFrequency: string;
|
||||||
|
versionKey: string;
|
||||||
|
qualityStatus: 'ok' | 'warning' | 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputOutputRow {
|
||||||
|
id: string;
|
||||||
|
componentType: string;
|
||||||
|
componentId: string;
|
||||||
|
componentName: string;
|
||||||
|
direction: 'input' | 'output';
|
||||||
|
ioName: string;
|
||||||
|
ioDataType: string;
|
||||||
|
schema: string;
|
||||||
|
sourceComponent: string;
|
||||||
|
sourceTable: string;
|
||||||
|
targetComponent: string;
|
||||||
|
targetTable: string;
|
||||||
|
required: string;
|
||||||
|
defaultValue: string;
|
||||||
|
snapshotPolicy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationRow {
|
||||||
|
id: string;
|
||||||
|
targetType: string;
|
||||||
|
targetId: string;
|
||||||
|
targetName: string;
|
||||||
|
validationType: string;
|
||||||
|
method: string;
|
||||||
|
metric: string;
|
||||||
|
baselineData: string;
|
||||||
|
threshold: string;
|
||||||
|
frequency: string;
|
||||||
|
resultField: string;
|
||||||
|
severity: 'error' | 'warning' | 'info';
|
||||||
|
status: 'ok' | 'warning' | 'error';
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkflowModel {
|
export interface WorkflowModel {
|
||||||
nodes: WorkflowNode[];
|
nodes: WorkflowNode[];
|
||||||
edges: WorkflowEdge[];
|
edges: WorkflowEdge[];
|
||||||
parameters: WorkflowParameter[];
|
parameters: WorkflowParameter[];
|
||||||
behaviorSections: BehaviorSection[];
|
behaviorSections: BehaviorSection[];
|
||||||
|
schedules: ScheduleRow[];
|
||||||
|
associatedData: AssociatedDataRow[];
|
||||||
|
inputOutputs: InputOutputRow[];
|
||||||
|
validations: ValidationRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InitialSpaceType = 'grid' | 'network' | 'continuous' | 'gis';
|
export type InitialSpaceType = 'grid' | 'network' | 'continuous' | 'gis';
|
||||||
@@ -308,6 +379,203 @@ export function createDefaultWorkflow(spaceType?: InitialSpaceType): WorkflowMod
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
schedules: [
|
||||||
|
{
|
||||||
|
id: 'S001',
|
||||||
|
order: '1',
|
||||||
|
phase: 'initialize',
|
||||||
|
actorAgent: 'Environment',
|
||||||
|
targetBehavior: 'create_context',
|
||||||
|
trigger: 'model_start',
|
||||||
|
executionMode: 'sync',
|
||||||
|
timeUnit: 'tick',
|
||||||
|
condition: 'always',
|
||||||
|
repeatRule: 'once',
|
||||||
|
dependsOn: '-',
|
||||||
|
writesTo: 'ENV_CONTEXT',
|
||||||
|
notes: 'Create shared model context',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S002',
|
||||||
|
order: '2',
|
||||||
|
phase: 'initialize',
|
||||||
|
actorAgent: 'Grid Space',
|
||||||
|
targetBehavior: 'allocate_cells',
|
||||||
|
trigger: 'space_ready',
|
||||||
|
executionMode: 'sync',
|
||||||
|
timeUnit: 'tick',
|
||||||
|
condition: 'environment_ready',
|
||||||
|
repeatRule: 'once',
|
||||||
|
dependsOn: 'S001',
|
||||||
|
writesTo: 'SPACE_CONTEXT',
|
||||||
|
notes: 'Initialize spatial container',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S003',
|
||||||
|
order: '3',
|
||||||
|
phase: 'daily',
|
||||||
|
actorAgent: 'Example Agent',
|
||||||
|
targetBehavior: 'on_sensing',
|
||||||
|
trigger: 'every_tick',
|
||||||
|
executionMode: 'sync',
|
||||||
|
timeUnit: 'day',
|
||||||
|
condition: 'neighbor_radius valid',
|
||||||
|
repeatRule: 'every tick',
|
||||||
|
dependsOn: 'S002',
|
||||||
|
writesTo: 'happiness',
|
||||||
|
notes: 'Observe neighbors and update agent state',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
associatedData: [
|
||||||
|
{
|
||||||
|
id: 'D001',
|
||||||
|
name: 'agent_group_seed',
|
||||||
|
type: 'parameter',
|
||||||
|
sourceType: 'initialization',
|
||||||
|
sourceName: 'default workflow',
|
||||||
|
sourceTable: 'model_initial_state',
|
||||||
|
sourceColumn: 'race',
|
||||||
|
ownerAgent: 'Example Agent',
|
||||||
|
relatedVariable: 'race',
|
||||||
|
usedByBehavior: 'on_sensing',
|
||||||
|
updateFrequency: 'model start',
|
||||||
|
versionKey: 'workflow_v3',
|
||||||
|
qualityStatus: 'ok',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'D002',
|
||||||
|
name: 'grid_position_map',
|
||||||
|
type: 'derived_data',
|
||||||
|
sourceType: 'space connection',
|
||||||
|
sourceName: 'Grid Space',
|
||||||
|
sourceTable: 'space_cell_map',
|
||||||
|
sourceColumn: 'x,y',
|
||||||
|
ownerAgent: 'Example Agent',
|
||||||
|
relatedVariable: 'position',
|
||||||
|
usedByBehavior: 'on_sensing',
|
||||||
|
updateFrequency: 'every tick',
|
||||||
|
versionKey: 'space_config_v1',
|
||||||
|
qualityStatus: 'ok',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'D003',
|
||||||
|
name: 'neighbor_lookup_radius',
|
||||||
|
type: 'space_config',
|
||||||
|
sourceType: 'manual config',
|
||||||
|
sourceName: 'Grid Space',
|
||||||
|
sourceTable: 'space_parameters',
|
||||||
|
sourceColumn: 'neighbor_radius',
|
||||||
|
ownerAgent: 'Grid Space',
|
||||||
|
relatedVariable: 'neighbor_radius',
|
||||||
|
usedByBehavior: 'on_sensing',
|
||||||
|
updateFrequency: 'model start',
|
||||||
|
versionKey: 'missing',
|
||||||
|
qualityStatus: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputOutputs: [
|
||||||
|
{
|
||||||
|
id: 'IO001',
|
||||||
|
componentType: 'Behavior',
|
||||||
|
componentId: 'on_sensing',
|
||||||
|
componentName: 'on_sensing',
|
||||||
|
direction: 'input',
|
||||||
|
ioName: 'self.position',
|
||||||
|
ioDataType: 'Coordinate',
|
||||||
|
schema: '{ x: number, y: number }',
|
||||||
|
sourceComponent: 'Grid Space',
|
||||||
|
sourceTable: 'space_cell_map',
|
||||||
|
targetComponent: 'Example Agent',
|
||||||
|
targetTable: '-',
|
||||||
|
required: 'true',
|
||||||
|
defaultValue: '-',
|
||||||
|
snapshotPolicy: 'run_start',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'IO002',
|
||||||
|
componentType: 'Behavior',
|
||||||
|
componentId: 'on_sensing',
|
||||||
|
componentName: 'on_sensing',
|
||||||
|
direction: 'input',
|
||||||
|
ioName: 'neighbor.race',
|
||||||
|
ioDataType: 'String',
|
||||||
|
schema: 'A | B',
|
||||||
|
sourceComponent: 'Observable pool',
|
||||||
|
sourceTable: 'agent_state',
|
||||||
|
targetComponent: 'Example Agent',
|
||||||
|
targetTable: '-',
|
||||||
|
required: 'true',
|
||||||
|
defaultValue: '-',
|
||||||
|
snapshotPolicy: 'each_tick',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'IO003',
|
||||||
|
componentType: 'Behavior',
|
||||||
|
componentId: 'on_sensing',
|
||||||
|
componentName: 'on_sensing',
|
||||||
|
direction: 'output',
|
||||||
|
ioName: 'self.happiness',
|
||||||
|
ioDataType: 'Boolean',
|
||||||
|
schema: 'true | false',
|
||||||
|
sourceComponent: 'on_sensing',
|
||||||
|
sourceTable: '-',
|
||||||
|
targetComponent: 'Example Agent',
|
||||||
|
targetTable: 'agent_state',
|
||||||
|
required: 'true',
|
||||||
|
defaultValue: 'false',
|
||||||
|
snapshotPolicy: 'each_tick',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
validations: [
|
||||||
|
{
|
||||||
|
id: 'V001',
|
||||||
|
targetType: 'Variable',
|
||||||
|
targetId: 's2',
|
||||||
|
targetName: 'neighbor_radius',
|
||||||
|
validationType: 'schema_check',
|
||||||
|
method: 'required_value',
|
||||||
|
metric: 'missing_count',
|
||||||
|
baselineData: 'space_parameters.neighbor_radius',
|
||||||
|
threshold: '= 0',
|
||||||
|
frequency: 'model start',
|
||||||
|
resultField: 'validation.required_missing',
|
||||||
|
severity: 'error',
|
||||||
|
status: 'error',
|
||||||
|
notes: 'Required for on_sensing neighborhood lookup',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'V002',
|
||||||
|
targetType: 'Behavior',
|
||||||
|
targetId: 'on_sensing',
|
||||||
|
targetName: 'on_sensing',
|
||||||
|
validationType: 'rule_check',
|
||||||
|
method: 'output_assignment',
|
||||||
|
metric: 'assigned',
|
||||||
|
baselineData: 'self.happiness',
|
||||||
|
threshold: 'true',
|
||||||
|
frequency: 'every tick',
|
||||||
|
resultField: 'behavior.output_valid',
|
||||||
|
severity: 'error',
|
||||||
|
status: 'ok',
|
||||||
|
notes: 'Behavior must write a Boolean result to the agent state',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'V003',
|
||||||
|
targetType: 'Workflow',
|
||||||
|
targetId: 'workflow_v3',
|
||||||
|
targetName: 'workflow',
|
||||||
|
validationType: 'reproducibility',
|
||||||
|
method: 'input_snapshot',
|
||||||
|
metric: 'coverage',
|
||||||
|
baselineData: 'all required I/O rows',
|
||||||
|
threshold: '100%',
|
||||||
|
frequency: 'before run',
|
||||||
|
resultField: 'run.snapshot_coverage',
|
||||||
|
severity: 'warning',
|
||||||
|
status: 'warning',
|
||||||
|
notes: 'All required inputs should be captured for reproduction',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +587,171 @@ export function getBehaviorSectionsForNode(workflow: WorkflowModel, nodeId: stri
|
|||||||
return workflow.behaviorSections.filter((section) => section.nodeId === nodeId);
|
return workflow.behaviorSections.filter((section) => section.nodeId === nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInputOutputsForEdge(workflow: WorkflowModel, edgeId: string) {
|
||||||
|
const edge = workflow.edges.find((workflowEdge) => workflowEdge.id === edgeId);
|
||||||
|
if (!edge) return [];
|
||||||
|
|
||||||
|
const sourceNode = workflow.nodes.find((node) => node.id === edge.source);
|
||||||
|
const targetNode = workflow.nodes.find((node) => node.id === edge.target);
|
||||||
|
if (!sourceNode || !targetNode) return [];
|
||||||
|
|
||||||
|
return workflow.inputOutputs.filter((row) => (
|
||||||
|
row.sourceComponent === sourceNode.title
|
||||||
|
&& row.targetComponent === targetNode.title
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInputOutputsForNode(workflow: WorkflowModel, nodeId: string) {
|
||||||
|
const node = workflow.nodes.find((workflowNode) => workflowNode.id === nodeId);
|
||||||
|
if (!node) return [];
|
||||||
|
|
||||||
|
return workflow.inputOutputs.filter((row) => (
|
||||||
|
row.sourceComponent === node.title
|
||||||
|
|| row.targetComponent === node.title
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validationBelongsToNode(workflow: WorkflowModel, validation: ValidationRow, node: WorkflowNode) {
|
||||||
|
if (validation.targetId === node.id || validation.targetName === node.title) return true;
|
||||||
|
|
||||||
|
if (validation.targetType === 'Variable') {
|
||||||
|
return workflow.parameters.some((parameter) => (
|
||||||
|
parameter.nodeId === node.id
|
||||||
|
&& (parameter.id === validation.targetId || parameter.name === validation.targetName)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.targetType === 'Behavior') {
|
||||||
|
return workflow.behaviorSections.some((section) => (
|
||||||
|
section.nodeId === node.id
|
||||||
|
&& (section.id === validation.targetId || section.title === validation.targetName || validation.targetId === 'on_sensing')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValidationsForNode(workflow: WorkflowModel, nodeId: string) {
|
||||||
|
const node = workflow.nodes.find((workflowNode) => workflowNode.id === nodeId);
|
||||||
|
if (!node) return [];
|
||||||
|
|
||||||
|
return workflow.validations.filter((validation) => validationBelongsToNode(workflow, validation, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNodeValidationChecklist(workflow: WorkflowModel, node: WorkflowNode): ValidationRow[] {
|
||||||
|
const hasIoContract = workflow.inputOutputs.some((row) => (
|
||||||
|
row.sourceComponent === node.title
|
||||||
|
|| row.targetComponent === node.title
|
||||||
|
));
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `AUTO-${node.id}-CONFIG`,
|
||||||
|
targetType: 'Node',
|
||||||
|
targetId: node.id,
|
||||||
|
targetName: node.title,
|
||||||
|
validationType: 'schema_check',
|
||||||
|
method: 'node_config_present',
|
||||||
|
metric: 'config_items',
|
||||||
|
baselineData: `${node.id}.config`,
|
||||||
|
threshold: '> 0',
|
||||||
|
frequency: 'before run',
|
||||||
|
resultField: `validation.${node.id}.config_present`,
|
||||||
|
severity: 'warning',
|
||||||
|
status: node.config.length > 0 ? 'ok' : 'warning',
|
||||||
|
notes: 'Generated checklist item: node should have explicit configuration before running.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `AUTO-${node.id}-IO`,
|
||||||
|
targetType: 'Node',
|
||||||
|
targetId: node.id,
|
||||||
|
targetName: node.title,
|
||||||
|
validationType: 'contract_check',
|
||||||
|
method: 'io_contract_coverage',
|
||||||
|
metric: 'io_contract_count',
|
||||||
|
baselineData: 'inputOutputs',
|
||||||
|
threshold: node.inputs.length || node.outputs.length ? '> 0' : '>= 0',
|
||||||
|
frequency: 'before run',
|
||||||
|
resultField: `validation.${node.id}.io_contract_coverage`,
|
||||||
|
severity: 'warning',
|
||||||
|
status: hasIoContract || (!node.inputs.length && !node.outputs.length) ? 'ok' : 'warning',
|
||||||
|
notes: 'Generated checklist item: node ports should be represented by I/O contract rows.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureValidationChecklist(workflow: WorkflowModel): WorkflowModel {
|
||||||
|
const existingIds = new Set(workflow.validations.map((validation) => validation.id));
|
||||||
|
const generatedValidations = workflow.nodes.flatMap((node) => (
|
||||||
|
createNodeValidationChecklist(workflow, node).filter((validation) => !existingIds.has(validation.id))
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!generatedValidations.length) return workflow;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...workflow,
|
||||||
|
validations: [...workflow.validations, ...generatedValidations],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNodeStatusFromValidation(workflow: WorkflowModel, nodeId: string): WorkflowNodeStatus {
|
||||||
|
const node = workflow.nodes.find((workflowNode) => workflowNode.id === nodeId);
|
||||||
|
if (!node) return 'ready';
|
||||||
|
|
||||||
|
const relatedValidations = getValidationsForNode(workflow, node.id);
|
||||||
|
|
||||||
|
if (relatedValidations.some((validation) => validation.status === 'error')) return 'error';
|
||||||
|
if (relatedValidations.some((validation) => validation.status === 'warning')) return 'warning';
|
||||||
|
|
||||||
|
return node.status === 'success' ? 'success' : 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRuntimeConfig(workflow: WorkflowModel) {
|
||||||
|
const workflowWithChecklist = ensureValidationChecklist(workflow);
|
||||||
|
|
||||||
|
return {
|
||||||
|
runtime: {
|
||||||
|
version: 'workflow-runtime-v1',
|
||||||
|
model: {
|
||||||
|
nodes: workflowWithChecklist.nodes.map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
kind: node.kind,
|
||||||
|
title: node.title,
|
||||||
|
status: node.status,
|
||||||
|
})),
|
||||||
|
edges: workflowWithChecklist.edges.map((edge) => ({
|
||||||
|
id: edge.id,
|
||||||
|
source: edge.source,
|
||||||
|
target: edge.target,
|
||||||
|
type: edge.type,
|
||||||
|
label: edge.label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
ioContracts: workflowWithChecklist.inputOutputs.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
direction: row.direction,
|
||||||
|
name: row.ioName,
|
||||||
|
dataType: row.ioDataType,
|
||||||
|
schema: row.schema,
|
||||||
|
source: {
|
||||||
|
component: row.sourceComponent,
|
||||||
|
table: row.sourceTable,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
component: row.targetComponent,
|
||||||
|
table: row.targetTable,
|
||||||
|
},
|
||||||
|
required: row.required === 'true',
|
||||||
|
defaultValue: row.defaultValue === '-' ? null : row.defaultValue,
|
||||||
|
snapshotPolicy: row.snapshotPolicy,
|
||||||
|
})),
|
||||||
|
schedule: workflowWithChecklist.schedules,
|
||||||
|
associatedData: workflowWithChecklist.associatedData,
|
||||||
|
validation: workflowWithChecklist.validations,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const NODE_CARD_SIZE = {
|
export const NODE_CARD_SIZE = {
|
||||||
width: 176,
|
width: 176,
|
||||||
height: 128,
|
height: 128,
|
||||||
|
|||||||
Reference in New Issue
Block a user