Types Module — Run Model Vocabulary¶
types
¶
Public types directory — shared vocabulary for functualize users.
This module re-exports frozen dataclasses and enums that form the shared type vocabulary for job authors, plugin authors, and app constructors.
Usage::
from functualize.types import JobResult, JobDescriptor, RunStatus
GLOBAL_BOOL_FLAGS = frozenset({'--no-dotenv', '--prompt-gates', '--no-prompt-gates', '--force', '--help', '-h'})
module-attribute
¶
GLOBAL_OPTIONS_ALWAYS_VALUE = frozenset({'--log-level', '--dotenv-file', '--config-directory', '--discovery-depth', '--require-file-import', '--require-file-prefix', '--require-file-postfix', '--require-file-marker', '--require-job-prefix', '--require-job-postfix', '--require-job-decorators', '--exclude', '--perf-filter', '--import-libs'})
module-attribute
¶
GLOBAL_OPTIONS_OPTIONAL_VALUE = frozenset({'--perf-report', '--emit-format'})
module-attribute
¶
GLOBAL_OPTIONS_WITH_VALUE = GLOBAL_OPTIONS_ALWAYS_VALUE | GLOBAL_OPTIONS_OPTIONAL_VALUE
module-attribute
¶
OPTIONAL_VALUE_VALID_SET = {'--perf-report': (frozenset({'text', 'json'}), 'text'), '--emit-format': (frozenset({'auto', 'json', 'ndjson', 'raw', 'none'}), 'auto')}
module-attribute
¶
RUN_SURFACES = frozenset(get_args(RunSurface))
module-attribute
¶
Every legal value of :attr:RunRequest.surface.
Derived from the Literal rather than repeated, so the two cannot drift.
RunSurface = Literal['func.job', 'func.group', 'func.single-file', 'app.cli', 'app.execute', 'func.builtin', 'tui.inline', 'tui.shell', 'mcp.tool', 'mcp.run-job', 'mcp.async', 'http', 'lambda', 'invoke', 'invoke.parallel', 'app.parallel', 'event.job-submit', 'engine.step', 'engine.dependency']
module-attribute
¶
__all__ = ['ExitCode', 'exit_code_for_status', 'Family', 'is_failure', 'report_line', 'status_from_wire', 'wire_value', 'MissingValueError', 'RUN_SURFACES', 'RunRequest', 'request_from_envelope', 'RunSurface', 'JobResult', 'JobDescriptor', 'FieldDescriptor', 'RunStatus', 'RunType', 'JobPhase', 'CacheInfo', 'ConfigFileInfo', 'ConfigFileRole', 'EnvironmentSource', 'Secret', 'http_status_for_status', 'GLOBAL_OPTIONS_ALWAYS_VALUE', 'GLOBAL_OPTIONS_OPTIONAL_VALUE', 'OPTIONAL_VALUE_VALID_SET', 'GLOBAL_OPTIONS_WITH_VALUE', 'GLOBAL_BOOL_FLAGS', 'flag_aliases', 'negative_aliases', 'match_group_flag', 'negative_flag_for']
module-attribute
¶
MissingValueError(field, env_var, *, hint='')
¶
Bases: Exception
A required value was absent and could not be collected.
Carries the field and env var rather than only a message so a caller (or a test) can act on the parts without parsing prose.
Source code in src/functualize/_engine/missing_value.py
CacheInfo(entry_count, stale_count, file_size_bytes, cache_path)
dataclass
¶
Statistics about the job discovery cache.
Attributes:
| Name | Type | Description |
|---|---|---|
entry_count |
int
|
Number of entries currently in the cache. |
stale_count |
int
|
Number of entries that are stale (source changed). |
file_size_bytes |
int
|
Size of the cache file in bytes. |
cache_path |
Path | None
|
Path to the cache file, or None if no cache exists. |
ConfigFileInfo(path, environment_slot, role, precedence, values=dict(), parsed=True)
dataclass
¶
A discovered config file and the part it plays in resolution.
Answers, for one file: where is it, which environment slot does it name, is it actually contributing under the active environment, and what did it contribute. Delivery layers need all of that together — knowing a file merely exists is not enough to explain why its values aren't winning.
Attributes:
| Name | Type | Description |
|---|---|---|
path |
str
|
Absolute path to the file. |
environment_slot |
str | None
|
The |
role |
ConfigFileRole
|
Whether this file is merged always (BASE), merged on top for the active environment (OVERLAY), or belongs to a different environment and is never merged (INERT). |
precedence |
int | None
|
Merge rank among contributing files — lower wins. None for INERT files, which never merge and so have no rank. |
values |
dict[str, Any]
|
The file's own parsed contents (not the merged view). Empty when the file could not be parsed. |
parsed |
bool
|
False when no FormatProvider matched the extension, or the file could not be read — the file is reported, not silently dropped, so a typo'd extension is diagnosable. |
path
instance-attribute
¶
environment_slot
instance-attribute
¶
role
instance-attribute
¶
precedence
instance-attribute
¶
values = field(default_factory=dict)
class-attribute
instance-attribute
¶
parsed = True
class-attribute
instance-attribute
¶
is_active
property
¶
True if this file contributes to the resolved configuration.
FieldDescriptor(name, type_annotation, default, description, required, choices=None, positional=False, short_flag=None, is_stdin=False, stdin_flag=None, secret=False, from_config_model=False)
dataclass
¶
Structured parameter schema for a job's configuration field.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Field name. |
type_annotation |
str
|
Type string (e.g., "str", "int", "bool", "list[str]"). |
default |
Any | None
|
Default value, or None if required. |
description |
str
|
Field description text. |
required |
bool
|
True if the field has no default value. |
choices |
list[str] | None
|
Enum member values (non-empty list if type is enum, None otherwise). |
positional |
bool
|
True if marked with Arg() — a positional CLI argument. |
short_flag |
str | None
|
Short flag alias (e.g., "-t") from Option() marker, or None. |
is_stdin |
bool
|
True if marked with Stdin() — reads from a pipe when available. |
stdin_flag |
str | None
|
Explicit flag name from Stdin(flag=...), or None to derive from the field name. |
secret |
bool
|
True when the field is marked secret by the model — either the
|
from_config_model |
bool
|
True when this field came from a job's config model rather than from its plain signature. The two are rendered by different rules, and a The rule that needs it: a config field's click option defaults to
|
name
instance-attribute
¶
type_annotation
instance-attribute
¶
default
instance-attribute
¶
description
instance-attribute
¶
required
instance-attribute
¶
choices = None
class-attribute
instance-attribute
¶
positional = False
class-attribute
instance-attribute
¶
short_flag = None
class-attribute
instance-attribute
¶
is_stdin = False
class-attribute
instance-attribute
¶
stdin_flag = None
class-attribute
instance-attribute
¶
secret = False
class-attribute
instance-attribute
¶
from_config_model = False
class-attribute
instance-attribute
¶
type
property
¶
Backward-compatible alias for type_annotation.
help
property
¶
Backward-compatible alias for description.
JobDescriptor(name, group, function=None, docstring=None, parameters=list(), source='', metadata=dict(), module_path='', source_file='', source_mtime=0.0, content_hash='', config_fields=list(), dependencies=dict(), requires_tty=False, optional_tty=False, uses_live=False, suppress_live=(), decorators=(), surface_hint=None, declaration=None, workflow=None, from_job_deps=(), python_name='')
dataclass
¶
Serializable metadata for a discovered job.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Job function name. |
group |
str | None
|
Job group name (None for top-level jobs). |
function |
Callable[..., Any] | None
|
The callable job function (None for cache-only descriptors). |
docstring |
str | None
|
Function docstring (None if absent). |
parameters |
list[FieldDescriptor]
|
Config parameters for the job as FieldDescriptors. |
source |
str
|
Module path or file path where the job was discovered. |
metadata |
dict[str, Any]
|
Additional metadata about the job. |
module_path |
str
|
Dotted module path for lazy import (defaults to source). |
source_file |
str
|
Filesystem path to source file (for cache invalidation). |
source_mtime |
float
|
Last modification time of source file. |
content_hash |
str
|
Content hash for cache invalidation. |
config_fields |
list[FieldDescriptor]
|
Alias for parameters (backward-compatible). |
dependencies |
dict[str, str]
|
First-level in-project imports {abs_path: sha256}. |
requires_tty |
bool
|
True if the signature declares a non-optional |
optional_tty |
bool
|
True if the signature declares |
uses_live |
bool
|
True if the signature declares a |
name
instance-attribute
¶
group
instance-attribute
¶
function = None
class-attribute
instance-attribute
¶
docstring = None
class-attribute
instance-attribute
¶
parameters = field(default_factory=list)
class-attribute
instance-attribute
¶
source = ''
class-attribute
instance-attribute
¶
metadata = field(default_factory=dict)
class-attribute
instance-attribute
¶
module_path = ''
class-attribute
instance-attribute
¶
source_file = ''
class-attribute
instance-attribute
¶
source_mtime = 0.0
class-attribute
instance-attribute
¶
content_hash = ''
class-attribute
instance-attribute
¶
config_fields = field(default_factory=list)
class-attribute
instance-attribute
¶
dependencies = field(default_factory=dict)
class-attribute
instance-attribute
¶
requires_tty = False
class-attribute
instance-attribute
¶
optional_tty = False
class-attribute
instance-attribute
¶
uses_live = False
class-attribute
instance-attribute
¶
suppress_live = ()
class-attribute
instance-attribute
¶
decorators = ()
class-attribute
instance-attribute
¶
surface_hint = None
class-attribute
instance-attribute
¶
declaration = None
class-attribute
instance-attribute
¶
workflow = None
class-attribute
instance-attribute
¶
from_job_deps = ()
class-attribute
instance-attribute
¶
python_name = ''
class-attribute
instance-attribute
¶
func_name
property
¶
The leaf of the registered name — after the last dot, or all of it.
This is the canonical leaf (build-wheel), not the Python
__name__ (build_wheel), because it is derived from name.
Callers wanting the module attribute want :attr:python_name.
attribute_name
property
¶
The module attribute to resolve this job's function from.
to_dict()
¶
Serialize to a JSON-compatible dict for cache persistence.
Uses config_fields if populated, otherwise falls back to parameters. Enum defaults are converted via .value attribute. Non-JSON-serializable defaults are converted to None.
Source code in src/functualize/_types/descriptors.py
from_dict(data)
classmethod
¶
Deserialize from a JSON dict.
Raises:
| Type | Description |
|---|---|
ValueError
|
If required keys are missing or values have wrong types. |
Source code in src/functualize/_types/descriptors.py
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 | |
JobResult(status, return_value, duration_ms, metadata=dict(), exception=None, job_name='')
dataclass
¶
Result of a job execution.
Attributes:
| Name | Type | Description |
|---|---|---|
status |
RunStatus
|
The final run status of the job. |
return_value |
Any
|
The value returned by the job function. |
duration_ms |
float
|
Execution duration in milliseconds. |
metadata |
dict[str, Any]
|
Additional metadata about the execution. |
exception |
BaseException | None
|
The exception that caused failure, or None on success. |
status
instance-attribute
¶
return_value
instance-attribute
¶
duration_ms
instance-attribute
¶
metadata = field(default_factory=dict)
class-attribute
instance-attribute
¶
exception = field(default=None, compare=False, hash=False)
class-attribute
instance-attribute
¶
job_name = ''
class-attribute
instance-attribute
¶
Secret(value)
¶
A string value marked secret — masked in every string rendering (§B.6).
str(secret) and repr(secret) return :data:MASK; the real value is
only reachable through :meth:get_secret_value, so a secret dropped into an
f-string, a log line, or a traceback shows ••• rather than leaking.
Secret[str] is a usable Pydantic field type: it accepts a plain str
from any source (config file, environment, CLI) and wraps it, so a config
author writes token: Secret[str] and gets a value that is masked
everywhere without needing arbitrary_types_allowed.
Source code in src/functualize/_types/redaction.py
__slots__ = ('_value',)
class-attribute
instance-attribute
¶
get_secret_value()
¶
__str__()
¶
__repr__()
¶
__eq__(other)
¶
__hash__()
¶
__class_getitem__(item)
¶
__get_pydantic_core_schema__(source, handler)
classmethod
¶
Accept a plain str (or an existing Secret) and wrap it.
Without this, token: Secret[str] on a plain BaseModel raises
PydanticSchemaGenerationError at class-definition time, and the job
declaring it disappears from func with only a warning on stderr —
so the framework's own public secret type could not be used in the
framework's own config models.
The serializer masks on the way out to JSON only
(when_used="json"), which is the path that actually leaks: a
resolved config reaching a file, a log sink, or an HTTP body without
passing :func:redacted_snapshot. model_dump() in python mode
returns the Secret itself, still masked by its own __str__.
Masking in python mode as well was tried and reverted. The framework
passes config models through model_dump() internally — Invoke
builds a child job's kwargs from one, RunContext.with_plugin_config
rebuilds a model from one, the argument validator merges one back — so
an unconditional serializer replaced live credentials with •••
between two of our own jobs, with no error and no warning. The child
then authenticates with the mask string. Losing the real value silently
is a worse failure than rendering it, and when_used="json" closes
the leak without opening that.
Source code in src/functualize/_types/redaction.py
__get_pydantic_json_schema__(schema, handler)
classmethod
¶
Emit {"secret": true} so the marker survives into the cache.
The TUI panels mask from the cached FieldDescriptor, which is
built by reading model_json_schema() — a warm boot never imports the
config model. Without this hook, Secret[str] would mask in
info --job (which has the live FieldInfo) and leak in the TUI
(which does not). This is what keeps the annotation and the
json_schema_extra flag one mechanism rather than two that happen to
agree in :func:is_secret_field.
Source code in src/functualize/_types/redaction.py
ConfigFileRole
¶
Bases: Enum
The role a discovered config file plays under the active environment.
Config files are named config.<slot>.<ext>. The base slot is
always loaded; a slot matching the active environment is merged on top
of it; any other slot belongs to a different environment and is not
merged at all.
BASE = 'base'
class-attribute
instance-attribute
¶
Always merged, regardless of the active environment.
OVERLAY = 'overlay'
class-attribute
instance-attribute
¶
Slot matches the active environment — merged on top of BASE.
INERT = 'inert'
class-attribute
instance-attribute
¶
Slot names a different environment — discovered, but never merged.
EnvironmentSource
¶
Bases: Enum
Where the active environment name came from.
Delivery layers use this to distinguish "explicitly selected" from "fell back to the default", which are very different things to show a user staring at a config file that isn't taking effect.
JobPhase
¶
Bases: Enum
Named phases within a job execution lifecycle.
DISCOVERY = 'discovery'
class-attribute
instance-attribute
¶
CONFIGURATION = 'configuration'
class-attribute
instance-attribute
¶
VALIDATION = 'validation'
class-attribute
instance-attribute
¶
EXECUTION = 'execution'
class-attribute
instance-attribute
¶
TEARDOWN = 'teardown'
class-attribute
instance-attribute
¶
RunStatus
¶
Bases: Enum
Status of a run context execution.
SUCCESS = 'Success'
class-attribute
instance-attribute
¶
FAILURE = 'Failure'
class-attribute
instance-attribute
¶
BLOCKED = 'Blocked'
class-attribute
instance-attribute
¶
SKIPPED = 'Skipped'
class-attribute
instance-attribute
¶
RUNNING = 'Running'
class-attribute
instance-attribute
¶
CANCELLED = 'Cancelled'
class-attribute
instance-attribute
¶
TIMEOUT = 'Timeout'
class-attribute
instance-attribute
¶
UNKNOWN = 'Unknown'
class-attribute
instance-attribute
¶
REFUSED = 'Refused'
class-attribute
instance-attribute
¶
resumable
property
¶
True when the run stopped at a declared pause point, not an error.
A workflow that reaches a Gate with no input is BLOCKED, not FAILURE:
it did everything it was asked to, and re-invoking it with the input
deposited continues from where it stopped. Callers that treat "not
SUCCESS" as "broken" — CI gates, Deps satisfaction, TUI colouring —
need to tell the two apart.
terminal
property
¶
True when the phase is over and cannot be transitioned from.
One definition, because there were two and they disagreed.
_engine/capabilities/workflow.py and
_engine/capabilities/runcontext.py each carried a _TERMINAL_STATES
frozenset; one included REFUSED and one did not. The first one's own
comment explains why the omission matters — "a refused step simply
never gets end_time or duration, so it reads as still running in
every consumer of this record" — and its sibling had exactly that
defect, one module away. The runcontext copy's test mirrored the
omission and asserted agreement with it, so the two could never
converge by failing.
This is the state machine's question — may a phase transition out
of here — and not the delivery question functualize.types.is_failure
answers. The membership is the one workflow.py reasoned about, kept
exactly: BLOCKED is a declared pause and resumable is the property
that says so; SKIPPED is a phase a run can still move on from; RUNNING
is the non-terminal case by definition; and UNKNOWN is deliberately
left transitionable, because a status nobody could determine should not
also be a state nobody can leave.
ran
property
¶
True when the job's body actually executed.
SKIPPED is not a failure — a guard or an up-to-date fingerprint
answered "no work to do", which is the point of declaring them. But it
is not SUCCESS either: a caller that treats them alike cannot tell a
build that ran from one that was already current, and func why
exists precisely to answer that.
RunType
¶
ExitCode
¶
Bases: IntEnum
Process exit codes functualize commits to.
Family
¶
Bases: StrEnum
The kind of boundary a run's outcome is crossing.
A :class:~enum.StrEnum so a family can be logged, serialised and compared
against a plain string without the caller unwrapping it. contracts.md
spells it (str, Enum); on this interpreter that is the same type with a
ruff warning attached (UP042), so the modern spelling is used.
PROCESS = 'process'
class-attribute
instance-attribute
¶
An exit code a shell sees.
PANEL = 'panel'
class-attribute
instance-attribute
¶
A rendered outcome inside a live surface, where the run stays addressable.
TOOL = 'tool'
class-attribute
instance-attribute
¶
A status string in a tool response.
WIRE = 'wire'
class-attribute
instance-attribute
¶
An HTTP status.
RunRequest(job_name, surface, kwargs=(lambda: _EMPTY)(), prompt_gates=False, output_format='auto', force=False, group_option_values=None, parent_scope=None, workflow_scope_id=None, invoke_depth=0, run_dependencies=True, force_fresh=False, cwd=None, job_directory=None, parent_run_id=None)
dataclass
¶
Everything one run needs, frozen at the door that built it.
The job is named, never resolved: :meth:JobExecutionEngine.run performs
the lookup, as it already does for workflow steps and dependencies. Nothing
outside _engine/ holds a job function in order to execute it.
surface has no default. A door must name itself — the coverage audit had
to reconstruct which door a run came from by reading code, and the run record
(F5) cannot record what the request never carried.
job_name
instance-attribute
¶
surface
instance-attribute
¶
kwargs = field(default_factory=(lambda: _EMPTY))
class-attribute
instance-attribute
¶
prompt_gates = False
class-attribute
instance-attribute
¶
output_format = 'auto'
class-attribute
instance-attribute
¶
force = False
class-attribute
instance-attribute
¶
group_option_values = None
class-attribute
instance-attribute
¶
parent_scope = None
class-attribute
instance-attribute
¶
workflow_scope_id = None
class-attribute
instance-attribute
¶
invoke_depth = 0
class-attribute
instance-attribute
¶
run_dependencies = True
class-attribute
instance-attribute
¶
force_fresh = False
class-attribute
instance-attribute
¶
cwd = None
class-attribute
instance-attribute
¶
job_directory = None
class-attribute
instance-attribute
¶
parent_run_id = None
class-attribute
instance-attribute
¶
__post_init__()
¶
Source code in src/functualize/_types/run_request.py
__hash__()
¶
Hash the scalar identity, not the payload.
kwargs and group_option_values are Mappings, and no mapping
the callers actually pass is hashable — a request carrying a plain dict
would make the whole object unhashable, which is the wrong trade for a
value object that wants to key a cache or join a set. Equality still
compares every field, so equal requests still hash equal; unequal
requests may collide, which is all a hash promises.
Source code in src/functualize/_types/run_request.py
replace(**changes)
¶
Return a copy with changes applied.
The engine re-points a request at a dependency or a workflow step this way, so the surface and delivery inputs travel with it instead of being re-derived at the second hop.
Source code in src/functualize/_types/run_request.py
exit_code_for_status(status)
¶
The process exit code a finished run should terminate with.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
RunStatus
|
The terminal :class: |
required |
Returns:
| Type | Description |
|---|---|
ExitCode
|
The pinned :class: |
ExitCode
|
|
ExitCode
|
recognise is a failure, and inventing a code here would put an |
ExitCode
|
unpinned number into the contract. |
Source code in src/functualize/_types/exit_codes.py
flag_aliases(field)
¶
Every spelling that selects field positively.
The long form is derived from the field name with underscores hyphenated
(dry_run -> --dry-run), matching what the click param builder
renders, plus the undecorated --dry_run so the name as written also
works. A short flag is included when the Option marker declared one.
Source code in src/functualize/_types/flag_grammar.py
match_group_flag(token, specs)
¶
Find the field a mid-path token selects, if any declares it.
Searched nearest-declaration-first so a nested group may shadow an
ancestor's flag. Returns (field, inline_value, negated) where
inline_value is the right-hand side of a --flag=value spelling and
negated says the --no- spelling was used.
negated is a third element rather than a synthesised inline="false"
because the caller must tell --no-strict from --strict=false: the
first is the supported spelling and the second is refused.
Source code in src/functualize/_types/flag_grammar.py
negative_aliases(field, siblings)
¶
Every spelling that selects field negatively, for a bool.
Empty for a non-boolean, and empty when a sibling literally named
no_<name> owns the spelling — the same rule the click builders render
from. Two surfaces asking one function is the point: if they decided
independently, --no-cache would mean different things depending on how
the program was invoked.
Source code in src/functualize/_types/flag_grammar.py
negative_flag_for(name, siblings=())
¶
The --no- spelling that turns boolean field name off.
Returns None when a sibling field is literally called no_<name>.
That field owns the spelling, and name renders with no negative form.
The rule exists because click will not enforce one. Declaring both cache
and no_cache gives two parameters contending for --no-cache, and
click raises nothing — it binds whichever was declared first, so the same
two fields produce opposite results depending on the order they were
written in. That silent, order-dependent shadowing is the defect; this
function's guarantee is determinism, not detection. A user gets a
working CLI in which one field simply has no negative spelling.
Lives here, and is re-exported through functualize.app.utils, because
two surfaces must agree on it: the click param builders render the flag,
and func's pre-boot dispatch parser matches it mid-path. If they decided
this independently, --no-cache would come to mean different things
depending on how the program was invoked — the divergence class that
already produced three disagreeing dependency resolvers here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The boolean field's Python name ( |
required |
siblings
|
Iterable[str]
|
Every field name visible on the same command, this one included. Passing only some of them re-opens the collision. |
()
|
Returns:
| Type | Description |
|---|---|
str | None
|
|
Source code in src/functualize/_types/flag_grammar.py
http_status_for_status(status)
¶
The HTTP status code a finished run should be reported with.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
RunStatus
|
The terminal :class: |
required |
Returns:
| Type | Description |
|---|---|
int
|
The HTTP status code. Unmapped statuses — only |
int
|
never observed at a request boundary — fall back to |
int
|
than inventing a code. |
Source code in src/functualize/_types/http_status.py
is_failure(status, *, family)
¶
Is this outcome a failure at family's boundary?
The rule that was spelled three times, with the family as an argument
instead of as a comment. is_failure(BLOCKED, family=PROCESS) is True;
is_failure(BLOCKED, family=PANEL) is False.
RUNNING is transient and never observed at a boundary; asking about it
is a caller bug, and it answers True rather than inventing a fifth
outcome, matching how the exit-code table treats anything unmapped.
Source code in src/functualize/_types/outcome.py
report_line(status)
¶
The one line a status owes the caller before its code is delivered.
Two statuses have something to say first, and saying nothing is worse than saying it twice:
BLOCKED— without the message a paused run looks like a plain success to anything reading only stdout;REFUSED— a stage that declined because its declared inputs were absent must reach the boundary, or silence plus exit 0 reads as "verified, nothing wrong". That is precisely the false clean.
Returns None for every status that owes nothing, so a caller can write
if (line := report_line(status)): without a second table.
The detail — which gate, which scope, the resume incantation — stays with the surface that has the result object. This is the sentence, not the report.
Source code in src/functualize/_types/outcome.py
status_from_wire(value)
¶
Read a status string back, or None if it names no status.
This replaces the hand-rolled reverse lookup in _cli/builtins.py, whose
fallback — 0 if status in {"answered", "drafted"} else 1 — invented two
outcomes the RunStatus vocabulary does not have and mapped everything
else to a bare failure. Returning None hands the decision back to the
caller with the fact that the string was unrecognised, which is the thing
the fallback threw away.
Source code in src/functualize/_types/outcome.py
wire_value(status)
¶
The status string a tool response carries.
Lowercase, stable, and the inverse of :func:status_from_wire. Callers
branch on these strings, so they are an interface, not a rendering.
Source code in src/functualize/_types/outcome.py
request_from_envelope(payload, *, job_name, surface)
¶
Parse a wire payload into a request — the one copy of that contract.
The shape is an envelope: the job's own parameters live in a nested
arguments object and the control inputs sit beside it, never inside::
{"arguments": {"target": "prod"},
"group_option_values": {"env": "staging"},
"scope_id": "run-42",
"force": true}
The nesting is the fix, not decoration. A flat body meant a caller's key
could bind to a control parameter — send {"scope_id": "x"} and you were
choosing the workflow scope the run joined rather than passing an argument
(spec AC-17a). Nested, a job parameter literally named scope_id
arrives as an argument and the scope stays a separate, deliberate choice.
scope_id is also what makes a gated workflow resumable over the wire:
start it, read the scope id back from the result metadata, answer the gate,
send the same id again. The audit recorded that as impossible (D-6) because
there was no field to put it in.
Breaking, deliberately. Job parameters used to be the whole body; they
are now under arguments.
Why it lives here. It was written twice — HTTP and Lambda held
byte-identical copies differing only in the surface literal — and
restated in prose twice more at MCP's two doors. One wire contract
maintained in four places, with the breaking change documented four times
and three of the four citations wrong (two said "risk R-a, spec AC-9",
one said "spec AC-4, AC-9"; the criterion is AC-17a). A reader auditing
AC-17a through its keeper would conclude the wire doors were uncovered.
This module is stdlib-only and every one of those doors already imports
RunRequest from it, so there is no layering reason for the copies to
exist (rre F12).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
Mapping[str, Any]
|
The decoded wire body. |
required |
job_name
|
str
|
The job the door resolved. |
required |
surface
|
RunSurface
|
The calling door — the only thing that differed between the two copies, and now a parameter rather than a reason to fork. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
A field is present with the wrong JSON type. Each is reported by name, because "invalid payload" sends the caller hunting through a body they thought was correct. |
Source code in src/functualize/_types/run_request.py
Overview¶
functualize.types is the shared type vocabulary for job authors, plugin authors, and app constructors. This page documents the run-model slice of that vocabulary — the door a run entered through, and how a finished run's status translates into what a delivery surface actually reports. It is the largest addition to functualize.types in this release; see Discovery (Internal) for JobDescriptor/FieldDescriptor and Config (Internal) for the config-file and environment vocabulary.
Module location: src/functualize/_types/run_request.py (surface vocabulary) and src/functualize/_types/outcome.py (status translation).
Public API¶
from functualize.types import (
RunSurface,
RUN_SURFACES,
request_from_envelope,
wire_value,
status_from_wire,
report_line,
)
RunSurface and RUN_SURFACES¶
RunSurface is a Literal naming every door a run can enter through — the one field every RunRequest (functualize.types.RunRequest, the frozen dataclass every entry surface builds and JobExecutionEngine.run consumes) must set, with no default, because a door must name itself:
RunSurface = Literal[
"func.job", "func.group", "func.single-file",
"app.cli", "app.execute", "func.builtin",
"tui.inline", "tui.shell",
"mcp.tool", "mcp.run-job", "mcp.async",
"http", "lambda",
"invoke", "invoke.parallel", "app.parallel",
"event.job-submit", "engine.step", "engine.dependency",
]
RUN_SURFACES is the frozenset[str] of those same 19 values, derived from the Literal via get_args() rather than typed out a second time, so the two vocabularies cannot drift:
from functualize.types import RUN_SURFACES, RunSurface
def build_request(surface: RunSurface) -> None:
if surface not in RUN_SURFACES:
raise ValueError(f"unknown surface {surface!r}")
A RunRequest constructed with a surface outside RUN_SURFACES raises ValueError naming the offender and the closed set, so a door that mistypes its own name is caught at construction rather than traveling all the way to the run record.
request_from_envelope¶
def request_from_envelope(
payload: Mapping[str, Any],
*,
job_name: str,
surface: RunSurface,
) -> RunRequest: ...
Parses a wire payload into a RunRequest — the one shared implementation for every out-of-process door (HTTP, Lambda, and MCP's two doors), which previously carried byte-identical copies of this parsing differing only in the surface literal.
The payload is an envelope: a job's own parameters live under a nested "arguments" key, and control inputs sit beside it, never inside:
{
"arguments": {"target": "prod"},
"group_option_values": {"env": "staging"},
"scope_id": "run-42",
"force": True,
}
This nesting is deliberate, not decoration — a flat body let a caller's own argument name collide with a control field (sending {"scope_id": "x"} silently chose the workflow scope a run joined instead of passing an argument). scope_id is also what makes a gated workflow resumable over the wire: start it, read the scope id back from the result metadata, answer the gate, send the same id again.
from functualize.types import request_from_envelope
request = request_from_envelope(
{"arguments": {"target": "prod"}, "force": True},
job_name="deploy",
surface="http",
)
Breaking, deliberately. Job parameters used to be the whole request body; they are now nested under "arguments".
Raises: ValueError — a field is present with the wrong JSON type (arguments not an object, group_option_values not an object, scope_id not a string). Each case is reported by name.
wire_value and status_from_wire¶
The inverse pair that turns a RunStatus into the status string a tool or HTTP response carries, and reads one back:
def wire_value(status: RunStatus) -> str: ...
def status_from_wire(value: str) -> RunStatus | None: ...
from functualize.types import RunStatus, status_from_wire, wire_value
wire_value(RunStatus.BLOCKED) # "blocked"
status_from_wire("blocked") # RunStatus.BLOCKED
status_from_wire("not-a-status") # None
wire_value is status.value.lower() — lowercase, stable, and an interface callers branch on rather than a rendering choice. status_from_wire does the reverse lookup and returns None when the string names no status, handing the "this was unrecognised" fact back to the caller instead of guessing a fallback outcome.
report_line¶
The one line a status owes the caller before its exit code, HTTP status, or tool string is delivered. Only two statuses have something to say first:
from functualize.types import RunStatus, report_line
report_line(RunStatus.BLOCKED)
# "Blocked: the run paused at a declared gate and is resumable."
report_line(RunStatus.REFUSED)
# "Refused: a declared precondition for running this job was not met."
report_line(RunStatus.SUCCESS)
# None
Returns None for every status that owes nothing, so a caller can write if (line := report_line(status)): without a second table. The detail — which gate, which scope, the resume incantation — stays with the surface that holds the result object; this is the sentence, not the report.
Why this exists
Nine call sites used to translate a RunStatus into something a caller could act on, each with its own copy of the rules, and two of them disagreed about whether a paused (BLOCKED) run counts as a failure. functualize.types is now the single authority: a delivery surface asks, it does not decide. See report_line's sibling is_failure(status, *, family=...) for the family-scoped failure question (not part of this page's public surface).
Global flag vocabulary¶
GLOBAL_BOOL_FLAGS, GLOBAL_OPTIONS_OPTIONAL_VALUE, GLOBAL_OPTIONS_WITH_VALUE and OPTIONAL_VALUE_VALID_SET are the four tables that say how functualize's global options consume the tokens after them. They are public because the CLI is forbidden from reaching _types/ directly — _cli/ may import public folders only — so the re-export is the sanctioned route, not an accident.
Global options fall into three kinds, and the difference is entirely about the next token:
| Table | Meaning | Members |
|---|---|---|
GLOBAL_OPTIONS_ALWAYS_VALUE |
Always consumes the next token | --log-level, --dotenv-file, --config-directory, --discovery-depth, the seven --require-* filters, --exclude, --perf-filter, --import-libs |
GLOBAL_OPTIONS_OPTIONAL_VALUE |
May consume the next token, by lookahead | --perf-report, --emit-format |
GLOBAL_BOOL_FLAGS |
Never consumes a token | --no-dotenv, --prompt-gates, --no-prompt-gates, --force, --help, -h |
GLOBAL_OPTIONS_WITH_VALUE is the union of the first two — every flag that could be followed by a value — and exists for --option=value detection, where the distinction between "always" and "maybe" does not apply.
OPTIONAL_VALUE_VALID_SET¶
The lookahead needs to know what a legal value looks like, because that is the only thing separating a value from the next argument. This maps each optional-value flag to (valid_values, default):
from functualize.types import OPTIONAL_VALUE_VALID_SET
OPTIONAL_VALUE_VALID_SET["--perf-report"]
# (frozenset({'text', 'json'}), 'text')
OPTIONAL_VALUE_VALID_SET["--emit-format"]
# (frozenset({'auto', 'json', 'ndjson', 'raw', 'none'}), 'auto')
So func --perf-report deploy runs the deploy job with a text report, while func --perf-report json deploy consumes json as the report format. deploy is not in the valid set, so it is left alone as the job name.
Note that auto is both --emit-format's default and a typeable value. A bare --emit-format falls back to it through the lookahead, and that fallback is validated like any other, so it has to be legal — which also lets a caller name the default explicitly.
--emit-format governs out.emit() and nothing else
A job's return value is never rendered at any format, and print() ignores the flag entirely. It was called --output until 2026-09-10 and renamed precisely because that name promises to control "the command's output" and does not. There is deliberately no alias: pre-alpha, the constitution says delete rather than shim.
--version is absent from GLOBAL_BOOL_FLAGS
It is handled by a pre-boot fast path in _cli/main.py and is position-aware — recognised only before the first positional argument. --help and -h are listed, because Click needs to see them to render per-command help.
Alias matching for job and group flags — as opposed to these global ones — is flag_aliases, negative_aliases, match_group_flag and negative_flag_for, exported from the same module.