synth

package
v0.33.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package synth produces synthetic .pulse cohorts from either a schema declaration ("from-schema") or a statistical profile of a real cohort ("from-profile"). The generator is deterministic given a seed and writes directly into the .pulse binary format using the encoding package, so outputs are byte-identical for the same (spec, seed) pair.

Two top-level entry points are exported by this package:

Synth(spec Spec, opts Options) (*Result, error)
Profile(schema, records, opts ProfileOptions) (*Profile, error)

Pulse embedders should use the higher-level pulse.Pulse.Synth and pulse.Pulse.Profile facade methods instead.

Index

Constants

View Source
const (
	DistUniform             = "uniform"
	DistNormal              = "normal"
	DistLogNormal           = "lognormal"
	DistExponential         = "exponential"
	DistPoisson             = "poisson"
	DistPareto              = "pareto"
	DistBernoulli           = "bernoulli"
	DistMonotonicFrom       = "monotonic_from"
	DistWeightedCategorical = "weighted_categorical"
	DistUniformDate         = "uniform_date"
	DistRegex               = "regex"
	DistConstant            = "constant"
	DistMixture             = "mixture"
	DistSetBernoulli        = "set_bernoulli"
)

Distribution kind constants.

View Source
const (
	ModelPredictorCategoricalLevel = "categorical_level"
	ModelPredictorSetOption        = "set_option"
	ModelPredictorNumeric          = "numeric"
)

Model predictor kinds, as they appear on ModelPredictor.Kind. These are the string spellings of the internal dummyColumnKind — spelled out rather than exposing the internal enum because this shape is what the profile document's `models` section serialises.

View Source
const (
	// ResidualUnmeasuredNoOverlap means fewer than
	// minResidualPairObservations rows carried BOTH residuals. Listwise
	// deletion is per model, so two fields with disjoint null patterns
	// can each have thousands of residuals and share almost none.
	ResidualUnmeasuredNoOverlap = "insufficient_overlap"
	// ResidualUnmeasuredNoVariance means the rows DID overlap but at
	// least one side's residual was constant across them, so the
	// correlation is 0/0. A perfectly fitted model on the overlap is the
	// ordinary cause.
	ResidualUnmeasuredNoVariance = "no_variance"
)

Reasons a residual pair could not be measured. Closed vocabulary: a consumer reading `residual_correlations.unmeasured` must be able to switch on this exhaustively, and a new reason is a deliberate addition to this list rather than a free-text string invented at a call site.

View Source
const ContingencyCellCap = 128

ContingencyCellCap bounds the number of distinct (a_value, b_value) cells CategoricalPairProfile.Cells retains per categorical-categorical pair. A pair of two 50-plus category fields can still produce thousands of joint combinations even after each field's own ProfileOptions.TopK collapse (e.g. two fields each capped at the default TopK=32 allow up to 33*33=1089 joint cells) — this second, joint-cardinality cap keeps the profile document bounded regardless of either field's own cardinality. 128 was chosen as generous enough to preserve real structure for the common case while still bounding a pathological high-cardinality pair; every cell beyond the cap collapses into one ("other","other") catch-all cell rather than being dropped silently.

View Source
const MinPairObservations = 30

MinPairObservations is the minimum number of co-occurring, non-null observations a captured pair needs before its reconstruction parameters are trusted. This threshold and the warning mechanism below are deliberately generic across pair *kind* — numeric-numeric is the only kind this story populates, but categorical and set_* pairs (later epics) reuse the same threshold and the same thinPairWarning helper rather than inventing their own. Below the threshold the profile still ships the pair — a thin pair is never a refusal — but records a warning on Profile.Warnings so a caller downstream of profile capture knows the figure may be unstable.

View Source
const MinShapeFitObservations = 50

MinShapeFitObservations is the minimum number of retained samples a numeric field needs before ProfileOptions.FitShape attempts a mixture fit at all. Below this threshold the field falls back to the plain normal capture unconditionally — mirroring MinPairObservations' philosophy elsewhere in this package (a thin sample can't support a richer model), though here it gates whether a fit is even ATTEMPTED rather than emitting a warning about an already-shipped result.

View Source
const ModelRecoveryTolerance = 0.10

ModelRecoveryTolerance is the absolute latent-scale gap at which a recovered coefficient is FLAGGED as evidence of a generation fault rather than merely reported.

The unit is one standard deviation of the target's own reconstructed marginal (see this file's header on the latent scale), so the number carries the same meaning for a dollar-denominated spend field and an 11-point NPS scale — which is the whole reason the comparison is put on that scale before a threshold is applied to it. 0.10 SD is half of Cohen's small-effect boundary: a coefficient recovered within it is describing the same relationship the capture described, whatever the remaining digits do.

It is a floor, not the whole test. A coefficient estimated from a few thousand synthetic rows carries real sampling noise — a level at 5% prevalence over 2,000 rows has a standard error near 0.10 on its own — so a gap is flagged only when it exceeds BOTH this floor and modelRecoverySEMultiple times the refit's own standard error for that coefficient. A gap smaller than the estimator's noise cannot be evidence of anything, and a report that cries wolf on thin levels is one nobody reads to the bottom of.

View Source
const ResidualRecoveryTolerance = 0.10

ResidualRecoveryTolerance is the absolute gap between a captured residual correlation and the recovered one at which the pair is FLAGGED rather than merely reported.

A correlation is already unit-free, so unlike ModelRecoveryTolerance this number needs no scale argument to be comparable across fields — 0.10 is the same statement about a dollar-denominated spend pair and an 11-point NPS pair. The magnitude is chosen to match ModelRecoveryTolerance's, so the two halves of the model-recovery report flag at the same severity and a reader does not have to hold two thresholds in mind: a relationship recovered within a tenth is the relationship that was captured, whatever the remaining digits do.

Unlike the coefficient band there is no standard-error term. A Pearson correlation over the thousands of co-present rows this section measures on has a standard error well under 0.02, so a noise-scaled band would collapse onto the floor in every case that matters; the pairs where it would not are the thin ones, and those are already separated out as unmeasured rather than reported with a wide error bar.

View Source
const ResidualUnmeasuredNoModelFit = "no_model_fit"

ResidualUnmeasuredNoModelFit is a RECOVERY-side reason with no capture-side counterpart: the pair's endpoints both participated in generation's correlated draw, but one of their models could not be refitted on the synthetic partition at all (see ModelFidelity.Error), so there is no residual vector to correlate.

It is defined here rather than beside the ResidualUnmeasured* constants in synth/residual_corr.go on purpose. That vocabulary is the closed set a `residual_correlations.unmeasured` entry in a PROFILE DOCUMENT can carry, and a capture can never produce this reason — there is no refit at capture time. Mixing it in would widen a documented file-format vocabulary for a value that will never appear in a file.

View Source
const SyntheticFieldName = "_synthetic"

SyntheticFieldName is the provenance column every AugmentFromProfile output cohort carries: false for rows copied from the source cohort, true for newly generated rows.

Variables

This section is empty.

Functions

func AllDistributions

func AllDistributions() []string

AllDistributions returns the registered kind names in sorted order. Used by the manifest and tests.

func BuildCategoricalNumericPairwise added in v0.32.0

func BuildCategoricalNumericPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, pairs []CategoricalNumericPairSpec)

BuildCategoricalNumericPairwise computes each categorical-numeric pair's per-category conditional mean/std delta and appends the resulting CategoricalNumericPairFidelity entries onto report.CategoricalNumericPairwise. mergedSchema/records must be the same physical schema and record buffer BuildFidelityReport was given.

pairs is typically Spec.CategoricalNumericPairs — the source's captured per-category conditional mean/std synth/conditional_sample.go's categoricalNumericPairSampler draws from. A category with no realized synthetic observation gets an Error entry rather than aborting the rest of that pair's categories or the report — the same non-fatal contract every other Fidelity entry kind follows. As with BuildCategoricalPairwise, this function takes no warnings parameter: thin-cell warnings for this pair kind already ride Profile.Warnings through BuildPairwise's copy.

func BuildCategoricalPairwise added in v0.32.0

func BuildCategoricalPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, pairs []CategoricalPairSpec)

BuildCategoricalPairwise computes each categorical-categorical pair's contingency-table delta (categoricalTVD) and appends the resulting CategoricalPairFidelity entries onto report.CategoricalPairwise. mergedSchema/records must be the same physical schema and record buffer BuildFidelityReport was given for report's Fields section.

pairs is typically Spec.CategoricalPairs — the source's captured contingency cells synth/conditional_sample.go's categoricalPairSampler was built from, not a fresh re-derivation of it.

A pair whose synthetic partition produced no co-occurring observation at all gets an Error entry rather than aborting the rest of the pairs or the report — the same non-fatal contract BuildPairwise's own per-pair entries follow. This function takes no warnings parameter of its own: thin-cell warnings for this pair kind already ride Profile.Warnings alongside the numeric-pair warnings BuildPairwise copies onto report.Warnings, so giving this function a second copy point would either duplicate them or require the caller to split one warnings slice by kind for no reason — one shared shape, one place it lands.

func BuildModelFidelity added in v0.33.0

func BuildModelFidelity(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, spec *Spec)

BuildModelFidelity appends one ModelFidelity entry per APPLIED model onto report.Models. mergedSchema/records must be the same physical schema and record buffer BuildFidelityReport was given.

spec is the Spec generate() was given, unpruned. Which models were applied is not guessed from it: resolveConflicts and buildModelDrawers are both pure functions of the Spec (conflicts are static, and drawer compilation consumes no RNG), so calling them again here reproduces the exact surviving set generation compiled — including the two warn-and-skip refusals buildModelDrawers makes on its own, a target absent from the schema and a target whose marginal has no scale. That is the whole of how this section keeps the v0.32.2 promise: it cannot report a model generation did not run, because it asks generation's own compiler which ones those were.

The synthetic partition is decoded ONCE (decodeSyntheticRows, the cache v0.32.1 introduced to kill an O(pairs × records) blowup in this same report) and every model refits over that one slice. A refit per field with its own full decode would be exactly that bug in a new place, at 105 fields.

func BuildPairwise added in v0.32.0

func BuildPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, pairs []CorrelationSpec, warnings []string)

func BuildSetCategoricalPairwise added in v0.32.0

func BuildSetCategoricalPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, pairs []SetCategoricalPairSpec)

BuildSetCategoricalPairwise computes each set-option x categorical pair's contingency-table delta (categoricalTVD, reused verbatim from BuildCategoricalPairwise — SetCategoricalPairSpec.Cells is the same []CategoricalPairCellSpec shape) and appends the resulting SetCategoricalPairFidelity entries onto report.SetCategoricalPairwise (E5-S4). mergedSchema/records must be the same physical schema and record buffer BuildFidelityReport was given.

pairs is typically Spec.SetCategoricalPairs — the source's captured contingency cells synth/conditional_sample.go's setCategoricalPairSampler was built from. A pair whose synthetic partition produced no co-occurring observation at all gets an Error entry rather than aborting the rest of the pairs or the report — the same non-fatal contract BuildCategoricalPairwise's own per-pair entries follow.

func BuildSetNumericPairwise added in v0.32.0

func BuildSetNumericPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, pairs []SetNumericPairSpec)

BuildSetNumericPairwise computes each set-option x numeric pair's per-bucket conditional mean/std delta and appends the resulting SetNumericPairFidelity entries onto report.SetNumericPairwise (E5-S4). mergedSchema/records must be the same physical schema and record buffer BuildFidelityReport was given.

pairs is typically Spec.SetNumericPairs. A bucket with no realized synthetic observation gets an Error entry rather than aborting the rest of that pair's categories or the report — the same non-fatal contract BuildCategoricalNumericPairwise's own entries follow.

func BuildSetSetPairwise added in v0.32.0

func BuildSetSetPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, pairs []SetSetPairSpec)

BuildSetSetPairwise computes each option x option pair's 2x2 contingency-table delta between two DIFFERENT set_* fields (categoricalTVD, reused verbatim — SetSetPairSpec.Cells is the same []CategoricalPairCellSpec shape) and appends the resulting SetSetPairFidelity entries onto report.SetSetPairwise (E5-S4). mergedSchema/records must be the same physical schema and record buffer BuildFidelityReport was given.

pairs is typically Spec.SetSetPairs. A pair whose synthetic partition produced no co-occurring observation at all gets an Error entry rather than aborting the rest of the pairs or the report.

func CountWarningsNeedingAttention added in v0.33.0

func CountWarningsNeedingAttention(groups []WarningGroup) (attention, informational int)

CountWarningsNeedingAttention returns how many of the grouped warnings are in an Attention kind, and how many are expected outcomes. The two sum to the total, and separating them is what keeps a headline count meaningful on a cohort whose expected-outcome lines outnumber the real findings by three orders of magnitude.

func ResolveConflicts added in v0.32.2

func ResolveConflicts(s *Spec) (
	catPairs []CategoricalPairSpec,
	catNumPairs []CategoricalNumericPairSpec,
	setCatPairs []SetCategoricalPairSpec,
	setNumPairs []SetNumericPairSpec,
	setSetPairs []SetSetPairSpec,
	correlations []CorrelationSpec,
)

ResolveConflicts is the exported form of resolveConflicts: it prunes spec's captured conditional-pairing relationships down to exactly the subset generate() applies at row-draw time, in the same priority order (captured-shape pre-claim, then catPairs -> catNumPairs -> setSetPairs -> setCatPairs -> setNumPairs -> correlations).

Spec.Models is arbitrated by the same pass but deliberately NOT returned here: this function's contract is the six PAIRWISE relationship lists a fidelity report scores, and a model is not a pairwise relationship. Its arbitration is still visible through this call's effect on the lists — a correlation or numeric-target pair the model pre-claim excluded is absent from what comes back.

resolveConflicts is pure and deterministic in spec (conflicts are static for a given Spec — see resolveConflicts's own doc), so calling it again here against the same *Spec generate() was given reproduces the identical resolution without threading a return value through Result. Exists for callers building a fidelity report against a Spec's FULL captured relationships — e.g. writeSynthFidelityReport — that must restrict their comparison to relationships generation actually used: fidelity-checking a pair generation dropped would score a "delta" for a relationship that was never modeled, which is misleading rather than merely wasteful (see skills/synthetic-data.md, Fidelity report).

func SyntheticAsCategoricalSchema added in v0.32.0

func SyntheticAsCategoricalSchema(schema *encoding.Schema) *encoding.Schema

SyntheticAsCategoricalSchema returns a copy of schema whose SyntheticFieldName field is redeclared as categorical_u8 with a two-entry dictionary ({"false","true"} at IDs 0/1 — matching packed_bool's 0/1 on-wire values verbatim, no value remapping). TEST_KS.SplitBy and TEST_CHISQ's Rows/Cols both require a categorical field, but _synthetic itself is packed_bool (see CLAUDE.md Byte-layout invariants) — this presentation-only schema is what lets the two unmodified operators treat it as their split/contingency field.

It is never used to decode bytes — see TestRunner's physicalSchema contract — only to give a TestRunner's constructed Record something whose Field(SyntheticFieldName).Type reads back as categorical.

Types

type CategoricalNumericCategoryFidelity added in v0.32.0

type CategoricalNumericCategoryFidelity struct {
	Category string `json:"category"`
	// SourceMean/SourceStd echo the captured
	// CategoricalNumericCategorySpec verbatim.
	SourceMean float64 `json:"source_mean"`
	SourceStd  float64 `json:"source_std"`
	// SyntheticMean/SyntheticStd are the realized conditional mean/std
	// over the _synthetic=true partition. MeanDelta/StdDelta are
	// abs(source-synthetic). All four are zero-valued and meaningless
	// when Error is set.
	SyntheticMean float64 `json:"synthetic_mean,omitempty"`
	SyntheticStd  float64 `json:"synthetic_std,omitempty"`
	MeanDelta     float64 `json:"mean_delta,omitempty"`
	StdDelta      float64 `json:"std_delta,omitempty"`
	// N is the number of _synthetic=true rows carrying this category
	// value with a non-null B — the realized category's own observation
	// count.
	N int `json:"n"`
	// Error is set instead of the Synthetic*/*Delta fields when the
	// synthetic partition realized no observation of this category at
	// all — mirrors PairwiseFidelity's own per-entry non-fatal contract.
	Error string `json:"error,omitempty"`
}

CategoricalNumericCategoryFidelity is one observed category's conditional mean/std delta — the categorical-numeric analogue of PairwiseFidelity, one entry per category rather than one entry per pair, since a categorical-numeric pair reconstructs a whole distribution per category rather than a single scalar.

type CategoricalNumericCategorySpec added in v0.32.0

type CategoricalNumericCategorySpec struct {
	Category string  `json:"category"`
	Mean     float64 `json:"mean"`
	Std      float64 `json:"std"`
}

CategoricalNumericCategorySpec is the numeric field's conditional mean/std for one observed category value (or the "other" catch-all) of the paired categorical field — the Spec-facing counterpart of CategoricalNumericCategoryStat.

type CategoricalNumericCategoryStat added in v0.32.0

type CategoricalNumericCategoryStat struct {
	Category string  `json:"category"`
	Mean     float64 `json:"mean"`
	Std      float64 `json:"std"`
	// N is the number of non-null B observations conditioned on this
	// category — the input thinPairWarning checks against
	// MinPairObservations, exactly as NumericPairProfile.N and each
	// ContingencyCell.Count already do for the other two pair kinds.
	N int `json:"n"`
}

CategoricalNumericCategoryStat is the numeric field's conditional mean/std/observation-count for one observed category value (or the otherCategoryLabel catch-all) of the paired categorical field.

type CategoricalNumericPairFidelity added in v0.32.0

type CategoricalNumericPairFidelity struct {
	A          string                                `json:"a"`
	B          string                                `json:"b"`
	Categories []*CategoricalNumericCategoryFidelity `json:"categories,omitempty"`
}

CategoricalNumericPairFidelity is one row of FidelityReport.CategoricalNumericPairwise: the per-category delta between a captured categorical-numeric pair's source conditional mean/std (CategoricalNumericPairSpec.Categories — the same figures synth/conditional_sample.go's categoricalNumericPairSampler draws from) and that category's REALIZED conditional mean/std over the _synthetic=true partition of the output cohort.

This section and Models are DISJOINT by construction (E5-S2)

A numeric target reached by a linear model has NO entry here, ever. The pick-one sampler this section scores does not run for such a field — the model owns its value — so an entry would be a delta for a mechanism that never executed, which is the v0.32.2 bug class. Two independent guards make it impossible rather than merely unlikely: SpecFromProfile leaves a modelled target's captured pair off the Spec entirely (per TARGET, not per document — an unmodelled numeric in the same profile keeps its pair and is still scored here), and resolveConflicts pre-claims every modelled field ahead of the categorical-numeric stage, which catches a hand-authored spec declaring both. So this section covers exactly the numerics that kept the pick-one mechanism, `models` covers exactly the ones that did not, and the two field sets never intersect. The same rule applies verbatim to SetNumericPairFidelity; the three NON-numeric-target sections (CategoricalPairFidelity, SetCategoricalPairFidelity, SetSetPairFidelity) are untouched by any of it, because a linear model targets a numeric and subsumes nothing they describe.

type CategoricalNumericPairProfile added in v0.32.0

type CategoricalNumericPairProfile struct {
	A          string                           `json:"a"` // categorical field name
	B          string                           `json:"b"` // numeric field name
	Categories []CategoricalNumericCategoryStat `json:"categories"`
	// N is the number of rows in the capture where both A and B were
	// simultaneously non-null, summed across every category (including
	// "other") — the pair's true co-occurrence count, mirroring
	// CategoricalPairProfile.N and NumericPairProfile.N.
	N int `json:"n"`
}

CategoricalNumericPairProfile is one captured categorical-numeric pair's reconstruction input: the numeric field B's conditional mean/std, broken out per observed category of the categorical field A. Categories already reflect A's own per-field top-K cap (ProfileOptions.TopK, via FieldProfile.Categorical.Top) — any category outside A's own top-K collapses into one otherCategoryLabel bucket before its conditional numeric summary is computed, the same per-field collapse CategoricalPairProfile applies on each of its own axes. Unlike CategoricalPairProfile there is no second, joint-cell cap: the number of retained Categories entries is already bounded by ProfileOptions.TopK (plus one "other" bucket), since this section captures one numeric summary per category rather than a joint table over two categorical axes.

type CategoricalNumericPairSpec added in v0.32.0

type CategoricalNumericPairSpec struct {
	A          string                           `json:"a"`
	B          string                           `json:"b"`
	Categories []CategoricalNumericCategorySpec `json:"categories"`
	// Min/Max clamp the conditional draw exactly as field B's own
	// unconditional normal reconstruction clamps to its observed range
	// (see SpecFromProfile). HasClamp distinguishes "clamp to [0,0]"
	// (rare, but a legitimate observed range for a constant field) from
	// "no clamp was declared" — a hand-authored from-schema spec that
	// omits Min/Max gets no clamping, exactly as normal's own optional
	// min/max params behave when absent.
	Min      float64 `json:"min,omitempty"`
	Max      float64 `json:"max,omitempty"`
	HasClamp bool    `json:"has_clamp,omitempty"`
}

CategoricalNumericPairSpec is one categorical-numeric pair's generation-time reconstruction input — the Spec-facing counterpart of CategoricalNumericPairProfile, carrying only what a conditional resample needs (each category's mean/std, plus the numeric field's own overall min/max so the conditional draw clamps exactly as the field's unconditional `normal` reconstruction already does; N is capture-time provenance and is not needed here).

type CategoricalPairCellSpec added in v0.32.0

type CategoricalPairCellSpec struct {
	AValue string `json:"a_value"`
	BValue string `json:"b_value"`
	Count  int    `json:"count"`
}

CategoricalPairCellSpec is one observed (a_value, b_value) co-occurrence count — the Spec-facing counterpart of ContingencyCell.

type CategoricalPairFidelity added in v0.32.0

type CategoricalPairFidelity struct {
	A string `json:"a"`
	B string `json:"b"`
	// Delta is the total variation distance between the source and
	// synthetic joint-frequency tables (each normalized to proportions
	// before comparison), in [0, 1] — 0 when the two tables carry
	// identical support and proportions, 1 when they share none. Chosen
	// over a chi-square-style distance because it stays well-defined and
	// boundedly comparable to a fixed tolerance even when synthesis
	// realizes a cell the capture never saw (or vice versa) — a
	// chi-square statistic diverges when an expected cell is zero. Zero
	// valued and meaningless when Error is set.
	Delta float64 `json:"delta,omitempty"`
	// N is the number of _synthetic=true rows with both A and B
	// simultaneously non-null and dictionary-resolvable — the realized
	// pair's own co-occurrence count. CategoricalPairSpec carries no
	// source N of its own; the source total used to normalize its side
	// of the comparison is recovered by summing Cells' Count.
	N int `json:"n"`
	// Error is set instead of Delta when the synthetic partition
	// produced no co-occurring observation at all for this pair — mirrors
	// PairwiseFidelity's own per-pair non-fatal failure contract.
	Error string `json:"error,omitempty"`
}

CategoricalPairFidelity is one row of FidelityReport.CategoricalPairwise: the delta between a captured categorical-categorical pair's source joint-frequency table (CategoricalPairSpec.Cells — the same contingency cells synth/conditional_sample.go's categoricalPairSampler was built from) and that pair's REALIZED joint-frequency table over the _synthetic=true partition of the output cohort, expressed as total variation distance (see categoricalTVD) — the categorical-pair analogue of PairwiseFidelity's numeric-numeric correlation delta.

type CategoricalPairProfile added in v0.32.0

type CategoricalPairProfile struct {
	A     string            `json:"a"`
	B     string            `json:"b"`
	Cells []ContingencyCell `json:"cells"`
	// N is the number of rows in the capture where both A and B were
	// simultaneously non-null — the pair's true co-occurrence count,
	// summed across every cell (the cells kept plus whatever is folded
	// into the "other"/"other" catch-all).
	N int `json:"n"`
}

CategoricalPairProfile is one captured categorical-categorical pair's reconstruction input: a bounded contingency table of observed (a_value, b_value) co-occurrence counts. Values already reflect each field's own per-field top-K cap (ProfileOptions.TopK, via each field's own FieldProfile.Categorical.Top) — any category outside a field's own top-K collapses to "other" before the joint table is built — and the table is further bounded to at most ContingencyCellCap cells by co-occurrence count, with everything past that cut folded into one ("other","other") catch-all cell. The two caps compose: the existing per-field cap is an input bound to this story's new joint-cell cap, never replaced by it.

type CategoricalPairSpec added in v0.32.0

type CategoricalPairSpec struct {
	A     string                    `json:"a"`
	B     string                    `json:"b"`
	Cells []CategoricalPairCellSpec `json:"cells"`
}

CategoricalPairSpec is one categorical-categorical pair's generation-time reconstruction input — the Spec-facing counterpart of CategoricalPairProfile, carrying only what a conditional resample needs (the cells; N is capture-time provenance and is not needed here).

type CategoricalProfile

type CategoricalProfile struct {
	Cardinality int           `json:"cardinality"`
	Top         []CategoryHit `json:"top"`
}

CategoricalProfile holds the top-K observed values and the total distinct count.

type CategoryHit

type CategoryHit struct {
	Value  string  `json:"value"`
	Weight float64 `json:"weight"`
}

CategoryHit records a categorical value with its observed weight.

type ConditionalProfile added in v0.32.0

type ConditionalProfile struct {
	// NumericPairs lists the numeric-numeric pairs captured: the
	// row-aligned Pearson correlation and the count of rows where both
	// fields were simultaneously non-null (N) — the input
	// thinPairWarning checks against MinPairObservations. Capped at
	// ProfileOptions.CorrelationTopK strongest |rho| pairs, same
	// convention as Profile.Pairwise.
	NumericPairs []NumericPairProfile `json:"numeric_pairs,omitempty"`
	// CategoricalPairs lists the categorical-categorical pairs
	// captured: a bounded contingency table of observed
	// (a_value, b_value) co-occurrence counts per pair. See
	// CategoricalPairProfile for the two composed collapse points
	// (per-field top-K, then joint-cell ContingencyCellCap) that keep
	// this section bounded regardless of either field's cardinality.
	CategoricalPairs []CategoricalPairProfile `json:"categorical_pairs,omitempty"`
	// CategoricalNumericPairs lists the categorical-numeric pairs
	// captured: the numeric field's conditional mean/std broken out per
	// observed category of the categorical field. See
	// CategoricalNumericPairProfile for the single collapse point
	// (per-field top-K) that keeps this section bounded — no separate
	// joint-cardinality cap is needed here, unlike CategoricalPairs,
	// because a conditional numeric summary is one row per already-capped
	// category rather than a joint cell per pair of categories.
	CategoricalNumericPairs []CategoricalNumericPairProfile `json:"categorical_numeric_pairs,omitempty"`
	// SetCategoricalPairs lists the set-option x categorical pairs
	// captured (E5-S2): one bounded contingency table per (set field,
	// option, categorical field) combination, the set-option axis fixed
	// to the two-value {"selected","not_selected"} Bernoulli domain. See
	// SetCategoricalPairProfile.
	SetCategoricalPairs []SetCategoricalPairProfile `json:"set_categorical_pairs,omitempty"`
	// SetNumericPairs lists the set-option x numeric pairs captured
	// (E5-S2): one conditional mean/std pair (selected vs. not_selected)
	// per (set field, option, numeric field) combination. See
	// SetNumericPairProfile.
	SetNumericPairs []SetNumericPairProfile `json:"set_numeric_pairs,omitempty"`
	// SetSetPairs lists the option x option pairs captured between two
	// DIFFERENT set_* fields (E5-S2): a 2x2 contingency table per
	// (fieldA option, fieldB option) combination. Never captured between
	// two options of the same field. See SetSetPairProfile.
	SetSetPairs []SetSetPairProfile `json:"set_set_pairs,omitempty"`
}

ConditionalProfile carries the joint reconstruction structure `profile create --conditional` captures beyond independent per-field marginals. E2-S1 covers numeric-numeric pairs; E3-S1 adds categorical-categorical pairs; E3-S2 adds categorical-numeric pairs — all to the same struct rather than a new top-level section, so the same nil-check keeps gating all of them. E5-S2 extends this to pairs involving a set_* field: SetCategoricalPairs, SetNumericPairs and SetSetPairs, one entry PER OPTION (bit position) rather than per field — E5-S1's design decision that each dictionary entry is its own independent Bernoulli sub-field composes directly with E3's pairwise machinery by running it once per option, reusing ContingencyCell / ContingencyCellCap / condCatNumAcc verbatim rather than inventing a parallel mechanism for set_* fields.

type ConstraintSpec

type ConstraintSpec struct {
	Expr string `json:"expr"`
}

ConstraintSpec wraps an expression evaluated against the in-memory row.

type ContingencyCell added in v0.32.0

type ContingencyCell struct {
	AValue string `json:"a_value"`
	BValue string `json:"b_value"`
	Count  int    `json:"count"`
}

ContingencyCell is one observed (a_value, b_value) combination and its co-occurrence count, as captured by --conditional's categorical-categorical contingency table. AValue/BValue read "other" (otherCategoryLabel) when the underlying value fell outside its field's own top-K, or when the cell itself fell outside the pair's ContingencyCellCap.

type CorrelationSpec

type CorrelationSpec struct {
	A           string  `json:"a"`
	B           string  `json:"b"`
	Correlation float64 `json:"correlation"`
}

CorrelationSpec declares a target Pearson correlation between two numeric fields.

type CorrelationStat

type CorrelationStat struct {
	A   string  `json:"a"`
	B   string  `json:"b"`
	Rho float64 `json:"rho"`
}

CorrelationStat is a captured pairwise correlation entry.

type DateProfile

type DateProfile struct {
	Start    string `json:"start"`
	End      string `json:"end"`
	Weekdays [7]int `json:"weekdays"`
}

DateProfile holds the (start, end) range of date values plus a weekday histogram for Mode-A reconstruction.

type FidelityReport added in v0.32.0

type FidelityReport struct {
	SourceRows                 int                               `json:"source_rows"`
	SyntheticRows              int                               `json:"synthetic_rows"`
	Fields                     []*FieldFidelity                  `json:"fields,omitempty"`
	Pairwise                   []*PairwiseFidelity               `json:"pairwise,omitempty"`
	CategoricalPairwise        []*CategoricalPairFidelity        `json:"categorical_pairwise,omitempty"`
	CategoricalNumericPairwise []*CategoricalNumericPairFidelity `json:"categorical_numeric_pairwise,omitempty"`
	SetFields                  []*SetFieldFidelity               `json:"set_fields,omitempty"`
	SetCategoricalPairwise     []*SetCategoricalPairFidelity     `json:"set_categorical_pairwise,omitempty"`
	SetNumericPairwise         []*SetNumericPairFidelity         `json:"set_numeric_pairwise,omitempty"`
	SetSetPairwise             []*SetSetPairFidelity             `json:"set_set_pairwise,omitempty"`
	// Models is the per-field MODEL-RECOVERY section (E5-S1): for every
	// linear model generation actually applied, the captured coefficients
	// beside the ones a refit on the generated partition recovers. It
	// answers a different question from every section above — those ask
	// whether the generated rows LOOK like the source, this asks whether
	// the captured STRUCTURE survived generation — and it is the only
	// section that can tell a faithful generation whose marginal
	// contrasts are merely compressed apart from one that silently lost
	// its conditioning. Absent (omitempty) for every spec carrying no
	// `models`, which is every spec predating `profile create
	// --fit-models`. See synth/fidelity_models.go.
	Models []*ModelFidelity `json:"models,omitempty"`
	// ModelResidualCorrelations is the residual-correlation half of the
	// same model-recovery question (E5-S2): for every residual
	// correlation generation applied, the captured rho beside the rho
	// recovered from the generated partition. It is deliberately NOT
	// folded into Pairwise, which scores the value-scale copula arm over
	// a DISJOINT set of fields — a modelled field is excluded from
	// Spec.Correlations by resolveConflicts precisely so the two arms
	// never both fire on one field. The two section names are what tell
	// a reader scanning this document which mechanism a given number
	// describes. Absent (omitempty) for every spec carrying no
	// `residual_correlations`, which is every spec predating `profile
	// create --residual-correlations`. See synth/fidelity_residual.go.
	ModelResidualCorrelations *ModelResidualCorrelationFidelity `json:"model_residual_correlations,omitempty"`
	Warnings                  []string                          `json:"warnings,omitempty"`
}

FidelityReport is the JSON document written to Options.FidelityReportPath, comparing the freshly generated rows against the source cohort's rows inside the one combined output. Fields is the per-field marginal section (E1-S2); Pairwise is the numeric-numeric correlation-delta section (E2-S3); CategoricalPairwise and CategoricalNumericPairwise are the categorical-categorical contingency-delta and categorical-numeric conditional-mean/std-delta sections (E3-S4); SetFields is the set_* per-option marginal frequency-delta section, and SetCategoricalPairwise / SetNumericPairwise / SetSetPairwise are the three set_* joint-structure delta sections (E5-S4) — all populated only when the spec/schema carried at least one entry of the matching kind — absent (omitempty), never an empty placeholder array, on every plain synthesis and every from-profile run with no captured structure of that kind. Warnings mirrors any thin-pair/thin-cell warnings the caller supplied (see Options.FidelityWarnings) — typically Profile.Warnings from a --conditional capture, covering every pair kind through the same shared shape — so a reader sees "how well did it match" (the pairwise sections) and "which parts were built on thin data" (Warnings) in one document (FR-18).

func BuildFidelityReport added in v0.32.0

func BuildFidelityReport(mergedSchema *encoding.Schema, records []byte, sourceRows, syntheticRows int, run TestRunner) *FidelityReport

BuildFidelityReport compares every eligible field of mergedSchema (skipping SyntheticFieldName itself) between the _synthetic=false and _synthetic=true partitions of records: TEST_KS for numeric fields, TEST_CHISQ for categorical fields, both driven through run; a direct per-option frequency comparison (computeSetFieldFidelity, no run/TestRunner involved) for set_* fields, appended onto report.SetFields instead of report.Fields (E5-S4 — see SetFieldFidelity for why this section does not reuse TEST_KS/ TEST_CHISQ). Fields of any other on-wire type (date, datetime, packed_bool, u4) are out of scope for both sections; later stories may widen coverage.

A single field's failing test (entry.Error set, entry.Result nil) never aborts the rest of the report — run is called once per field on its own Test, not once for every field on a shared Request.

type FieldFidelity added in v0.32.0

type FieldFidelity struct {
	Field  string            `json:"field"`
	Test   types.TestType    `json:"test"`
	Result *types.TestResult `json:"result,omitempty"`
	Error  string            `json:"error,omitempty"`
}

FieldFidelity is one row of FidelityReport.Fields: the marginal comparison between the source and generated partitions of a single output-cohort field, computed by driving the field's numeric values through TEST_KS (SplitBy=_synthetic) or its categorical values through TEST_CHISQ (contingency against _synthetic) — never new comparison math, always the existing operator run through the ordinary Process pipeline (see TestRunner).

Result is nil (Error carries the reason instead) when the operator itself could not produce a statistic for this field — e.g. a categorical column with fewer than 2 distinct values, or a numeric field with fewer than 2 observations in one partition — which never fails the surrounding synth call; a fidelity check that cannot run for one field must not withhold the report for every other field, let alone the generated cohort itself.

type FieldModel added in v0.33.0

type FieldModel struct {
	// Field is the numeric target this model predicts.
	Field string `json:"field"`
	// Intercept is the fitted constant term: the predicted value for a
	// row sitting at every categorical's reference level (see
	// References) with no set option selected.
	Intercept float64 `json:"intercept"`
	// Predictors lists the fitted coefficients in design-column order.
	// Empty is legal and meaningful only in the sense that such a model
	// is never retained — a target with no usable candidate columns is
	// skipped, not stored with an empty predictor list.
	Predictors []ModelPredictor `json:"predictors"`
	// References names, per categorical predictor field, the level
	// dropped from Predictors and folded into Intercept. Absent
	// (omitempty) exactly when the model carries no categorical
	// predictor at all — a set-only model has nothing to drop, since a
	// multi-select is not a partition — which a reader can confirm from
	// predictors[].kind rather than having to assume. See ModelReference
	// for why the baseline is written down instead of inferred.
	References []ModelReference `json:"references,omitempty"`
	// NObs is the number of rows that actually contributed to the fit.
	// The engine applies listwise deletion, so a row with a null target
	// or a null predictor SOURCE field contributes nothing and this is
	// below the profile's RowCount whenever the cohort has nulls.
	NObs int `json:"n_obs"`
	// R2 and ResidualStd are the fit's explanatory power and residual
	// scale. ResidualStd is the standard deviation of the residual that
	// generation adds back on top of the linear predictor.
	R2          float64 `json:"r2"`
	ResidualStd float64 `json:"residual_std"`
	// ShrinkageAlpha is the ridge penalty this model was fitted under,
	// absent (omitempty, and therefore zero) when it was fitted by plain
	// unpenalized OLS.
	//
	// It is on the wire because a shrunk coefficient and a free one are
	// not the same kind of number, and nothing else in the document says
	// which was produced. A reader comparing two models' coefficients —
	// or comparing one model against a hand-run regression — needs to
	// know that at least one of this model's levels was thin enough to
	// pull the whole design toward its baselines, and by how much: the
	// penalty is minLevelObservations pseudo-observations spread over
	// NObs rows, so alpha·NObs recovers the pseudo-count. See
	// profile_models_shrink.go for the construction.
	ShrinkageAlpha float64 `json:"shrinkage_alpha,omitempty"`
	// Residuals is the row-aligned fitted residual (observed − predicted)
	// for each row retained in the capture's residual reservoir, and
	// ResidualPresent[i] reports whether row i contributed one at all —
	// a row listwise-deleted for THIS field has no residual even though
	// another field's model may have one for the same row.
	//
	// Every FieldModel from one capture indexes the SAME retained rows,
	// which is the whole point: residual correlation between two fields
	// is only meaningful on rows where both residuals exist.
	//
	// Both are `json:"-"` — DELIBERATELY not part of the document. They
	// are a bounded row-aligned SAMPLE (modelResidualCap entries per
	// model, per field), so serialising them would add tens of thousands
	// of numbers per numeric column to a document whose every other
	// section is a handful of summary statistics, and would do it to
	// carry rows a profile exists precisely to avoid retaining. Their
	// consumer is the residual-correlation measurement, which runs in
	// the SAME process as the capture and reads them straight off
	// FittedModels(); what belongs in the document is the small derived
	// quantity that measurement produces, not its input. A Profile read
	// back from JSON therefore has coefficients and ResidualStd but nil
	// Residuals, and any consumer that needs per-row residuals must
	// capture rather than re-read.
	Residuals       []float64 `json:"-"`
	ResidualPresent []bool    `json:"-"`
}

FieldModel is one numeric field's fitted linear predictor.

The additive form is Intercept + Σ Coefficient·column, where each column is 1 when the row carries that categorical level or has that set option selected and 0 otherwise. A categorical contributes one column per level EXCEPT its reference level (see modelColumns), whose effect is folded into Intercept — that is what keeps the design full-rank alongside an intercept.

type FieldModelSpec added in v0.33.0

type FieldModelSpec struct {
	// Field is the numeric field this model draws.
	Field string `json:"field"`
	// Intercept is the constant term — the prediction for a row sitting
	// at every categorical predictor's reference level with no set
	// option selected.
	Intercept float64 `json:"intercept"`
	// Predictors are the per-(field, level) contributions summed into
	// the prediction. A row whose drawn value for a predictor's Field is
	// not that predictor's Level contributes nothing for it.
	Predictors []ModelPredictorSpec `json:"predictors"`
	// ResidualStd is the scale of the residual added on top of the
	// linear prediction. Zero means a deterministic prediction, which is
	// a legal (if degenerate) model rather than an error.
	ResidualStd float64 `json:"residual_std"`
	// Min/Max are the numeric field's observed bounds, carried alongside
	// HasClamp exactly as CategoricalNumericPairSpec and
	// SetNumericPairSpec already carry them. They are the same bounds
	// the field's own reconstructed marginal would clamp to; whether and
	// where the draw applies them is the generation stage's decision,
	// and this slot only guarantees the numbers are available to it
	// without a second lookup into the profile.
	Min      float64 `json:"min,omitempty"`
	Max      float64 `json:"max,omitempty"`
	HasClamp bool    `json:"has_clamp,omitempty"`
}

FieldModelSpec is one numeric field's additive linear predictor as generation consumes it: the Spec-facing counterpart of FieldModel, carrying only what a draw needs.

It is a distinct type from FieldModel rather than a re-use of it for the same reason every *PairProfile has its own *PairSpec: the profile shape is a MEASUREMENT (it carries n_obs, R2 and a residual reservoir that describe how well the fit was supported), while the spec shape is an INSTRUCTION (it carries the clamp bounds the profile keeps on the numeric field summary instead). Collapsing them would drag capture diagnostics into a hand-authorable generation surface.

type FieldProfile

type FieldProfile struct {
	Name        string              `json:"name"`
	Type        string              `json:"type"`
	Description string              `json:"description,omitempty"`
	NullRate    float64             `json:"null_rate"`
	Numeric     *NumericProfile     `json:"numeric,omitempty"`
	Categorical *CategoricalProfile `json:"categorical,omitempty"`
	Date        *DateProfile        `json:"date,omitempty"`
	// Set carries per-option marginal selection-frequency stats for a
	// set_* (multi-select bitmask) field (E5-S1). Populated instead of
	// Numeric/Categorical/Date — exactly one of the four is non-nil per
	// field, based on the field's schema type.
	Set *SetProfile `json:"set,omitempty"`
	// Precision/Scale carry decimal128 metadata so synth-from-profile can
	// reconstruct the original field shape.
	Precision uint8 `json:"precision,omitempty"`
	Scale     uint8 `json:"scale,omitempty"`
}

FieldProfile holds per-field summary statistics. Exactly one of Numeric, Categorical, Date, or Set is populated based on the field's type.

type FieldSpec

type FieldSpec struct {
	Name         string         `json:"name"`
	Type         string         `json:"type"`
	Nullable     bool           `json:"nullable,omitempty"`
	Description  string         `json:"description,omitempty"`
	Distribution string         `json:"distribution"`
	Params       map[string]any `json:"params,omitempty"`

	// Precision and Scale apply to decimal128.
	Precision uint8 `json:"precision,omitempty"`
	Scale     uint8 `json:"scale,omitempty"`

	// NullRate is the per-row probability that the field will be null.
	// Only meaningful when Nullable is true; ignored otherwise.
	NullRate float64 `json:"null_rate,omitempty"`
}

FieldSpec is a single column declaration.

type ModelFidelity added in v0.33.0

type ModelFidelity struct {
	Field string `json:"field"`
	// Marginal marks an INTERCEPT-ONLY model — one whose predictor
	// selection admitted nothing (see profile_models_select.go's variance
	// floor). It is a COMPLETE model, not a failed capture: the linear
	// predictor is the field's own mean and the residual carries all of
	// its spread, so generation reproduces the marginal, which is the
	// correct answer for a field nothing explains. Such an entry carries
	// no Predictors and reports only the intercept comparison — which is
	// still a real check, since a drifted intercept on a marginal model
	// means the marginal itself did not survive.
	Marginal bool `json:"marginal,omitempty"`
	// LatentScale is the target's reconstructed marginal standard
	// deviation — the divisor that puts every captured coefficient below
	// on the latent scale. Multiply a latent figure by it to return to
	// the units the profile document's `models` section reports, which
	// for a `normal` target are the same units either way.
	LatentScale float64 `json:"latent_scale"`
	// CapturedIntercept / RecoveredIntercept are the latent-scale
	// intercepts: (Intercept − mean)/std as generation standardises it,
	// against the refit's own. InterceptDelta is their absolute
	// difference.
	CapturedIntercept  float64 `json:"captured_intercept"`
	RecoveredIntercept float64 `json:"recovered_intercept,omitempty"`
	InterceptDelta     float64 `json:"intercept_delta,omitempty"`
	// Predictors is one entry per compiled model term, in the order
	// generation evaluates them. Absent on a Marginal model.
	Predictors []*ModelPredictorFidelity `json:"predictors,omitempty"`
	// NObs is the number of synthetic rows the refit admitted: rows
	// carrying a non-null, latent-invertible target and a non-null value
	// for every predictor SOURCE field (listwise deletion, the same rule
	// capture's fit applied). Bounded by modelRecoveryRowCap.
	NObs int `json:"n_obs"`
	// R2 is the refit's own coefficient of determination on the latent
	// scale — how much of the generated latent the recovered model
	// explains. It is NOT comparable to the captured FieldModel.R2, which
	// was measured on real source rows against real source noise;
	// synthetic residuals are Gaussian by construction, so this figure
	// reads high and is here to characterise the refit, not to score it.
	R2 float64 `json:"r2,omitempty"`
	// MaxDelta is the largest absolute latent coefficient gap across
	// Predictors (the intercept excluded — a shifted intercept moves the
	// level, not the structure). Flagged is true when any predictor is.
	MaxDelta float64 `json:"max_delta,omitempty"`
	Flagged  bool    `json:"flagged,omitempty"`
	// Error is set instead of the recovered figures when the refit could
	// not run at all for this model — too few admitted rows, a design the
	// solver refused. It never withholds the rest of the report, the same
	// non-fatal per-entry contract every other Fidelity kind follows.
	Error string `json:"error,omitempty"`
}

ModelFidelity is one row of FidelityReport.Models: one APPLIED linear model's captured coefficients beside the coefficients recovered by refitting the same model on the output cohort's `_synthetic` partition.

Entries exist for exactly the models generation applied — the subset resolveConflicts left, further narrowed by buildModelDrawers' own warn-and-skip refusals, reproduced by calling both functions again against the same *Spec generate() was given. A field carrying no model has NO entry here (rather than an entry with empty values), and neither does a captured model that lost its target to an earlier claim or whose marginal generation refused: reporting a computed-looking delta for a relationship that was never applied is the v0.32.2 bug class, and it is worse than reporting nothing because the number looks like evidence.

type ModelPredictor added in v0.33.0

type ModelPredictor struct {
	// Column is the design-matrix column name the engine keyed the
	// coefficient by. Internal encoding; diagnostics and the in-process
	// residual replay only, never serialised — see the type comment.
	Column string `json:"-"`
	// Kind is one of the ModelPredictor* constants above. It is carried
	// on the wire because it is not derivable from (Field, Level): a
	// categorical level is one arm of a partition read against a dropped
	// reference (see FieldModel.References), while a set option is an
	// independent indicator with no reference at all, and a consumer
	// reconstructing effects has to know which it is holding.
	Kind string `json:"kind"`
	// Field is the real cohort field this column was expanded from.
	Field string `json:"field"`
	// Level is the categorical level text or set option text this column
	// indicates. Empty when Kind is ModelPredictorNumeric. Deliberately
	// NOT omitempty: it is half of the addressing key, and a categorical
	// dictionary may legitimately carry the empty-string level, so the
	// key is always written rather than left to be inferred from its
	// absence.
	Level string `json:"level"`
	// Coefficient is the fitted slope for this column.
	Coefficient float64 `json:"coefficient"`
}

ModelPredictor is one fitted coefficient: which real cohort field and level the design column was expanded from, and the coefficient itself.

Why (Field, Level) is the serialised identity and Column is not

Column is the design-matrix name the solver keyed the coefficient by, and it is an internal, length-prefixed encoding (see dummyCategoricalName) invented purely so two different fields cannot collide on a level spelling. Writing it into the document would freeze that encoding into the profile file format: every future reader would have to parse it, and changing it — to widen the field namespace, say — would silently invalidate every document already on disk. (Field, Level) is the durable identity a consumer actually needs ("the coefficient for dma=602"), so Column is `json:"-"` and survives only in-process, where the residual replay uses it.

type ModelPredictorFidelity added in v0.33.0

type ModelPredictorFidelity struct {
	// Kind / Field / Level address the term exactly as
	// ModelPredictorSpec does, so an entry here zips against the profile
	// document's `models[].predictors[]` without a join key.
	Kind  string `json:"kind"`
	Field string `json:"field"`
	Level string `json:"level"`
	// CapturedCoefficient is the spec's coefficient divided by
	// LatentScale — the quantity buildModelDrawers actually adds into the
	// latent, not the raw number the profile document carries.
	CapturedCoefficient float64 `json:"captured_coefficient"`
	// RecoveredCoefficient is the refit's estimate for the same term.
	// StdError is the refit's own standard error for it, which is the
	// second half of the flagging band — see ModelRecoveryTolerance.
	RecoveredCoefficient float64 `json:"recovered_coefficient,omitempty"`
	StdError             float64 `json:"std_error,omitempty"`
	Delta                float64 `json:"delta,omitempty"`
	// NFired is the number of admitted synthetic rows on which this
	// term's indicator was 1. It is the term's own support and it is what
	// distinguishes the two ways a recovered coefficient reaches zero: a
	// term that fired thousands of times and recovered nothing is a
	// generation fault, while a term that never fired had nothing to
	// recover — the level was captured but the field's reconstructed
	// marginal does not generate it.
	NFired int `json:"n_fired"`
	// Flagged is true when Delta exceeds both ModelRecoveryTolerance and
	// modelRecoverySEMultiple standard errors. False whenever Error is
	// set: a term that was not estimated has no gap to judge.
	Flagged bool `json:"flagged,omitempty"`
	// Error is set instead of the recovered figures when this term could
	// not be carried into the refit at all — its level is absent from the
	// output cohort's dictionary, or its column is constant over the
	// admitted rows and so carries no information to estimate from. Both
	// are informative outcomes rather than failures, and both leave every
	// OTHER term of the same model estimated.
	Error string `json:"error,omitempty"`
}

ModelPredictorFidelity is one compiled model term's recovery: the captured coefficient generation used, on the latent scale, beside the coefficient a refit of the same term on the generated rows recovers.

type ModelPredictorSpec added in v0.33.0

type ModelPredictorSpec struct {
	// Kind is one of the ModelPredictor* constants
	// (ModelPredictorCategoricalLevel / ModelPredictorSetOption /
	// ModelPredictorNumeric).
	Kind string `json:"kind"`
	// Field is the predictor's source field.
	Field string `json:"field"`
	// Level is the categorical level text or set option text that turns
	// this contribution on.
	Level string `json:"level"`
	// Coefficient is added to the prediction when the indicator is 1.
	Coefficient float64 `json:"coefficient"`
}

ModelPredictorSpec is one (field, level) contribution of a FieldModelSpec.

Kind distinguishes the two indicator semantics and is not derivable from (Field, Level): a categorical_level column is one arm of a partition whose omitted reference level is the zero baseline, while a set_option column is an independent indicator with no reference at all. The internal design-column name the solver used is deliberately absent — (Field, Level) is the addressing key here exactly as it is on the profile document.

type ModelReference added in v0.33.0

type ModelReference struct {
	// Field is the categorical predictor source field.
	Field string `json:"field"`
	// Level is the level whose effect is folded into the intercept. A
	// row carrying it contributes nothing beyond the intercept for this
	// field; every other level's coefficient is read relative to it.
	Level string `json:"level"`
}

ModelReference names one categorical predictor field's dropped REFERENCE level — the level whose column modelColumns removed to keep the design full-rank alongside an intercept, and whose effect is therefore folded into FieldModel.Intercept.

It exists so the document is self-describing about the baseline. A consumer reading `predictors` alone sees k−1 levels of a k-level field and has to GUESS that the missing one is the zero baseline — and, worse, guess WHICH one it was, since the drop rule (dictionary ID 0) is an implementation choice this section must not require a reader to know. Writing the dropped level down turns both guesses into a lookup, and leaves the drop rule free to change later without invalidating a single document already written.

type ModelResidualCorrelationFidelity added in v0.33.0

type ModelResidualCorrelationFidelity struct {
	// Fields names the participants in COMPONENT order — the order
	// buildResidualCorrelator assigned them, which is drawer order,
	// which is schema field order. It is the generation-side
	// participant set, so a modelled field absent from it took no part
	// in the correlated draw (its model was dropped, or no surviving
	// correlation named it) and drew an independent residual.
	Fields []string `json:"fields"`
	// Compared is the number of applied residual correlations that
	// produced a recovered figure. Flagged counts those whose Delta
	// exceeds ResidualRecoveryTolerance. Both count over every compared
	// pair, not over the bounded Pairs listing.
	Compared int `json:"compared"`
	Flagged  int `json:"flagged,omitempty"`
	// MeanDelta / MaxDelta are the mean and maximum absolute gap over
	// every compared pair — the two figures that make the truncated
	// listing safe to read, since they cannot be improved by what the
	// listing left out.
	MeanDelta float64 `json:"mean_delta"`
	MaxDelta  float64 `json:"max_delta"`
	// Pairs is the worst maxResidualRecoveryPairs compared pairs by
	// absolute Delta, largest first, ties broken by (A, B) so the
	// listing is a function of the numbers alone. Omitted is how many
	// compared pairs it left out; every one of them recovered at least
	// as well as the last entry listed.
	Pairs   []*ModelResidualPairFidelity `json:"pairs,omitempty"`
	Omitted int                          `json:"omitted,omitempty"`
	// Unmeasured holds pairs generation applied that the synthetic
	// partition could not produce a correlation for, each with its
	// reason, capped the same way. It is a separate list rather than an
	// Error field on ModelResidualPairFidelity for the same structural
	// reason ResidualCorrelationProfile splits its two lists: an
	// unmeasured pair has no rho, and a struct with a rho slot in it is
	// how a zero gets written into one.
	Unmeasured        []*ModelResidualUnmeasuredPair `json:"unmeasured,omitempty"`
	UnmeasuredOmitted int                            `json:"unmeasured_omitted,omitempty"`
}

ModelResidualCorrelationFidelity is FidelityReport's `model_residual_correlations` section: how well the generated partition reproduces the residual correlation structure generation was asked to impose.

The aggregate counters describe EVERY compared pair; Pairs carries a bounded worst-first sample of them and Omitted counts the rest. See this file's header for why the listing is bounded and why nothing is lost by it.

type ModelResidualPairFidelity added in v0.33.0

type ModelResidualPairFidelity struct {
	A string `json:"a"`
	B string `json:"b"`
	// CapturedRho is CorrelationSpec.Correlation — the figure
	// buildResidualCorrelator was asked to impose, which is the source
	// residual correlation the profile captured. RecoveredRho is the
	// Pearson correlation between the two models' REFIT residuals over
	// the synthetic partition, on the latent scale. Delta is their
	// absolute difference.
	CapturedRho  float64 `json:"captured_rho"`
	RecoveredRho float64 `json:"recovered_rho"`
	Delta        float64 `json:"delta"`
	// N is the number of synthetic rows carrying BOTH residuals — the
	// realized pair's own co-occurrence count within the refit row cap,
	// not the cohort's row count and not either model's NObs.
	N int `json:"n"`
	// Flagged is true when Delta exceeds ResidualRecoveryTolerance.
	Flagged bool `json:"flagged,omitempty"`
}

ModelResidualPairFidelity is one applied residual correlation's recovery.

type ModelResidualUnmeasuredPair added in v0.33.0

type ModelResidualUnmeasuredPair struct {
	A string `json:"a"`
	B string `json:"b"`
	// CapturedRho still rides, because the reader needs to know how much
	// structure went unverified: an unmeasured pair captured at 0.8 is a
	// far larger hole in the report than one captured at 0.02.
	CapturedRho float64 `json:"captured_rho"`
	// N is how many synthetic rows DID carry both residuals — zero or a
	// handful for ResidualUnmeasuredNoOverlap, and however many
	// overlapped for ResidualUnmeasuredNoVariance. Zero and meaningless
	// for ResidualUnmeasuredNoModelFit, where one side has no residual
	// vector at all.
	N int `json:"n"`
	// Reason is ResidualUnmeasuredNoOverlap, ResidualUnmeasuredNoVariance
	// or ResidualUnmeasuredNoModelFit.
	Reason string `json:"reason"`
}

ModelResidualUnmeasuredPair records one applied residual correlation the synthetic partition could not be measured for, and why. It deliberately carries no recovered-rho slot at all — see ResidualUnmeasuredPair, whose reasoning this mirrors exactly.

type NumericPairProfile added in v0.32.0

type NumericPairProfile struct {
	A   string  `json:"a"`
	B   string  `json:"b"`
	Rho float64 `json:"rho"`
	// N is the number of rows in the capture where both A and B were
	// simultaneously non-null — the pair's true co-occurrence count,
	// unlike Profile.Pairwise's Rho (computed from two independently
	// capped reservoirs that can drift out of alignment once either
	// field has nulls).
	N int `json:"n"`
}

NumericPairProfile is one captured numeric-numeric pair's reconstruction input.

type NumericProfile

type NumericProfile struct {
	Min  float64 `json:"min"`
	Max  float64 `json:"max"`
	Mean float64 `json:"mean"`
	Std  float64 `json:"std"`
	// Percentiles holds {p1, p5, p25, p50, p75, p95, p99} when
	// IncludeStats was on; nil otherwise.
	Percentiles []float64 `json:"percentiles,omitempty"`
	// Shape carries a fitted 2-component Gaussian mixture, captured
	// only when ProfileOptions.FitShape was set AND the mixture is a
	// genuine improvement over the plain Mean/Std normal above (BIC
	// selection — see fitNumericShape in shape.go). Additive +
	// omitempty: absent from every profile document captured without
	// --fit-shape (including every pre-existing document), and absent
	// for any numeric field whose observed distribution is already
	// close to normal — SpecFromProfile falls back to the ordinary
	// Mean/Std/Min/Max normal reconstruction whenever this is nil,
	// exactly as it always has.
	Shape *ShapeProfile `json:"shape,omitempty"`
}

NumericProfile is the detail block for numeric fields.

type Options

type Options struct {
	// Seed makes the output deterministic. Same spec + same seed must
	// produce a byte-identical .pulse file.
	Seed int64

	// SourceCohort activates the tagged top-up path (AugmentFromProfile)
	// when non-empty: real rows from this cohort are copied into the
	// output tagged _synthetic=false, spec.RowCount new rows are
	// generated and tagged _synthetic=true, and the source is never
	// opened for write. Leaving it empty (the default) reproduces
	// today's behavior exactly — a plain synthesis with no tag column,
	// used by synth from-schema and any caller building a Spec directly.
	SourceCohort string

	// FidelityReportPath activates the post-generation fidelity report
	// (`synth from-profile --fidelity-report`) when non-empty: after
	// generation completes, AugmentFromProfile drives TEST_KS (numeric
	// fields, SplitBy=_synthetic) / TEST_CHISQ (categorical fields,
	// contingency against _synthetic) against the tagged output cohort
	// and writes the resulting FidelityReport JSON document to this
	// path. Only meaningful alongside SourceCohort — the plain Synth
	// path (SourceCohort empty) has no _synthetic partition to compare
	// against and ignores this field entirely.
	FidelityReportPath string

	// FidelityWarnings, when non-empty, are copied verbatim onto the
	// written FidelityReport's own Warnings slot — the mechanism that
	// surfaces thin-pair warnings (e.g. Profile.Warnings from a
	// `profile create --conditional` capture) beside the pairwise
	// correlation deltas they qualify in the same document (FR-18),
	// rather than requiring a caller to cross-reference the profile
	// file separately. This package has no Profile type of its own to
	// read Warnings from directly — the caller (internal/cli's `synth
	// from-profile`, or any programmatic caller building Options from a
	// Profile it already has in hand) is expected to populate this.
	// Ignored when FidelityReportPath is empty, exactly like every
	// other Fidelity* field.
	FidelityWarnings []string
}

Options modulates how the spec is realized.

type PairwiseFidelity added in v0.32.0

type PairwiseFidelity struct {
	A            string  `json:"a"`
	B            string  `json:"b"`
	SourceRho    float64 `json:"source_rho"`
	SyntheticRho float64 `json:"synthetic_rho,omitempty"`
	// Delta is abs(SourceRho - SyntheticRho). Zero-valued and
	// meaningless when Error is set.
	Delta float64 `json:"delta,omitempty"`
	// N is the number of _synthetic=true rows with both A and B
	// simultaneously non-null — the realized pair's own co-occurrence
	// count, distinct from (and typically much larger than) the source
	// capture's own N.
	N int `json:"n"`
	// Error is set instead of SyntheticRho/Delta when the synthetic
	// partition could not produce a defined correlation for this pair
	// (fewer than two co-occurring non-null observations) — mirrors
	// FieldFidelity's own per-field failure contract: one pair's
	// failure never withholds the rest of the report.
	Error string `json:"error,omitempty"`
}

PairwiseFidelity is one row of FidelityReport.Pairwise: the delta between a captured numeric-numeric pair's source correlation (the same rho synth/copula.go's conditional-Gaussian reconstruction was asked to hit — CorrelationSpec.Correlation, itself sourced from Profile.Conditional.NumericPairs when --conditional captured it, or Profile.Pairwise otherwise, via SpecFromProfile) and that pair's REALIZED Pearson correlation over the _synthetic=true partition of the output cohort. The realized figure is computed with the same `pearson` helper Profile capture itself uses (see computeConditionalNumericPairs) — never a second, divergent implementation, and the exact computation TestSynth_CorrelationReconstructionWithinTolerance and its siblings already assert against.

type Profile

type Profile struct {
	RowCount int               `json:"row_count"`
	Fields   []FieldProfile    `json:"fields"`
	Pairwise []CorrelationStat `json:"pairwise,omitempty"`
	// Conditional carries joint-reconstruction structure captured only
	// when ProfileOptions.IncludeConditional was set. Additive and
	// omitempty: absent on every profile document captured without
	// --conditional, including every pre-existing one, and
	// SpecFromProfile treats a nil Conditional exactly as it always
	// has (falling back to Pairwise).
	Conditional *ConditionalProfile `json:"conditional,omitempty"`
	// Models carries the per-numeric additive linear predictors captured
	// only when ProfileOptions.FitModels was set (`profile create
	// --fit-models`). Additive and omitempty, exactly like Conditional:
	// the key is entirely absent from every document captured without
	// the flag — including every document written before this section
	// existed — and SpecFromProfile treats an empty Models exactly as it
	// always has, falling back to Conditional's numeric-target pairs.
	//
	// The section is a REPLACEMENT for those pairs, not a supplement:
	// when it is present SpecFromProfile stops populating
	// Spec.CategoricalNumericPairs / Spec.SetNumericPairs entirely,
	// because one additive model already accounts for every predictor at
	// once and the pairs exist only to be applied one overwrite at a
	// time. See SpecFromProfile for the full rule.
	//
	// Not every part of a captured FieldModel is durable: the residual
	// reservoir and the internal design-column names are `json:"-"`, so
	// a Profile read back from JSON carries coefficients but no
	// per-row residuals. See FieldModel for why.
	Models []FieldModel `json:"models,omitempty"`
	// ResidualCorrelations carries the correlation structure among the
	// fitted model residuals, captured only when
	// ProfileOptions.FitResidualCorrelations was set (`profile create
	// --residual-correlations`) alongside --fit-models. Additive and
	// omitempty, exactly like Conditional and Models.
	//
	// It is a SEPARATE measurement from Pairwise and
	// Conditional.NumericPairs, not a refinement of either: those two
	// correlate raw values and this correlates what is left after each
	// field's model has explained what it can. All three can coexist in
	// one document, and none of them replaces another.
	//
	// SpecFromProfile translates the MEASURED pairs onto
	// Spec.ResidualCorrelations, where generation consumes them as the
	// shared source of randomness every modelled field's residual is
	// drawn from (synth/residual_draw.go). The `unmeasured` list is
	// translated into nothing at all: an absent pair is completed as
	// independent by the generator, which counts and names the
	// assumption, whereas writing it out as a zero would present it as
	// a measurement. See ResidualCorrelationProfile.
	ResidualCorrelations *ResidualCorrelationProfile `json:"residual_correlations,omitempty"`
	Warnings             []string                    `json:"warnings,omitempty"`
	Meta                 map[string]any              `json:"meta,omitempty"`
}

Profile is a serialization-friendly statistical summary of a cohort. It contains everything needed to drive synth from-profile without retaining any individual rows from the source data.

func ProfileBytes

func ProfileBytes(data []byte, opts ProfileOptions) (*Profile, error)

ProfileBytes summarizes a .pulse file given its raw bytes.

func ProfileFile

func ProfileFile(fs afero.Fs, path string, opts ProfileOptions) (*Profile, error)

ProfileFile reads a .pulse file from fs and produces a Profile.

func (*Profile) FittedModels added in v0.33.0

func (p *Profile) FittedModels() []FieldModel

FittedModels returns the per-numeric linear models captured under ProfileOptions.FitModels, or nil when the flag was off.

It is a thin accessor over Profile.Models, retained because it is the name the capture stage published and because it reads correctly at both ends of a round trip: on a freshly captured Profile it hands back models complete with their residual reservoir, and on one decoded from a document it hands back the same models minus the `json:"-"` parts. Callers that need per-row residuals must therefore hold the capture, not re-read the file — see FieldModel.Residuals.

func (*Profile) MarshalJSON

func (p *Profile) MarshalJSON() ([]byte, error)

MarshalJSON serializes the profile.

type ProfileOptions

type ProfileOptions struct {
	// TopK is the number of top categorical values to capture per
	// categorical field. Defaults to 32 when zero.
	TopK int
	// IncludeStats turns on percentile / stdev / kurtosis collection.
	// When false, only mean / min / max / null-rate are recorded for
	// numeric fields. Defaults to true.
	IncludeStats bool
	// IncludeCorrelations enables pairwise Pearson correlation capture
	// between numeric fields. Off by default to keep profile size bounded.
	IncludeCorrelations bool
	// CorrelationTopK caps the number of strongest |rho| pairs retained.
	// Defaults to 16 when IncludeCorrelations or IncludeConditional is
	// true.
	CorrelationTopK int
	// IncludeConditional enables capture of the joint reconstruction
	// structure (`profile create --conditional`): for numeric-numeric
	// field pairs, a row-aligned correlation + observation count
	// (Profile.Conditional.NumericPairs) that SpecFromProfile uses to
	// drive the exact conditional-Gaussian reconstruction in
	// synth/copula.go, in place of the plain (and, for cohorts with
	// nulls, only approximately aligned) Pairwise correlation stats.
	// Off by default; the profile document's `conditional` key is
	// entirely absent (omitempty) when this is false, and every
	// pre-existing profile document — which has no such key — remains
	// valid SpecFromProfile input either way.
	IncludeConditional bool
	// SampleLimit caps the number of records ingested for the profile.
	// Zero = unlimited.
	SampleLimit int
	// FitShape enables shape-fitting for numeric fields
	// (`profile create --fit-shape`, E4-S2): instead of always
	// summarizing a numeric field as a single normal(mean, std),
	// attempt a 2-component Gaussian mixture fit and keep it — as
	// FieldProfile.Numeric.Shape — only when it is a genuine
	// improvement over the plain normal by BIC (see fitNumericShape in
	// shape.go for the full selection rule and its documented
	// limitations). Off by default; a field whose fit is not kept (not
	// enough data, or the source is already close to normal) carries no
	// Shape, and SpecFromProfile reconstructs it exactly as it always
	// has. Forces sample retention (as if IncludeStats were also set)
	// for numeric fields so there is something to fit against, even
	// when IncludeStats itself is false.
	FitShape bool
	// FitModels enables per-numeric linear model capture
	// (`profile create --fit-models`, E1-S2): for each numeric field,
	// fit one linear predictor of that field on the cohort's
	// categorical LEVELS and set OPTIONS, driven off this same single
	// scan, and retain the fitted coefficients, the residual scale and
	// a bounded row-aligned residual vector.
	//
	// Off by default. When on, the fits are written to the document as
	// the additive `models` section (Profile.Models) and nothing else in
	// the document moves: every other section is byte-identical to the
	// same capture with the flag off, and a capture WITHOUT the flag
	// emits no `models` key at all. It draws from its own RNG stream, so
	// --conditional's captured output does not depend on whether this
	// flag was also passed.
	//
	// It does change what SpecFromProfile builds: a document carrying
	// `models` reconstructs its numeric fields from the additive linear
	// predictors instead of from --conditional's one-pair-at-a-time
	// numeric-target arms, which are then not populated at all.
	FitModels bool
	// FitResidualCorrelations enables capture of the correlation
	// structure among the fitted model RESIDUALS
	// (`profile create --residual-correlations`, E3-S1), written as the
	// additive `residual_correlations` section
	// (Profile.ResidualCorrelations).
	//
	// It requires FitModels — residuals are what the models produce, so
	// with no models there is nothing to correlate, and asking for this
	// alone warns rather than silently emitting nothing.
	//
	// It is deliberately its own flag rather than an implication of
	// FitModels: the section is QUADRATIC in modelled fields (105
	// targets is 5,460 pairs) and no existing caller asked for it, so
	// switching it on with --fit-models would grow every such document
	// without notice. It is likewise deliberately NOT wired to
	// IncludeCorrelations / CorrelationTopK, which keep their exact
	// meaning — the strongest top-K pairs of RAW values. This section is
	// a different measurement (residuals, not raw values) with a
	// different shape (a full submatrix, not a ranked list), and sharing
	// either flag would silently change what those two already mean.
	FitResidualCorrelations bool
	// Seed drives the reservoir-sampling RNG used by
	// IncludeConditional's categorical-categorical contingency capture
	// (E7-S2) once the source cohort exceeds conditionalJointCap rows.
	// Zero (the default) is itself a valid, deterministic seed — same
	// as synth.Options.Seed's convention — so two profile runs against
	// the same source with the same Seed (including the unset zero
	// value) produce byte-identical captured output. It does not affect
	// any other capture path: every other section in this file is
	// either an exact online accumulation or a first-N reservoir with
	// no random draws.
	Seed int64
}

ProfileOptions modulates how Profile summarizes a cohort.

type ResidualCorrelation added in v0.33.0

type ResidualCorrelation struct {
	A string `json:"a"`
	B string `json:"b"`
	// Rho is the Pearson correlation between the two fields' fitted
	// residuals over the rows where both exist. Deliberately NOT
	// omitempty: a measured zero must appear on the wire, because
	// omitting it would make it indistinguishable from an unmeasured
	// pair on read-back and would reintroduce exactly the ambiguity
	// this section removes.
	Rho float64 `json:"rho"`
	// N is the number of retained rows carrying both residuals — the
	// pair's true co-occurrence count within the capture's residual
	// reservoir, not the cohort's row count and not either model's
	// NObs.
	N int `json:"n"`
}

ResidualCorrelation is one MEASURED residual pair.

Every entry in ResidualCorrelationProfile.Pairs is a real measurement over N co-present rows, including one whose Rho is 0 — that is the measured-zero case this section exists to keep distinguishable from a gap, and it is meaningful information (these two residuals were watched together and did not move together).

type ResidualCorrelationProfile added in v0.33.0

type ResidualCorrelationProfile struct {
	// Fields names the model residuals this submatrix covers, sorted.
	// It is the participant set, and it is written down so a reader can
	// tell an absent pair from an absent FIELD — a field with no model
	// has no residual and was never a candidate, which is a different
	// statement from a modelled pair that could not be measured.
	Fields []string `json:"fields"`
	// Pairs holds every MEASURED pair, in (A, B) sorted order.
	Pairs []ResidualCorrelation `json:"pairs,omitempty"`
	// Unmeasured holds every pair among Fields that Pairs does not, each
	// with its reason. Present whenever there is one; a fully measured
	// submatrix omits the key entirely, which is itself the signal that
	// nothing was assumed.
	Unmeasured []ResidualUnmeasuredPair `json:"unmeasured,omitempty"`
}

ResidualCorrelationProfile is the captured correlation structure among model residuals: the FULL submatrix over participants, split into what was measured and what was not.

The representation is two lists over a declared participant set rather than an N x N array of numbers, and that is the load-bearing choice. A dense array has one slot per pair and every slot must hold a number, so "unmeasured" has nowhere to live except as a sentinel — and the sentinel that reads most naturally is 0, which is also a perfectly ordinary measurement. Splitting the pairs by whether they were measured makes the two states structurally different rather than conventionally different, and a JSON round trip preserves a structural difference for free.

Fields + Pairs + Unmeasured is exhaustive and non-overlapping: every unordered pair among Fields appears in exactly one of the two lists.

type ResidualUnmeasuredPair added in v0.33.0

type ResidualUnmeasuredPair struct {
	A string `json:"a"`
	B string `json:"b"`
	// N is how many rows DID carry both residuals — zero or a handful
	// for ResidualUnmeasuredNoOverlap, and however many overlapped for
	// ResidualUnmeasuredNoVariance. It is the diagnostic that separates
	// "these two fields never co-occur" from "they co-occur constantly
	// and one of them is fitted exactly".
	N int `json:"n"`
	// Reason is one of the ResidualUnmeasured* constants. There is no
	// rho key on this struct at all — an unmeasured pair has no
	// correlation to report, and providing a slot for one is how a zero
	// gets written into it.
	Reason string `json:"reason"`
}

ResidualUnmeasuredPair records one pair among participants that could NOT be measured, and why.

It is written down rather than left as an absence from Pairs because the whole subject of this section is the difference between "we looked and there is no relationship" and "we could not look". A reader holding Fields and Pairs can already derive the complement, but deriving a fact is not the same as being told it, and the reason is not derivable at all.

type Result

type Result struct {
	RowsGenerated int      `json:"rows_generated"`
	RowsRejected  int      `json:"rows_rejected"`
	OutputPath    string   `json:"output_path"`
	Warnings      []string `json:"warnings,omitempty"`

	// FidelityReportPath echoes Options.FidelityReportPath when a
	// fidelity report was written alongside the output cohort. Empty
	// when the flag was not set.
	FidelityReportPath string `json:"fidelity_report_path,omitempty"`
}

Result is what the writer reports after a successful Synth call.

func AugmentFromProfile added in v0.32.0

func AugmentFromProfile(fs afero.Fs, spec *Spec, sourcePath, output string, opts Options) (*Result, error)

AugmentFromProfile is the tagged top-up entry point behind `synth from-profile`. It never opens sourcePath for write: it reads the source cohort, generates spec.RowCount brand-new rows, and writes a single combined cohort to output containing every source row (tagged _synthetic=false) followed by every generated row (tagged _synthetic=true). output must be a distinct path from sourcePath — synthesis never mutates the source in place.

spec.RowCount is an explicit count of NEW rows to generate; it is never a "top up to N total" target. A 200-row source with spec.RowCount=500 produces a 700-row output.

func Synth

func Synth(fs afero.Fs, spec *Spec, output string, opts Options) (*Result, error)

Synth materializes a synthetic .pulse file from a Spec into the given path on the provided filesystem. It is the package-level entry point; pulse.Pulse.Synth wraps it.

When opts.SourceCohort is non-empty, this delegates to AugmentFromProfile instead: the source cohort's real rows are copied into output tagged _synthetic=false, spec.RowCount new rows are generated and tagged _synthetic=true, and the source is never opened for write. opts.SourceCohort empty (the default) reproduces the unmodified plain-synthesis behavior below, byte-for-byte.

func SynthBytes

func SynthBytes(spec *Spec, opts Options) ([]byte, *Result, error)

SynthBytes returns the byte slice that Synth would have written, without touching the filesystem. Useful for round-trip tests and embedders that stream the output elsewhere.

type SetCategoricalPairFidelity added in v0.32.0

type SetCategoricalPairFidelity struct {
	Set         string  `json:"set"`
	Option      string  `json:"option"`
	Categorical string  `json:"categorical"`
	Delta       float64 `json:"delta,omitempty"`
	// N is the number of _synthetic=true rows with both the set field
	// and the categorical field simultaneously non-null — the realized
	// pair's own co-occurrence count.
	N int `json:"n"`
	// Error is set instead of Delta when the synthetic partition
	// produced no co-occurring observation at all for this pair —
	// mirrors CategoricalPairFidelity's own per-pair contract.
	Error string `json:"error,omitempty"`
}

SetCategoricalPairFidelity is one row of FidelityReport.SetCategoricalPairwise: the delta between a captured set-option x categorical pair's source contingency table (SetCategoricalPairSpec.Cells) and that pair's REALIZED contingency table over the _synthetic=true partition of the output cohort — the set-option analogue of CategoricalPairFidelity, with the A axis fixed to the option's {"selected","not_selected"} Bernoulli domain instead of an arbitrary categorical value set. Delta reuses categoricalTVD verbatim (SetCategoricalPairSpec.Cells is the same []CategoricalPairCellSpec shape CategoricalPairSpec.Cells is).

type SetCategoricalPairProfile added in v0.32.0

type SetCategoricalPairProfile struct {
	Set         string            `json:"set"`
	Option      string            `json:"option"`
	Categorical string            `json:"categorical"`
	Cells       []ContingencyCell `json:"cells"`
	// N is the number of rows where both the set field and the
	// categorical field were simultaneously non-null — this option's
	// true co-occurrence count, mirroring CategoricalPairProfile.N.
	N int `json:"n"`
}

SetCategoricalPairProfile is one captured set-option x categorical pair's reconstruction input (E5-S2): the bounded contingency table between one dictionary option (bit position) of a set_* field — treated as its own two-valued "selected"/"not_selected" Bernoulli sub-field, per E5-S1's per-option representation — and a categorical field's observed values. Reuses the exact same ContingencyCell / ContingencyCellCap / otherCategoryLabel machinery CategoricalPairProfile already established for two categorical fields; the only difference is the A axis is always the fixed two-value domain {"selected","not_selected"} rather than an arbitrary categorical value set, so it needs no per-field top-K collapse of its own — only B's (the categorical field's) values are collapsed, exactly as CategoricalPairProfile already does for either of its two axes.

type SetCategoricalPairSpec added in v0.32.0

type SetCategoricalPairSpec struct {
	Set         string                    `json:"set"`
	Option      string                    `json:"option"`
	Categorical string                    `json:"categorical"`
	Cells       []CategoricalPairCellSpec `json:"cells"`
}

SetCategoricalPairSpec is one set-option x categorical pair's generation-time reconstruction input — the Spec-facing counterpart of SetCategoricalPairProfile. Cells reuse CategoricalPairCellSpec verbatim: AValue is always "selected"/"not_selected" (the set option's own fixed two-value domain), BValue is the categorical field's observed value.

type SetFieldFidelity added in v0.32.0

type SetFieldFidelity struct {
	Field   string               `json:"field"`
	Options []*SetOptionFidelity `json:"options,omitempty"`
}

SetFieldFidelity is one row of FidelityReport.SetFields: the per-option marginal frequency comparison for a set_* (multi-select bitmask) field, extending E1-S2's per-field marginal section to set_* fields (E5-S4). It deliberately does NOT reuse TEST_KS/ TEST_CHISQ the way FieldFidelity does — driving each option through the ordinary Process pipeline would need a presentation schema widening every option into its own pseudo-categorical field, purely to satisfy TEST_CHISQ's Rows/Cols contract (the same trick SyntheticAsCategoricalSchema plays for _synthetic itself, multiplied by option count). A direct frequency comparison is simpler, equally rigorous for a Bernoulli marginal (E5-S1's own representation), and composes with the same per-option addressing SetProfile.Options and ConditionalProfile's three set-pair kinds already use — see computeSetFieldFidelity.

type SetNumericPairFidelity added in v0.32.0

type SetNumericPairFidelity struct {
	Set        string                                `json:"set"`
	Option     string                                `json:"option"`
	Numeric    string                                `json:"numeric"`
	Categories []*CategoricalNumericCategoryFidelity `json:"categories,omitempty"`
}

SetNumericPairFidelity is one row of FidelityReport.SetNumericPairwise: the per-bucket delta between a captured set-option x numeric pair's source conditional mean/std (SetNumericPairSpec.Categories, keyed "selected"/"not_selected") and that bucket's REALIZED conditional mean/std over the _synthetic=true partition — the set-option analogue of CategoricalNumericPairFidelity.

It is a NUMERIC-TARGET section, so the model-retirement rule on CategoricalNumericPairFidelity applies to it verbatim: a numeric reached by a linear model has no entry here.

type SetNumericPairProfile added in v0.32.0

type SetNumericPairProfile struct {
	Set     string `json:"set"`
	Option  string `json:"option"`
	Numeric string `json:"numeric"`
	// Categories carries at most two entries, keyed "selected" /
	// "not_selected" — reuses CategoricalNumericCategoryStat verbatim
	// rather than a bespoke two-field struct, so a caller already
	// handling CategoricalNumericPairProfile.Categories needs no second
	// shape to learn.
	Categories []CategoricalNumericCategoryStat `json:"categories"`
	// N mirrors CategoricalNumericPairProfile.N: the total non-null
	// co-occurrence count summed across both categories.
	N int `json:"n"`
}

SetNumericPairProfile is one captured set-option x numeric pair's reconstruction input (E5-S2): the numeric field's conditional mean/std broken out by whether the set field's option (bit position) was selected — the set-field analogue of CategoricalNumericPairProfile, with the categorical axis fixed to the two-value {"selected","not_selected"} domain instead of an arbitrary category set.

type SetNumericPairSpec added in v0.32.0

type SetNumericPairSpec struct {
	Set        string                           `json:"set"`
	Option     string                           `json:"option"`
	Numeric    string                           `json:"numeric"`
	Categories []CategoricalNumericCategorySpec `json:"categories"`
	Min        float64                          `json:"min,omitempty"`
	Max        float64                          `json:"max,omitempty"`
	HasClamp   bool                             `json:"has_clamp,omitempty"`
}

SetNumericPairSpec is one set-option x numeric pair's generation-time reconstruction input — the Spec-facing counterpart of SetNumericPairProfile. Categories carries at most two entries keyed "selected" / "not_selected", reusing CategoricalNumericCategorySpec verbatim. Min/Max/HasClamp mirror CategoricalNumericPairSpec: the numeric field's own observed range, applied identically regardless of which bucket the conditional draw lands in.

type SetOptionFidelity added in v0.32.0

type SetOptionFidelity struct {
	Value string `json:"value"`
	// SourceFrequency/SyntheticFrequency are P(bit set) over each
	// partition's non-null rows of Field. Delta is
	// abs(SourceFrequency - SyntheticFrequency). SyntheticFrequency and
	// Delta are zero-valued and meaningless when Error is set.
	SourceFrequency    float64 `json:"source_frequency"`
	SyntheticFrequency float64 `json:"synthetic_frequency,omitempty"`
	Delta              float64 `json:"delta,omitempty"`
	// N is the number of _synthetic=true rows with a non-null Field —
	// the realized synthetic partition's own denominator for this
	// option's frequency, shared across every option of the same field.
	N int `json:"n"`
	// Error is set instead of SyntheticFrequency/Delta when the
	// synthetic partition has no non-null observation of Field at all —
	// mirrors every other Fidelity entry kind's non-fatal per-entry
	// contract.
	Error string `json:"error,omitempty"`
}

SetOptionFidelity is one dictionary entry (bit position)'s realized selection-frequency comparison between the source (_synthetic=false) and synthetic (_synthetic=true) partitions of the output cohort — the set_* analogue of FieldFidelity, one entry per option rather than one entry per field, mirroring CategoricalNumericCategoryFidelity's per-category breakout for the same reason: a set_* field reconstructs one independent Bernoulli marginal per option rather than a single scalar.

type SetOptionStat added in v0.32.0

type SetOptionStat struct {
	Value string `json:"value"`
	// Count is the number of non-null rows with this option's bit set.
	Count int `json:"count"`
	// Frequency is Count / SetProfile.N — P(bit set), the per-option
	// marginal Bernoulli rate.
	Frequency float64 `json:"frequency"`
}

SetOptionStat is one dictionary entry (bit position)'s observed marginal selection frequency within a set_* field's non-null rows.

type SetProfile added in v0.32.0

type SetProfile struct {
	// N is the number of non-null rows Options' frequencies are computed
	// over — the same denominator FieldProfile.NullRate already implies,
	// surfaced here so a caller reading only the Set block can interpret
	// Count as a rate without cross-referencing NullRate.
	N int `json:"n"`
	// Options lists EVERY dictionary entry in bit/dictionary order (bit i
	// first) — NOT sorted by frequency like CategoricalProfile.Top — so
	// two profiles of the same schema are directly comparable
	// position-for-position regardless of which option happens to be
	// more popular.
	Options []SetOptionStat `json:"options"`
}

SetProfile holds per-option (marginal) selection-frequency statistics for a set_* (multi-select bitmask) field. E5-S1's design decision: each dictionary entry (bit position) is profiled as an independent Bernoulli-selected sub-field — Frequency is P(bit i set | field non-null) — matching "select all that apply" semantics, where each option genuinely is its own independent yes/no question a respondent answers. This is deliberately NOT "whole mask as one categorical" (which would treat every distinct combination as an arbitrary, combinatorially exploding category) nor "N fully independent binaries with no shared identity" (which would drop the fact that every option rides one bounded, shared dictionary — the same per-option addressing E5-S2's joint/conditional capture reuses). Full rationale: skills/synthetic-data.md ("Set (multi-select) field profiling").

This section is marginal-only: joint/conditional structure between options, or between a set_* field and another field, is E5-S2's job.

type SetSetPairFidelity added in v0.32.0

type SetSetPairFidelity struct {
	SetA    string  `json:"set_a"`
	OptionA string  `json:"option_a"`
	SetB    string  `json:"set_b"`
	OptionB string  `json:"option_b"`
	Delta   float64 `json:"delta,omitempty"`
	// N is the number of _synthetic=true rows with both set fields
	// simultaneously non-null — the realized pair's own co-occurrence
	// count.
	N int `json:"n"`
	// Error is set instead of Delta when the synthetic partition
	// produced no co-occurring observation at all for this pair.
	Error string `json:"error,omitempty"`
}

SetSetPairFidelity is one row of FidelityReport.SetSetPairwise: the delta between a captured option x option pair's source 2x2 contingency table (SetSetPairSpec.Cells) between two DIFFERENT set_* fields, and that pair's REALIZED 2x2 contingency table over the _synthetic=true partition — the cross-field-option analogue of CategoricalPairFidelity, with both axes fixed to their own option's {"selected","not_selected"} domain.

type SetSetPairProfile added in v0.32.0

type SetSetPairProfile struct {
	SetA    string            `json:"set_a"`
	OptionA string            `json:"option_a"`
	SetB    string            `json:"set_b"`
	OptionB string            `json:"option_b"`
	Cells   []ContingencyCell `json:"cells"`
	// N is the number of rows where both fields were simultaneously
	// non-null — this option pair's true co-occurrence count.
	N int `json:"n"`
}

SetSetPairProfile is one captured option x option pair's reconstruction input between two DIFFERENT set_* fields (E5-S2): a bounded 2x2-cell contingency table of {"selected","not_selected"} x {"selected","not_selected"} co-occurrence counts for one option of each field. Never captured between two options of the SAME set field — E5-S2's scope is cross-field option association only, per the PRD's "do two different set_* fields' option selections correlate with each other?" framing.

type SetSetPairSpec added in v0.32.0

type SetSetPairSpec struct {
	SetA    string                    `json:"set_a"`
	OptionA string                    `json:"option_a"`
	SetB    string                    `json:"set_b"`
	OptionB string                    `json:"option_b"`
	Cells   []CategoricalPairCellSpec `json:"cells"`
}

SetSetPairSpec is one option x option pair's generation-time reconstruction input between two DIFFERENT set_* fields — the Spec-facing counterpart of SetSetPairProfile. Cells reuse CategoricalPairCellSpec verbatim: AValue is SetA's OptionA state ("selected"/"not_selected"), BValue is SetB's OptionB state.

type ShapeProfile added in v0.32.0

type ShapeProfile struct {
	Means   []float64 `json:"means"`
	Stds    []float64 `json:"stds"`
	Weights []float64 `json:"weights"`
}

ShapeProfile is a fitted 2-component Gaussian mixture: parallel means/stds/weights, directly usable as the "mixture" distribution's (synth.DistMixture, E4-S1) own "means"/"stds"/"weights" params with no translation needed at generation time. See fitNumericShape in shape.go for how and when this gets populated.

type Spec

type Spec struct {
	// RowCount is the number of rows to generate. Required (>0).
	RowCount int `json:"row_count"`

	// Fields declares each column with its type and distribution.
	Fields []FieldSpec `json:"fields"`

	// Constraints, if non-empty, are evaluated per row by the expression
	// evaluator. Rows that fail any constraint are rejected and re-drawn.
	Constraints []ConstraintSpec `json:"constraints,omitempty"`

	// MaxRejectionRate caps the fraction of rejected rows during
	// constraint-driven rejection sampling. Defaults to 0.5 if zero.
	MaxRejectionRate float64 `json:"max_rejection_rate,omitempty"`

	// Correlations lists optional pairwise correlations to induce via
	// Gaussian copula post-processing. Each entry references two numeric
	// fields by name with a target Pearson correlation in [-1, 1]. Only
	// numeric fields can participate.
	Correlations []CorrelationSpec `json:"correlations,omitempty"`

	// CategoricalPairs lists optional categorical-categorical joint
	// structure to reproduce during generation: for each pair, field B
	// is resampled conditioned on field A's independently-drawn value
	// using the captured contingency cells, instead of being drawn from
	// its own unconditional marginal. Populated by SpecFromProfile from
	// ConditionalProfile.CategoricalPairs (`profile create --conditional`)
	// when present; absent (nil, the zero value) reproduces today's
	// independent-marginal behavior exactly — every pre-existing spec,
	// hand-authored or profile-derived, has no such key.
	CategoricalPairs []CategoricalPairSpec `json:"categorical_pairs,omitempty"`

	// CategoricalNumericPairs lists optional categorical-numeric
	// conditional structure to reproduce during generation: for each
	// pair, numeric field B is resampled from Normal(mean, std)
	// parameterized by categorical field A's drawn value, using the
	// captured per-category conditional mean/std, instead of being drawn
	// from its own unconditional normal. Populated by SpecFromProfile
	// from ConditionalProfile.CategoricalNumericPairs when present;
	// absent (nil) reproduces today's independent-marginal behavior
	// exactly.
	CategoricalNumericPairs []CategoricalNumericPairSpec `json:"categorical_numeric_pairs,omitempty"`

	// SetCategoricalPairs lists optional set-option x categorical joint
	// structure to reproduce during generation (E5-S3): for each pair,
	// the set field's declared Option (bit) is resampled from
	// Bernoulli(P(selected | categorical field's drawn value)) using the
	// captured contingency cells, instead of the option's own
	// independent marginal frequency. Populated by SpecFromProfile from
	// ConditionalProfile.SetCategoricalPairs when present; absent (nil)
	// reproduces the independent per-option marginal behavior exactly.
	SetCategoricalPairs []SetCategoricalPairSpec `json:"set_categorical_pairs,omitempty"`

	// SetNumericPairs lists optional set-option x numeric joint
	// structure to reproduce during generation (E5-S3): for each pair,
	// numeric field B is resampled from Normal(mean, std) conditioned on
	// whether the set field's declared Option (bit) was independently
	// drawn selected, using the captured per-bucket conditional
	// mean/std. Populated by SpecFromProfile from
	// ConditionalProfile.SetNumericPairs when present; absent (nil)
	// reproduces today's independent-marginal behavior exactly.
	SetNumericPairs []SetNumericPairSpec `json:"set_numeric_pairs,omitempty"`

	// SetSetPairs lists optional option x option joint structure between
	// two DIFFERENT set_* fields to reproduce during generation (E5-S3):
	// for each pair, set B's declared OptionB (bit) is resampled from
	// Bernoulli(P(selected | set A's OptionA drawn state)) using the
	// captured 2x2 contingency cells. Populated by SpecFromProfile from
	// ConditionalProfile.SetSetPairs when present; absent (nil)
	// reproduces the independent per-option marginal behavior exactly.
	SetSetPairs []SetSetPairSpec `json:"set_set_pairs,omitempty"`

	// Models lists optional per-numeric ADDITIVE linear predictors to
	// drive generation with, the Spec-facing counterpart of the profile
	// document's `models` section (`profile create --fit-models`).
	// Populated by SpecFromProfile from Profile.Models when present;
	// absent (nil, the zero value) reproduces today's behaviour exactly
	// — every pre-existing spec, hand-authored or profile-derived, has
	// no such key.
	//
	// Where CategoricalNumericPairs and SetNumericPairs each RESAMPLE a
	// numeric field from one paired field's conditional moments — so two
	// pairs naming the same numeric compete to be the last writer, and
	// resolveConflicts has to drop one — a model accounts for every
	// predictor in a single expression. SpecFromProfile therefore treats
	// the two as alternatives rather than layers: a profile carrying
	// `models` populates this slot and leaves both numeric-target pair
	// slots empty.
	Models []FieldModelSpec `json:"models,omitempty"`

	// ResidualCorrelations lists optional target correlations between
	// the RESIDUALS of two modelled numeric fields — the Spec-facing
	// counterpart of the profile document's `residual_correlations`
	// section (`profile create --residual-correlations`), populated by
	// SpecFromProfile from Profile.ResidualCorrelations when present.
	// Absent (nil, the zero value) reproduces the independent-residual
	// draw exactly: every modelled field takes its own fresh z, which
	// is what every pre-existing spec gets.
	//
	// This is a DIFFERENT quantity from Correlations above and the two
	// are not interchangeable. Correlations names a correlation between
	// two fields' VALUES and is realized by drawing both values from a
	// shared copula, which requires owning the value outright. Once a
	// field carries a model, its value is the model's to produce and
	// the only part still free to move is the residual — so this slot
	// correlates THAT, supplying each modelled field its component of
	// one shared correlated normal vector instead of overwriting what
	// the model drew. Imposing a value-scale figure on the residual
	// draw would apply every predictor the two targets share a second
	// time (see synth/residual_corr.go), which is why a Correlations
	// entry naming a modelled field is excluded rather than rerouted
	// here.
	//
	// Entries reuse CorrelationSpec verbatim: a pair of field names and
	// a rho, read on the residual scale. Both endpoints must carry a
	// model; an entry naming a field whose model did not survive is
	// dropped with a warning at generate() time, and validateSpec
	// refuses one outright for a hand-authored spec.
	ResidualCorrelations []CorrelationSpec `json:"residual_correlations,omitempty"`
}

Spec is the parsed top-level synthesis request. It is the in-memory shape of from-schema JSON. A from-profile call builds a Spec internally from the Profile and shares the rest of the writer pipeline.

func ParseSpec

func ParseSpec(raw []byte) (*Spec, error)

ParseSpec parses Spec JSON. Returns SERVICE_VALIDATION if shape is wrong.

func SpecFromProfile

func SpecFromProfile(p *Profile, rowCount int) (*Spec, []string)

SpecFromProfile builds a Spec the synth pipeline can execute. Numeric fields are reconstructed as normal distributions (mean, std clipped at min/max), categorical fields as weighted_categorical, and date fields as uniform_date over the observed range.

Choosing between `models` and `conditional`'s numeric-target arms

A numeric field that lands a model on the Spec (`profile create --fit-models`) is reconstructed from that additive linear predictor, and the two NUMERIC-TARGET conditional arms — CategoricalNumericPairs and SetNumericPairs — are then not populated FOR THAT FIELD. This is a REPLACEMENT, not a preference between equals: those arms exist to resample a numeric from one paired field's conditional moments, so on a cohort with several categorical and set fields every one of them claims the same numeric target and all but the first are dropped by resolveConflicts, one warning each. A model already accounts for all of those predictors simultaneously, so the pairs have nothing left to add and populating them would generate thousands of conflict warnings about relationships the spec no longer needs. Leaving the slots empty is what retires them as claimants — resolveConflicts itself is untouched and still arbitrates exactly as before for every spec that does populate them.

The retirement is PER TARGET, deliberately, and it did not start that way. It was first written per DOCUMENT — one `models` section retired both arms wholesale — on the reasoning that a spec mixing model-driven and pair-driven numerics would give two fields in the same cohort two different reconstruction semantics with nothing on the wire saying which got which. That reasoning does not survive contact with a real cohort. Predictor selection means a target can legitimately end up with no model (its marginal reconstructed to something a linear predictor cannot ride, or its design refused as unsolvable), and under the document-wide rule such a field lost its captured conditional pair as well and came out reconstructed from NOTHING — strictly worse than before --fit-models existed. The mixed-semantics objection is also answerable: the spec names both sets explicitly (Spec.Models versus Spec.CategoricalNumericPairs / Spec.SetNumericPairs), so the wire does say which got which, and resolveConflicts already claims a modelled target before any pair stage bids, so a field can never be reached by both. Falling back per field costs nothing and keeps a skipped model from silently deleting structure the capture measured.

The three NON-numeric-target arms (CategoricalPairs, SetCategoricalPairs, SetSetPairs) are unaffected and populate exactly as they always have: a model predicts a numeric FROM categorical and set structure, it says nothing about how that structure co-varies with itself. A user may legitimately pass --conditional and --fit-models together and gets both halves.

Absent `models`, every arm populates exactly as before — which is the path every document written before the section existed takes, and the same silent-fallback shape Conditional.NumericPairs-over-Pairwise already uses below.

The second return value carries any conditional-relationship conflict warnings resolveConflicts (synth/conflict.go, E6-S1) produces when run against the just-composed Spec — e.g. two captured pairs both legitimately targeting the same field. generate() (synth/writer.go) independently runs the same pass again at its own setup time, so these two calls always agree; this one exists only because Profile.Warnings is serialized to disk at `profile create` time, before SpecFromProfile ever runs at `synth from-profile` time — there is no way to retroactively write a synth-time conflict warning back into that already-persisted document. Returning the warnings here lets a caller (internal/cli's `synth from-profile`) fold them into the same FidelityWarnings channel it already uses for prof.Warnings, so both surface together through `--fidelity-report`. Callers that don't need them may discard with `_`.

func (*Spec) Hash added in v0.11.0

func (s *Spec) Hash() string

Hash returns the canonical content hash of the synth Spec. Same logical spec produces the same hash across processes and Pulse versions where the spec's semantic meaning is unchanged.

type TestRunner added in v0.32.0

type TestRunner func(physicalSchema, presentedSchema *encoding.Schema, records []byte, test *types.Test) (*types.TestResult, error)

TestRunner executes a single statistical Test against an encoded record buffer and returns its result. BuildFidelityReport calls it once per eligible field rather than reimplementing KS/chi-square math itself.

This package cannot import processing directly to provide its own implementation: descriptor imports synth (for the built-in distribution registry), and processing's own internal test files import descriptor — synth importing processing back would close that cycle. pulse.go, which already imports both synth and processing, supplies the real implementation and is the only intended caller of BuildFidelityReport.

physicalSchema is records' actual on-wire layout and MUST be used to decode it. presentedSchema is a view schema — same fields, same order, but with SyntheticFieldName's type widened from packed_bool to categorical_u8 (see SyntheticAsCategoricalSchema) — that the implementation must attach to each decoded record for type and dictionary lookups instead of physicalSchema. The two diverge only so the provenance column can satisfy TEST_KS.SplitBy / TEST_CHISQ's categorical requirement without changing the on-wire byte format.

type WarningGroup added in v0.33.0

type WarningGroup struct {
	// Kind is the stable, field-free label for this group. It never
	// interpolates a field name, so the group count is a count of
	// findings of one shape.
	Kind string
	// Attention is true when this kind means a requested capture or
	// generation step did NOT happen. False marks an expected outcome:
	// a complete, correct result the caller may nonetheless want to know
	// about.
	Attention bool
	// Members holds every warning classified into this group, in
	// production order. Callers that render to a terminal are expected
	// to show a bounded prefix; the full list stays in whatever document
	// the command already writes.
	Members []string
}

WarningGroup is one kind of warning together with every member of that kind, in the order they were produced.

Attention is the whole point of the type. A capture on a wide cohort legitimately emits thousands of lines that describe EXPECTED outcomes — a thin pair that still ships, a target no candidate explained, a pair claim arbitrated away — and a summary that counts those as faults trains the reader to ignore the channel, which is how the v0.32.x model-drop defect survived a whole effort. Attention marks the kinds where something the caller asked for did not happen.

func GroupWarnings added in v0.33.0

func GroupWarnings(warnings []string) []WarningGroup

GroupWarnings folds a flat warning slice into counted kind groups.

Ordering is a property of the input, never of a map: groups needing attention come first so a truncated rendering keeps them, then by descending count, then by kind name. Members within a group keep production order.

A nil or empty slice returns nil, so a caller can render unconditionally.

func (WarningGroup) Count added in v0.33.0

func (g WarningGroup) Count() int

Count is the number of warnings in the group.

Jump to

Keyboard shortcuts

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