fix: reject scalar context foreach sources

This commit is contained in:
lda
2026-09-05 06:13:39 +07:00 Verified
parent 2a63c92c17
commit 043bda6a34
6 changed files with 150 additions and 65 deletions
+18 -7
View File
@@ -115,11 +115,7 @@ class SchemaView:
"""Return the first declared homogeneous item schema for an array."""
for cursor in self._cursors:
for branch, _ in _structural_branches(cursor, frozenset()):
schema_type = branch.contents.get("type")
is_array = schema_type == "array" or (
isinstance(schema_type, list) and "array" in schema_type
)
if not is_array:
if not _declares_array(branch.contents):
continue
child = branch.child(branch.contents.get("items"))
if child is not None:
@@ -127,8 +123,16 @@ class SchemaView:
return None
def is_array(self) -> bool:
"""Return whether any structural branch declares an array."""
return self.array_items() is not None
"""Return whether any structural branch declares an array type.
JSON Schema permits arrays without an ``items`` keyword; those are
still arrays whose element schema is unconstrained.
"""
return any(
_declares_array(branch.contents)
for cursor in self._cursors
for branch, _ in _structural_branches(cursor, frozenset())
)
def standalone_schema(self) -> dict[str, Any]:
"""Detach this view as a valid Draft 2020-12 schema resource.
@@ -208,6 +212,13 @@ class SchemaNavigator:
return None, ""
def _declares_array(schema: JsonSchema) -> bool:
schema_type = schema.get("type")
return schema_type == "array" or (
isinstance(schema_type, list) and "array" in schema_type
)
def _structural_branches(
cursor: _Cursor,
active_references: frozenset[ReferenceIdentity],
+25 -1
View File
@@ -79,7 +79,7 @@ def validate_context_paths(
# Context-rooted `over` paths reach this pass; the old
# input/state-only check stays permissive for them.
if node.over.root == "context":
_validate_one_context_path(
_validate_context_foreach_source(
node.over, f"nodes[{idx}].over", node.id, schema, report
)
elif isinstance(node, InterruptNode):
@@ -197,6 +197,30 @@ def _validate_one_context_path(
)
def _validate_context_foreach_source(
path: GraphSourcePath,
location: str,
node_id: str,
schema: ContextSchema | None,
report: ValidationReport,
) -> None:
"""Require an existing context path to declare an array source.
Missing paths keep the more precise ``INVALID_CONTEXT_PATH`` diagnostic;
this adds the foreach-specific error only after the path resolves.
"""
_validate_one_context_path(path, location, node_id, schema, report)
if schema is None:
return
source = SchemaNavigator(schema).at_path(path.parts)
if source is not None and not source.is_array():
report.add(
ValidationIssueCode.INVALID_FOREACH_SOURCE,
location,
f"foreach source {str(path)!r} must resolve to an array",
)
def _failing_segment(
schema: Mapping[str, Any], parts: tuple[str, ...]
) -> tuple[str | None, str]: