pipeline

package
v0.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 8 Imported by: 0

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:

  1. Parse / shape
  2. ADF + compatibility (pkg/adf calls)
  3. Field schema / screen validation (this file)
  4. Customfield registry (pkg/jira/customfield)
  5. Dry-run preview / live submit

Strict aborts at the first FATAL stage. Best-effort drops or coerces where defined and continues.

Index

Constants

This section is empty.

Variables

View Source
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.

View Source
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

func ApplyKnownSafeFallback(fields map[string]any) (map[string]any, []adf.Warning)

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

func FlattenCustomFields(fields map[string]any) (map[string]any, error)

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

func IsKnownSafeField(name string) bool

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

func NormalizeCreateAliases(fields map[string]any) map[string]any

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 "KAN" -> project {"key":"KAN"}). 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

func NormalizeCreateAliasesChecked(fields map[string]any) (map[string]any, error)

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

type CustomFieldDecision struct {
	Fatal   bool
	Forward bool
	Warning string
}

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

type CustomFieldError struct {
	Field  string
	Reason string
}

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.

func Run

func Run(in Input) Result

Run executes stages 1–5 in deterministic order.

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.

type Stage

type Stage int

Stage identifies which step of the mutation pipeline the pipeline reached.

const (
	StageNone Stage = iota
	StageParse
	StageADF
	StageFieldSchema
	StageCustomField
	StageSubmit
)

func (Stage) String

func (s Stage) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL