Documentation
¶
Overview ¶
Package pipeline implements the deterministic 5-stage validation pipeline that gates every Jira mutation submission. Stages run in fixed order; warnings collected by earlier stages survive a later-stage fatal so the envelope always carries the full context.
Stages:
- Parse / shape
- ADF + compatibility (pkg/adf calls)
- Field schema / screen validation (this file)
- Customfield registry (pkg/jira/customfield)
- Dry-run preview / live submit
Strict aborts at the first FATAL stage. Best-effort drops or coerces where defined and continues.
Index ¶
- Variables
- func ApplyKnownSafeFallback(fields map[string]any) (map[string]any, []adf.Warning)
- func FlattenCustomFields(fields map[string]any) (map[string]any, error)
- func IsKnownSafeField(name string) bool
- func KnownSafeFields() []string
- func NormalizeCreateAliases(fields map[string]any) map[string]any
- func NormalizeCreateAliasesChecked(fields map[string]any) (map[string]any, error)
- func ResolveAndApplySchema(fields map[string]any, fetch SchemaFetcher, mode adfmode.Mode) (map[string]any, []adf.Warning, error)
- func ValidateFields(fields map[string]any, schema ScreenSchema, mode adfmode.Mode) (map[string]any, []adf.Warning, error)
- type CustomFieldDecision
- type CustomFieldError
- type FieldValidationError
- type Input
- type MutationInput
- type MutationResult
- type Result
- type SchemaFetcher
- type ScreenSchema
- type Stage
Constants ¶
This section is empty.
Variables ¶
var ErrSchemaNotFound = errors.New("pipeline: project/issue-type schema not found")
ErrSchemaNotFound is returned by a schema fetcher when the project or issue type does not exist (a 404 / unknown-project). Unlike ErrSchemaUnknown this is a definite user error, never transient, so it is fatal in EVERY mode — the best-effort known-safe fallback must not paper over a typo'd project or issue type.
var ErrSchemaUnknown = errors.New("pipeline: project/issue-type schema unknown")
ErrSchemaUnknown is returned by a schema fetcher when the project / issue-type / screen schema cannot be resolved for a transient reason (transport failure, timeout, missing-permission). It is recoverable: callers attempt one refresh, then strict-abort or fall back to the known-safe field set.
Functions ¶
func ApplyKnownSafeFallback ¶
ApplyKnownSafeFallback strips every field outside the known-safe whitelist and emits one warning per dropped field. Used by the best-effort field-validation path after a single schema refresh has failed to disambiguate the screen.
func FlattenCustomFields ¶
FlattenCustomFields lifts a contract-level `custom_fields` sub-map into top-level customfield_NNNNN keys so screen validation and encoding operate on one flat namespace. Raw customfield_NNNNN keys already at the top level are left in place.
It is fatal when:
- the custom_fields value is present but not an object — the CLI cannot route it and silently dropping it would lose input;
- a nested key collides with an existing top-level key — the CLI will not silently pick a winner.
The input map is not mutated; a flattened copy is returned.
func IsKnownSafeField ¶
IsKnownSafeField reports whether a field is in the known-safe whitelist.
func KnownSafeFields ¶
func KnownSafeFields() []string
KnownSafeFields returns a copy of the known-safe whitelist.
func NormalizeCreateAliases ¶
NormalizeCreateAliases translates the CLI create-input aliases into the Jira wire field ids BEFORE the mutation pipeline runs. Screen validation (stage 3) keys on the wire ids, so an un-normalized alias such as project_key would be flagged off-screen even for a default create. The input map is not mutated; a normalized copy is returned.
A non-empty string alias value is encoded into its wire object shape (project_key "JCT" -> project {"key":"JCT"}). An alias with an empty or non-string value is dropped — it carries nothing to send. An alias whose wire key is also explicitly set is left for NormalizeCreateAliasesChecked to reject; this convenience form keeps the explicit value and discards the alias.
func NormalizeCreateAliasesChecked ¶
NormalizeCreateAliasesChecked is NormalizeCreateAliases that also reports a conflict: when an alias and its wire key are BOTH set, the CLI will not silently pick a winner. The user must supply the field in exactly one place.
func ResolveAndApplySchema ¶
func ResolveAndApplySchema(fields map[string]any, fetch SchemaFetcher, mode adfmode.Mode) (map[string]any, []adf.Warning, error)
ResolveAndApplySchema combines schema resolution + best-effort fallback. In best-effort mode, an unresolved schema after one refresh triggers known-safe stripping with warnings. In strict mode, the same condition aborts via ResolveScreenSchemaStrict's error.
func ValidateFields ¶
func ValidateFields(fields map[string]any, schema ScreenSchema, mode adfmode.Mode) (map[string]any, []adf.Warning, error)
ValidateFields runs Stage 3 of the mutation pipeline. In strict mode it aborts on the first invalid field; in best-effort it returns a copy with invalid fields stripped and one warning per drop.
Types ¶
type CustomFieldDecision ¶
CustomFieldDecision is the outcome of CustomFieldDropPolicy for one custom field value: whether to abort the whole mutation (Fatal), whether to forward the value unchanged (Forward), and a human-facing warning to surface when the value is forwarded or dropped without an abort.
func CustomFieldDropPolicy ¶
func CustomFieldDropPolicy(fieldID, fieldType string, malformed bool, mode adfmode.Mode) CustomFieldDecision
CustomFieldDropPolicy is the single shared decision used by every non-TUI mutation path (create/edit/clone/move) so they treat a problematic user-supplied custom field identically.
- A malformed value for a KNOWN schema.custom type is fatal in strict mode (dropping it would lose user input) and dropped with a warning in best-effort mode.
- A value for an UNKNOWN type (no schema.custom token, e.g. a marketplace/vendor field) is forwarded opaquely with a warning in both modes — the CLI cannot validate the shape, and forwarding with a warning is the shipped contract. It is never silently dropped.
fieldType is the schema.custom token resolved for the field, or "" when no type is known. malformed reports whether the value failed validation for a known type.
type CustomFieldError ¶
CustomFieldError is the typed error returned by stage 4.
func (*CustomFieldError) Error ¶
func (e *CustomFieldError) Error() string
type FieldValidationError ¶
type FieldValidationError struct {
Field string
Project string
IssueType string
Operation string
Reason string
}
FieldValidationError is the typed error returned by ValidateFields in strict mode. Carries the required context: field name, project, issue type, operation, reason.
func (*FieldValidationError) Error ¶
func (e *FieldValidationError) Error() string
type Input ¶
type Input struct {
Mode adfmode.Mode
// Stage 1 — parse.
ParseError string
// Stage 2 — ADF.
ADFInvalid bool // strict: fatal; best-effort: degradable iff ADFWarning set
ADFWarning string // if non-empty, surfaces as a stage-2 warning
// Stage 3 — field schema.
FieldOnScreen bool
// Stage 4 — customfield encoding.
CustomEncodeOK bool
// Stage 5.
DryRun bool
}
Input is the contract test surface for the pipeline orchestrator. It abstracts over the actual stage implementations so unit tests can inject failures at any stage without wiring full schemas. Production callers populate the same fields from real validation results.
type MutationInput ¶
type MutationInput struct {
Mode adfmode.Mode
// Stage 1 — parse / shape. Set by the caller after pulling values
// out of cobra flags + --json-input. Empty means parse succeeded.
ParseError string
// Stage 2 — ADF + compatibility. The Document the caller built
// (typically via adf.FromMarkdown for --description-markdown, or
// via --json-input for raw ADF). FieldCompat selects per-field
// compatibility (inlineCard rules).
ADFDoc *adf.Document
FieldCompat *adf.FieldCompatibility
// NamedADFDocs holds additional ADF substructures keyed by their
// field name (e.g. "description_adf"). All are validated through
// ValidateDoc at stage 2 with the same mode as ADFDoc. This is the
// hook used by issue create --json-input to catch garbage ADF in
// payload subfields.
NamedADFDocs map[string]adf.Document
// MarkdownWarnings carries warnings produced when the caller
// converted Markdown input to ADF (adf.FromMarkdownLossy). A lossy
// conversion means user content was dropped before the document
// even reached the pipeline. In strict mode any lossy warning here
// aborts the mutation at stage 2; in best-effort mode the warnings
// are surfaced and the partial document proceeds.
MarkdownWarnings []adf.Warning
// Stage 3 — field schema / screen. Either Schema (preloaded) or
// SchemaFetcher (lazy with refresh-once + known-safe fallback).
Schema ScreenSchema
SchemaFetcher SchemaFetcher
// ScreenValidationExemptFields skips stage 3 only for native fields
// that are already in Jira's API-ready shape. customfield_NNNNN keys
// still go through screen validation so their schema-backed encoding
// remains available in stage 4.
ScreenValidationExemptFields map[string]bool
// Stage 4 — customfield encoding. The Fields map is what the caller
// would post to Jira's REST API. Each customfield_xxxxx value is
// validated through pkg/jira/customfield.Registry.
Fields map[string]any
// Stage 5.
DryRun bool
}
MutationInput is the realistic, command-facing input shape for the 5-stage pipeline. internal/cli/root/commands.go assembles one of these from flag values, --json-input, the resolved profile, and the active schema; pipeline.RunMutation orchestrates stages 1–5.
Either Schema (preloaded, e.g., from cache) OR SchemaFetcher (lazy, triggers refresh-once on ErrSchemaUnknown) MAY be provided. When neither is set, stage 3 (field schema / screen validation) is skipped — stage 4 (customfield encoding) still runs and rejects malformed values regardless of schema availability.
CURRENT INTEGRATION DEPTH: internal/cli/root/commands.go wires this struct with Mode + Fields + DryRun + ADFDoc. Stage 2 (ADF validation) is now load-bearing: ValidateDoc rejects unknown nodes, illegal marks, and unknown mark types before submission. Stage 3 (field schema / screen validation) remains opt-in via SchemaFetcher — wiring ProjectService into a SchemaFetcher closure to activate it is documented future work. Stages 1 (parse), 2 (validate), 4 (customfield encoding), and 5 (dry-run vs submit gating) all carry production traffic; stage 3 is provided for future field-mapping work.
type MutationResult ¶
type MutationResult struct {
Aborted bool
AbortedAt Stage
Submitted bool
PreviewReady bool
// SubmitFields is the post-validation, post-encoding fields map the
// caller should send to Jira. In strict-abort cases this is nil.
SubmitFields map[string]any
// SubmitADF is the post-validation ADF doc (after compatibility
// passes). nil if no doc was supplied.
SubmitADF *adf.Document
// Warnings collected from every stage that ran. Non-empty even when
// a later stage aborted — earlier-stage observations always survive.
Warnings []adf.Warning
Err error
}
MutationResult reports the outcome of one RunMutation call.
func RunMutation ¶
func RunMutation(in MutationInput) MutationResult
RunMutation executes stages 1–5 with realistic inputs. Designed to be the single call site for every mutation command.
type Result ¶
type Result struct {
Aborted bool
AbortedAt Stage
Submitted bool
PreviewReady bool
Warnings []adf.Warning
Err error
}
Result reports what happened. Aborted/AbortedAt cooperate so the CLI can map the abort to a specific exit code; Warnings always include every observation from earlier stages even when a later stage aborts.
type SchemaFetcher ¶
type SchemaFetcher func() (ScreenSchema, error)
SchemaFetcher returns the active screen schema. It MAY return ErrSchemaUnknown to indicate a stale/missing cache.
type ScreenSchema ¶
type ScreenSchema struct {
Project string
IssueType string
ValidFields map[string]bool
FieldTypes map[string]string
}
ScreenSchema is the per-project / per-issue-type field whitelist. Fields not in ValidFields are "not on screen" and trigger a strict abort or best-effort drop.
FieldTypes maps a field id (customfield_NNNNN or a system field id) to the schema.custom token Jira reports for it (e.g. "select", "multiselect", "datepicker"). Stage 4 uses it to encode each custom field per its declared type instead of guessing from registry type-name keys. A field absent from FieldTypes has no declared type and is forwarded opaquely.
func ResolveScreenSchemaStrict ¶
func ResolveScreenSchemaStrict(fetch SchemaFetcher) (ScreenSchema, bool, error)
ResolveScreenSchemaStrict fetches the schema, refreshes once if unknown, then either returns the resolved schema or aborts (strict path). The bool reports whether a refresh was attempted.