docs: publish structured runtime context
This commit is contained in:
@@ -275,9 +275,7 @@ def _nested_foreach_path_options(
|
||||
# bounded object schema, mirroring input/state inventory behavior.
|
||||
if prop_name == "item":
|
||||
options.extend(
|
||||
_nested_item_subpaths(
|
||||
prop_schema, owner_id, availability, depth=0
|
||||
)
|
||||
_nested_item_subpaths(prop_schema, owner_id, availability, depth=0)
|
||||
)
|
||||
return options
|
||||
|
||||
@@ -302,7 +300,10 @@ def _nested_item_subpaths(
|
||||
for name, sub_schema in properties.items():
|
||||
if not isinstance(name, str) or not isinstance(sub_schema, Mapping):
|
||||
continue
|
||||
if isinstance(sub_schema.get("type"), str) and sub_schema.get("type") == "array":
|
||||
if (
|
||||
isinstance(sub_schema.get("type"), str)
|
||||
and sub_schema.get("type") == "array"
|
||||
):
|
||||
# Arrays are whole values; item indexes need real runtime indexes.
|
||||
path = str(
|
||||
GraphSourcePath(
|
||||
@@ -339,7 +340,10 @@ def _nested_item_subpaths(
|
||||
)
|
||||
options.extend(
|
||||
_nested_item_subpaths(
|
||||
sub_schema, owner_id, availability, depth=depth + 1,
|
||||
sub_schema,
|
||||
owner_id,
|
||||
availability,
|
||||
depth=depth + 1,
|
||||
prefix_parts=(*prefix_parts, name),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -296,9 +296,7 @@ def _context_schema_for_stack(
|
||||
continue
|
||||
entry_schemas[owner_id] = foreach_entry_schema(
|
||||
owner_id,
|
||||
_foreach_item_schema(
|
||||
workflow, foreach, foreach_nodes, owner_stack_by_node
|
||||
),
|
||||
_foreach_item_schema(workflow, foreach, foreach_nodes, owner_stack_by_node),
|
||||
)
|
||||
properties[FOREACH_CONTEXT_KEY] = {
|
||||
"type": "object",
|
||||
@@ -306,9 +304,9 @@ def _context_schema_for_stack(
|
||||
"required": sorted(entry_schemas),
|
||||
"additionalProperties": False,
|
||||
}
|
||||
required: list[str] = [
|
||||
field.name for field in STANDARD_CONTEXT_FIELDS
|
||||
] + [FOREACH_CONTEXT_KEY]
|
||||
required: list[str] = [field.name for field in STANDARD_CONTEXT_FIELDS] + [
|
||||
FOREACH_CONTEXT_KEY
|
||||
]
|
||||
if stack:
|
||||
innermost = foreach_nodes.get(stack[-1])
|
||||
if innermost is not None:
|
||||
@@ -445,10 +443,7 @@ def _schema_document(
|
||||
current: dict[str, object] = {
|
||||
field.name: field.schema for field in STANDARD_CONTEXT_FIELDS
|
||||
}
|
||||
if (
|
||||
foreach_nodes is not None
|
||||
and owner_stack_by_node is not None
|
||||
):
|
||||
if foreach_nodes is not None and owner_stack_by_node is not None:
|
||||
entry_schemas: dict[str, object] = {}
|
||||
for owner_id in resolved_stack:
|
||||
foreach = foreach_nodes.get(owner_id)
|
||||
@@ -480,7 +475,9 @@ def _schema_document(
|
||||
for field in foreach_context_fields(foreach.as_, item_schema):
|
||||
# Innermost loop keys win; outer aliases accumulate.
|
||||
# Validation owns collision diagnostics.
|
||||
is_innermost = bool(resolved_stack) and owner_id == resolved_stack[-1]
|
||||
is_innermost = (
|
||||
bool(resolved_stack) and owner_id == resolved_stack[-1]
|
||||
)
|
||||
if field.name not in current or is_innermost:
|
||||
current[field.name] = field.schema
|
||||
return {"type": "object", "properties": current}
|
||||
|
||||
@@ -64,12 +64,8 @@ def validate_context_paths(
|
||||
node.input, f"nodes[{idx}].input", node.id, schema, report
|
||||
)
|
||||
elif isinstance(node, ConditionNode):
|
||||
for location, path in _condition_paths(
|
||||
node.check, f"nodes[{idx}].check"
|
||||
):
|
||||
_validate_one_context_path(
|
||||
path, location, node.id, schema, report
|
||||
)
|
||||
for location, path in _condition_paths(node.check, f"nodes[{idx}].check"):
|
||||
_validate_one_context_path(path, location, node.id, schema, report)
|
||||
elif isinstance(node, ForeachNode):
|
||||
# Context-rooted `over` paths reach this pass; the old
|
||||
# input/state-only check stays permissive for them.
|
||||
@@ -110,9 +106,7 @@ def _validate_step_input_bindings(
|
||||
binding.expression, f"{binding_location}.expression"
|
||||
):
|
||||
if path.root == "context":
|
||||
_validate_one_context_path(
|
||||
path, location, node_id, schema, report
|
||||
)
|
||||
_validate_one_context_path(path, location, node_id, schema, report)
|
||||
|
||||
|
||||
def _expression_paths(
|
||||
@@ -219,9 +213,10 @@ def _path_in_schema(schema: Mapping[str, Any], parts: tuple[str, ...]) -> bool:
|
||||
# allows subpaths; scalar or closed schemas do not.
|
||||
if current == {}:
|
||||
return True
|
||||
if current.get("type") == "object" and current.get(
|
||||
"additionalProperties", True
|
||||
) is not False:
|
||||
if (
|
||||
current.get("type") == "object"
|
||||
and current.get("additionalProperties", True) is not False
|
||||
):
|
||||
return True
|
||||
return False
|
||||
if part not in properties:
|
||||
@@ -280,9 +275,7 @@ def _validate_alias_ownership(
|
||||
continue
|
||||
alias = foreach.as_
|
||||
idx = node_index_by_id.get(owner_id)
|
||||
location = (
|
||||
f"nodes[{idx}].as" if idx is not None else f"nodes[{owner_id}]"
|
||||
)
|
||||
location = f"nodes[{idx}].as" if idx is not None else f"nodes[{owner_id}]"
|
||||
if not alias or alias in RESERVED_CONTEXT_KEYS:
|
||||
if owner_id not in reported:
|
||||
reported.add(owner_id)
|
||||
|
||||
@@ -52,12 +52,8 @@ def validate_workflow(workflow: Workflow) -> ValidationReport:
|
||||
issue.path,
|
||||
issue.message,
|
||||
)
|
||||
context_schemas = context_schemas_by_node(
|
||||
workflow, control_regions=analysis
|
||||
)
|
||||
validate_context_paths(
|
||||
workflow, context_schemas=context_schemas, report=report
|
||||
)
|
||||
context_schemas = context_schemas_by_node(workflow, control_regions=analysis)
|
||||
validate_context_paths(workflow, context_schemas=context_schemas, report=report)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
Reference in New Issue
Block a user