validate method
ValidationResult
validate(
- ValidatorAction action,
- Observation observation,
- List<
ToolDescriptor> tools
Validate action against tools and the live UI in observation.
Implementation
ValidationResult validate(
ValidatorAction action,
Observation observation,
List<ToolDescriptor> tools,
) {
// Pass 1: tool known?
ToolDescriptor? tool;
for (final t in tools) {
if (t.name == action.tool) {
tool = t;
break;
}
}
if (tool == null) {
return ValidationReject(
tool: action.tool,
reason: 'unknown_tool',
expected: tools.map((t) => t.name).toList(growable: false),
got: action.tool,
description: 'tool "${action.tool}" is not available this turn',
);
}
// Pass 2: args validate against inputSchema?
final schema = JsonSchema.create(
tool.inputSchema,
schemaVersion: SchemaVersion.draft7,
);
final result = schema.validate(action.args);
if (!result.isValid) {
final err = result.errors.first;
final pointer = err.instancePath.isEmpty ? '/' : err.instancePath;
return ValidationReject(
tool: action.tool,
reason: 'schema_invalid',
pointer: pointer,
description: err.message,
);
}
// Pass 3: scenario-declared core.done reason form.
final RegExp? donePattern = doneReasonPattern;
if (donePattern != null && action.tool == _kCoreDoneTool) {
final Object? rawReason = action.args['reason'];
final String reason = rawReason is String ? rawReason : '';
if (!donePattern.hasMatch(reason)) {
return ValidationReject(
tool: action.tool,
reason: 'done_reason_mismatch',
pointer: '/reason',
expected: <String>[donePattern.pattern],
got: reason,
description:
'core.done reason must match ${donePattern.pattern}; copy the '
'index and tool from the row you actually observed, and keep '
'acting until you have observed it',
);
}
}
// Pass 4: scenario-declared done evidence, read from the observation.
final RegExp? evidencePattern = doneEvidencePattern;
if (evidencePattern != null && action.tool == _kCoreDoneTool) {
final List<({String text, RegExpMatch match})> rows =
<({String text, RegExpMatch match})>[];
for (final SemanticsNode node in observation.core.nodes.values) {
for (final String text in <String>[node.label, node.value]) {
if (text.isEmpty) continue;
final RegExpMatch? match = evidencePattern.firstMatch(text);
if (match != null) rows.add((text: text, match: match));
}
}
if (rows.isEmpty) {
return ValidationReject(
tool: action.tool,
reason: 'done_evidence_missing',
expected: <String>[evidencePattern.pattern],
description:
'core.done needs a node whose label or value matches '
'${evidencePattern.pattern}; the current observation carries '
'none, so keep acting until that evidence is on screen',
);
}
final Object? rawEvidenceReason = action.args['reason'];
final String evidenceReason = rawEvidenceReason is String
? rawEvidenceReason
: '';
final bool quotesObservedRow = rows.any(
(({String text, RegExpMatch match}) row) =>
_reasonQuotesRow(evidenceReason, row.match),
);
if (!quotesObservedRow) {
return ValidationReject(
tool: action.tool,
reason: 'done_evidence_mismatch',
pointer: '/reason',
expected: <String>[
for (final ({String text, RegExpMatch match}) row in rows) row.text,
],
got: evidenceReason,
description:
'core.done reason must quote a row that is actually observed; '
'copy the index and tool from one of the listed rows',
);
}
}
// Pass 5: core-tool semantic-node check.
if (!_coreNodeTools.contains(action.tool)) {
// Extension tools and node-less core tools (system_back / wait /
// done) need no semantic-node check.
return const ValidationOk();
}
final argKeys = _nodeArgKeys[action.tool] ?? const <String>[];
for (final argKey in argKeys) {
final v = action.args[argKey];
if (v is! int) {
// Schema validation already enforced int-ness; defensive skip.
continue;
}
final node = observation.core.nodes[v];
if (node == null) {
return ValidationReject(
tool: action.tool,
reason: 'node_not_found',
pointer: '/$argKey',
got: v,
description: '$argKey=$v is not present in observation.core.nodes',
);
}
if (node.state.contains('disabled')) {
return ValidationReject(
tool: action.tool,
reason: 'node_disabled',
pointer: '/$argKey',
got: v,
description: '$argKey=$v is disabled (state: ${node.state})',
);
}
}
return const ValidationOk();
}