cadrumo.core.json_contract module¶
Shared primitives for the CLI’s strict --json output contract.
Defines the strict pydantic v2 bases (OutputSchema,
OutputRootSchema), the canonical success envelope
(SchemaEnvelope), the typed diagnostic channel
(Notice), and
the emit helper (emit_json_success())
used by every authored machine-output path. CLI payload modules import
these primitives directly from this module and route JSON mode through
entrypoints.cli.common.emit_envelope().
emit_json_success() derives EnvelopeStatus from supplied
Notice values via derive_status() and applies
core.redaction.rules.redact_structured_for_cli_output() to the
entire envelope before writing stdout. Text output remains owned by
core.output_rendering.render_command_output(), so redaction
and the reveal_cli_identifiers_opt_in switch stay consistent across
text and JSON surfaces.
Living in core keeps domain and adapter packages free of any
dependency on entrypoints.cli: a wrapped command emits its
strict-validated payload through emit_json_success() without
having to know how the CLI itself wires Click options.
This module owns the stdout success contract and schema registry. The
stderr failure document is rendered by core.errors using the
same ENVELOPE_SCHEMA_VERSION, and text-mode layout remains owned
by core.output_rendering.
- ENVELOPE_SCHEMA_VERSION¶
Envelope contract version shared by the success
SchemaEnvelopeand the stderr error envelope. Both documents carry the same outer spine (schema_version,command,status,notices), so the version is pinned once here and bumped only on a backwards-incompatible change to that spine.
- class EnvelopeStatus(*values)[source]¶
Bases:
StrEnumOutcome discriminator carried on every CLI return document.
successandwarningride on the stdoutSchemaEnvelope(warningwhen the command attached at least one warning-severityNotice);errorrides on the stderr error envelope. A machine consumer reads this single field to learn the outcome instead of branching on stdout-vs-stderr, andderive_status()is the success-envelope authority for computing it.emit_json_success()never emitsERROR; blocking failures route through the sharedCadrumoErrorboundary instead of being smuggled into stdout notices.- SUCCESS¶
- WARNING¶
- ERROR¶
- class NoticeSeverity(*values)[source]¶
Bases:
StrEnumSeverity of a single operator-facing
Notice.infois a non-fatal next-step hint or informational advisory;warningis a non-blocking advisory the operator should act on. A command that attaches anywarningnotice resolves toEnvelopeStatus.WARNINGthroughderive_status().- INFO¶
- WARNING¶
- class ActionConditionEvidence(**data)[source]¶
Bases:
PreconditionEvidenceOne evaluated failed-condition fact projected onto the wire.
The model deliberately contains observed facts only. It does not select a condition, decide applicability, or resolve an action catalogue.
- Parameters:
data (Any)
- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ResolvedActionReference(**data)[source]¶
Bases:
PreconditionActionIdentityOne catalogue action after an outer resolver supplied its canonical target.
The resolver is outside
cadrumo.core; this record neither looks up the action ID nor discovers the live command surface.- Parameters:
data (Any)
- target_command_key: str¶
- cli_path: tuple[str, ...] | None¶
- arguments: Mapping[str, str] | None¶
- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ResolvedActionArgument(**data)[source]¶
Bases:
ActionArgumentResolutionOne resolved or missing target argument in the action wire projection.
- Parameters:
argument_name (str)
status (ActionArgumentStatus)
source (ActionArgumentSource | None)
source_key (str | None)
source_evidence_id (str | None)
- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ResolvedPreconditionAction(**data)[source]¶
Bases:
PreconditionOutcomeInvariant[ActionConditionEvidence, ResolvedActionReference, ResolvedActionArgument]A resolved precondition verdict projected for a machine consumer.
This is intentionally a wire DTO. Application/domain guards own the rejected-condition policy and evidence; the operator surface owns the catalogue and live-schema resolution. The DTO only proves that the action target and each argument were resolved before presentation.
- Parameters:
failed_condition_id (Annotated[str, StringConstraints(strip_whitespace=None, to_upper=None, to_lower=None, strict=None, min_length=3, max_length=160, pattern=^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$, ascii_only=None)])
evidence (tuple[EvidenceT, ...])
action (ActionT | None)
argument_bindings (tuple[ArgumentT, ...])
conditionality (ActionConditionality)
no_recovery_outcome (NoRecoveryOutcome | None)
- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class ResolvedNoticeAction(**data)[source]¶
Bases:
BaseModelA fully materialised next action carried by a successful notice.
This transport record is deliberately distinct from
ResolvedPreconditionAction: success notices do not report a failed condition, conditional recovery state, or a terminal outcome. The operator-surface resolver must establish the catalogue target and every required input before constructing this record; this DTO in turn refuses any unresolved argument so a consumer never receives a partial action.- Parameters:
action (ResolvedActionReference)
argument_bindings (tuple[ResolvedActionArgument, ...])
- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- action: ResolvedActionReference¶
- argument_bindings: tuple[ResolvedActionArgument, ...]¶
- class Notice(**data)[source]¶
Bases:
BaseModelOne typed, non-blocking diagnostic on the envelope
noticeschannel.The single uniform surface for operator-facing warnings, advisories, and next-step hints across every command. Domain diagnostics (e.g.
ModeloValidationFinding, source-resolution advisories) are projected into this shape rather than re-modelled as bespoke per-command payload fields. CLI helpers such asentrypoints.cli.common.emit_envelope()pass these values toemit_json_success(), while text renderers fold equivalent prose into their line output.- Variables:
severity –
infoorwarning; drives the envelopestatus.code – Stable machine-readable notice identifier (e.g.
"modelo.calculate.unconsumed_iva").message – Localized operator-facing presentation text. It cannot carry an executable command identity.
action –
Optional schema-resolved action projection, and still the ONLY notice field that may identify an executable action. It carries a
ResolvedPreconditionActionfor a failed precondition or a fully materialisedResolvedNoticeActionafter success.The success shape keeps forward guidance out of locale prose and retains every concrete target argument and its provenance.
context – Optional deterministic non-action diagnostic metadata (e.g. source-resolution
reason/source_kind). Reserved action keys and executable command prose are rejected here.
- Parameters:
severity (NoticeSeverity)
code (str)
message (str)
action (ResolvedPreconditionAction | ResolvedNoticeAction | None)
Blocking failures are not notices; they raise an
CadrumoErrorand emit on stderr. Command payload schemas should also avoid reintroducing bespoke advisory, hint, or warning fields insideresultwhen aNoticecan carry the same non-blocking diagnostic.- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- severity: NoticeSeverity¶
- code: str¶
- message: str¶
- action: ResolvedPreconditionAction | ResolvedNoticeAction | None¶
- context: Mapping[str, str] | None¶
- derive_status(notices)[source]¶
Return
EnvelopeStatus.WARNINGif any notice is warning-severity.Success documents never carry
EnvelopeStatus.ERROR; that status is reserved for the stderr error envelope. The returnedEnvelopeStatusis the stdoutSchemaEnvelopestatus used byemit_json_success().- Return type:
- Parameters:
- exception OutputSchemaError(message=None, *, context=None, translated_message=None)[source]¶
Bases:
CadrumoErrorRaised when a strict CLI output contract is violated.
It deliberately inherits
core.errors.hierarchy.CadrumoErrorso registry defects route through the shared CLI error boundary instead of bypassing structured output.- Parameters:
- Return type:
None
- code¶
- class OutputSchema(**data)[source]¶
Bases:
BaseModelStrict, frozen base class for every command-specific
--jsonpayload.Subclasses inherit
extra="forbid",frozen=True,strict=True, andvalidate_assignment=Trueso accidental field drift between contract and implementation surfaces as a validation error rather than a silently-extended payload. Each concrete result model should be referenced by the authored CLI command specification.The class describes the inner
resultpayload only; the outerSchemaEnvelopeis applied later byemit_json_success().- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class OutputRootSchema(root=PydanticUndefined, **data)[source]¶
Bases:
RootModel[TypeVar],GenericStrict root/list base class for
--jsonpayloads with a non-mapping root.Use this for commands whose top-level JSON value is a list or scalar rather than an object. Carries the same strict / frozen / validate-on- assignment configuration as
OutputSchema, and participates in the same strict command-spec result contract.- Parameters:
root (RootModelRootType)
- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- strict_round_trip(cls, obj)[source]¶
Project obj into cls through a genuine JSON-text round trip.
cls.model_validate(obj.model_dump(mode="json"))reads as a round trip but is not one under strict pydantic v2 validation:model_dump(mode="json")projects aStrEnumto its bare string, adatetimeto its isoformat string, and atupleto a JSON array, andmodel_validateon the resulting plaindictdoes not coerce any of those back to their native type – onlymodel_validate_json’s genuine JSON-text parse gets that leniency. A model that gains one of those field types after its call sites were written breaks silently at the two-step dict form with no local signal at the call site itself.Use this helper (or call
cls.model_validate_json(obj.model_dump_json())directly) at every typed cross-model projection boundary. It is the migration target themodel_dump(mode="json")-into-model_validateinventory gate enforces, not a substitute for the gate: the codebase already hadmodel_dump_json/model_validate_jsonone method-name swap away and around forty call sites independently reached for the unsafe two-step form anyway, so a convenience helper alone would only add a second available idiom rather than retiring the first.
- class SchemaEnvelope(**data)[source]¶
Bases:
BaseModel,GenericStable outer envelope wrapping a successful command’s payload.
Every successful
--jsonresponse is rendered through this envelope so consumers can rely on the same outer keys regardless of the inner payload shape. The outer spine (schema_version,command,status,notices) is shared with the stderr error envelope so one shape describes success, warning, and error outcomes.emit_json_success()constructs the runtime mapping and the JSON-contract conformance gate specialises this generic envelope over every schema authored by the command-spec graph.The envelope is a wire contract, not the command dispatcher. It does not discover Click/Typer leaves, choose text output, or own command authorization; those layers supply a strict
resultand stable command path before entering this contract.- Variables:
schema_version – Envelope version; bumped only on backwards-incompatible changes to the shared spine.
command – Stable command path string (e.g.
"workflow list").active_profile – Human-readable label of the active taxpayer profile (the operator-chosen display name), or
Nonebefore any profile exists and for non-profile-bound commands. The identity anchor a caller reconciles against; it is the label, never the redacted profile/bucket UUID. Resolved and injected at the CLI transport boundary (thecorelayer never scans profile manifests), so it staysNonefor any emitter that does not supply it.status – Outcome discriminator (
successorwarninghere).result – The strict-validated command result.
notices – Typed non-blocking diagnostics (warnings, advisories, next-step hints) surfaced to the caller. Replaces the former free-form
warningsstring list.
- Parameters:
- model_config: ClassVar[ConfigDict]¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- schema_version: str¶
- command: str¶
- active_profile: str | None¶
- status: EnvelopeStatus¶
- result: ResultT¶
- notices: list[Notice]¶
- emit_json_success(command, result, *, notices=None, active_profile=None, indent=2, sort_keys=False, stream=None)[source]¶
Wrap
resultin the success spine and emit it via_write_json_document().The envelope’s
schema_versionis pinned toENVELOPE_SCHEMA_VERSION; bumping it is a contract-breaking change handled by the JSON-contract test suite, not a casual edit. Thestatusis derived from the supplied notices (derive_status()) so the JSON outcome and the shell exit code never disagree. The assembled envelope is redacted throughcore.redaction.rules.redact_structured_for_cli_output()before_write_json_document()writes it.This helper is stdout-only. Any raised
CadrumoErroris handled by the CLI error boundary, which renders the sibling stderr envelope instead of returning a success document with an error-shapedresult.- Parameters:
command (
str) – Stable command path string (e.g."workflow list").result (
object) – The strict-validated command payload to surface asenvelope.result.notices (
Sequence[Notice] |None) – Optional typedNoticediagnostics (warnings, advisories, next-step hints); defaults to an empty list.active_profile (
str|None) – Optional human label of the active taxpayer profile placed on the shared spine (the identity anchor). Thecorelayer never scans profile manifests, so the CLI transport resolves the label and passes it here;Nonefor non-profile-bound emitters. It rides through the same redaction pass as the rest of the envelope, but it is the non-secret display name, not the redacted profile/bucket UUID.indent (
int|None) – Indent width forwarded to_write_json_document().sort_keys (
bool) – Sort-keys flag forwarded to_write_json_document().stream (
Optional[IO[str]]) – Target text stream; defaults tosys.stdout.
- Return type:
- validate_registered_result(command, result)[source]¶
Strictly revalidate an already typed operator result.
The low-level JSON emitter intentionally remains a transport primitive for metadata and diagnostic surfaces. Operator command output must enter through this check so an envelope cannot claim a registered command while carrying an arbitrary result shape.
- Return type:
OutputSchema|OutputRootSchema[Any]- Parameters: