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
- func AllDistributions() []string
- func ApplyRulesFile(fs afero.Fs, spec *Spec, path string) error
- func BuildCategoricalNumericPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, ...)
- func BuildCategoricalPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, ...)
- func BuildModelFidelity(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, ...)
- func BuildPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, ...)
- func BuildSetCategoricalPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, ...)
- func BuildSetNumericPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, ...)
- func BuildSetSetPairwise(report *FidelityReport, mergedSchema *encoding.Schema, records []byte, ...)
- func CountWarningsNeedingAttention(groups []WarningGroup) (attention, informational int)
- func ResolveConflicts(s *Spec) (catPairs []CategoricalPairSpec, catNumPairs []CategoricalNumericPairSpec, ...)
- func SyntheticAsCategoricalSchema(schema *encoding.Schema) *encoding.Schema
- func WriteRuleCandidates(fs afero.Fs, candidates []RuleSpec, path string) error
- func WriteSpec(fs afero.Fs, spec *Spec, path string) error
- type CategoricalNumericCategoryFidelity
- type CategoricalNumericCategorySpec
- type CategoricalNumericCategoryStat
- type CategoricalNumericPairFidelity
- type CategoricalNumericPairProfile
- type CategoricalNumericPairSpec
- type CategoricalPairCellSpec
- type CategoricalPairFidelity
- type CategoricalPairProfile
- type CategoricalPairSpec
- type CategoricalProfile
- type CategoryHit
- type ConditionalProfile
- type ConstraintSpec
- type ContingencyCell
- type CorrelationSpec
- type CorrelationStat
- type DateProfile
- type DiscreteLevel
- type DiscreteProfile
- type FidelityReport
- type FieldContinuation
- type FieldFidelity
- type FieldModel
- type FieldModelSpec
- type FieldProfile
- type FieldSpec
- type ModelFidelity
- type ModelPredictor
- type ModelPredictorFidelity
- type ModelPredictorSpec
- type ModelReference
- type ModelResidualCorrelationFidelity
- type ModelResidualPairFidelity
- type ModelResidualUnmeasuredPair
- type NumericPairProfile
- type NumericProfile
- type Options
- type PairwiseFidelity
- type Profile
- type ProfileOptions
- type ResidualCorrelation
- type ResidualCorrelationProfile
- type ResidualUnmeasuredPair
- type Result
- type RuleEvidence
- type RuleEvidenceBlockMember
- type RuleEvidenceDependency
- type RuleEvidenceLevel
- type RuleEvidenceMapValue
- type RuleEvidenceTarget
- type RuleSpec
- type RunContinuationProfile
- type SetCategoricalPairFidelity
- type SetCategoricalPairProfile
- type SetCategoricalPairSpec
- type SetFieldFidelity
- type SetNumericPairFidelity
- type SetNumericPairProfile
- type SetNumericPairSpec
- type SetOptionFidelity
- type SetOptionStat
- type SetProfile
- type SetSetPairFidelity
- type SetSetPairProfile
- type SetSetPairSpec
- type ShapeProfile
- type Spec
- type TestRunner
- type WarningGroup
Constants ¶
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" DistDiscrete = "discrete" DistSetBernoulli = "set_bernoulli" )
Distribution kind constants.
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.
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.
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.
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.
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.
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.
const RecoveryScaleProbitScore = "probit_score"
RecoveryScaleProbitScore is ModelFidelity.Scale for a target recovered through the calibrated interval-midpoint probit score rather than through a point latent inverse.
It is on the wire because the two comparisons are not the same measurement and a reader holding one must be able to tell which. A latent-inverse entry's recovered coefficient is a direct estimate; a score-scale entry's has been divided by a retention factor that is itself exact but that widens the standard error, so the same numeric gap carries less evidence.
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.
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.
const RunContinuationHighThreshold = 0.75
RunContinuationHighThreshold is the per-field rate at or above which RunContinuationProfile.HighFields lists a field: a field that repeats its previous row's bytes on at least three adjacent pairs in four is one the run-skip decode almost never rewrites.
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 ApplyRulesFile ¶ added in v0.34.0
ApplyRulesFile loads a standalone rules document from path, REPLACES spec.Rules with it, and validates the result against spec.
The file format is the Spec.Rules array itself — a bare JSON array of rule objects, identical to the `rules` key of a spec. E1-S2 fixed that deliberately so this loader needs no translation layer and an author can move a rule between the two places by cut and paste.
Merge semantics are REPLACE, not append. SpecFromProfile derives no rules, so on the path this exists for there is nothing to append to; inventing an append mode would only create an ordering question (does the file run before or after the spec's own rules?) that no caller has asked and that the declaration-order contract would then have to answer forever.
Validation runs HERE rather than being left to the eventual Synth call, for two reasons. A profile-derived spec bypasses validateSpec by design, so on the from-profile path there is no earlier gate; and the refusal has to name the FILE. An analyst editing a rules file beside a spec beside a profile, told only that "rule 2 names field \"nosuchfield\"", does not know which of the three documents is wrong. Every refusal below therefore carries details["path"], and the rule-specific faults keep E1-S2's own code (PULSE_SYNTH_RULE_FIELD_UNKNOWN and the rest) rather than degrading to a bare parse error — `pulse errors lookup` prose is the point of that family.
A refused file leaves spec.Rules exactly as it found it. A caller that logs and continues must not end up generating against half a rules document.
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
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.
func WriteRuleCandidates ¶ added in v0.34.0
WriteRuleCandidates writes detected rule candidates to path as an indented JSON array — the standalone rules-file format, byte for byte the shape ApplyRulesFile / ParseRules consume.
This is the library half of `profile create --suggest-rules`. The round trip is the contract and it is a GENERATION round trip, not a shape comparison: the written file, unmodified, is loaded by `synth from-profile --rules` and its rules fire (TestSuggestRules_WrittenFileGeneratesUnmodified). A test that merely unmarshals the file into []RuleSpec passes against a document whose predicates do not compile, whose targets name fields the derived spec does not declare, or whose `when` is silently never true — and all three are the silent-inertness class this effort exists to remove.
An EMPTY candidate list writes `[]` rather than nothing. "Detection found no gating relationship" is a real answer and the analyst asked for the file; leaving a stale file from a previous run in its place, or no file at all, makes the run's own result unreadable. `[]` is a legal rules document meaning "no rules" (see ParseRules).
Indented for the same reason WriteSpec is: the workflow is suggest -> read -> delete what you do not believe -> correct the `when` -> apply, and every step but the last is done by eye.
func WriteSpec ¶ added in v0.34.0
WriteSpec writes spec to path as indented JSON.
This is the library half of `synth from-profile --emit-spec`. It exists because SpecFromProfile derives a spec and generates from it in one breath, so until now nothing could print the thing that actually ran. That made the whole rule layer unreachable from the profile path — the only path the motivating use case takes — and it made authoring a rule guesswork, because writing {"when": "familiarity == 1", ...} requires knowing the field is called familiarity, that it is a u4 and that its floor is 1, and no command showed any of that.
It is equally a DIAGNOSTIC. The emitted document is the only place a reader can see which captured models survived translation, which distribution each field reconstructed to, and which conditional pairs were retired — three findings this package has silently lost before and each of which leaves a plausible-looking cohort behind.
Two properties are load-bearing rather than cosmetic:
- It marshals the spec VALUE, never a re-derivation from the profile and never a summary. The contract is that feeding the emitted file to `synth from-schema` at the same seed reproduces the same cohort byte for byte (TestEmitSpec_RoundTripsToByteIdenticalCohort), which a rendering cannot satisfy.
- It is INDENTED. The real workflow is emit -> read -> write rules -> apply, so a reader has to find a field name and its type in the document by eye.
Written through afero.Fs so the write path is hermetic in tests, per the repo-wide rule.
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"`
// Bernoulli is RETIRED, exactly as
// CategoricalNumericPairSpec.Bernoulli is — accepted so an older
// document still parses, read by nothing, derived from the target's
// own FieldSpec instead. THIS is the slot whose silent omission
// proved the case: SpecFromProfile never set it, so every
// profile-derived set-numeric pair over a boolean target drew a
// clamped normal. See CategoricalNumericPairSpec.Bernoulli for the
// full reasoning.
//
// Deprecated: declare the target field's distribution as
// DistBernoulli; this slot is ignored.
Bernoulli bool `json:"bernoulli,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 ¶
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 ¶
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 DiscreteLevel ¶ added in v0.34.0
type DiscreteLevel struct {
Value float64 `json:"value"`
Count int `json:"count"`
Frequency float64 `json:"frequency"`
}
DiscreteLevel is one observed integer level and its support.
type DiscreteProfile ¶ added in v0.34.0
type DiscreteProfile struct {
Levels []DiscreteLevel `json:"levels"`
}
DiscreteProfile is an integer-quantized numeric column's exact per-level histogram, in ascending level order.
Value/Count/Frequency mirrors SetOptionStat deliberately: a reader who has learnt one per-option table in this document has learnt them all. Count is the load-bearing figure — SpecFromProfile hands the raw counts to the `discrete` distribution's weights, so the reconstruction carries no float round trip — and Frequency is the human-readable share, normalised by the field's NON-NULL count (the same base SetProfile.Frequency uses), never by the cohort's row count.
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 FieldContinuation ¶ added in v0.39.0
FieldContinuation is one field's run-continuation rate.
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"`
// Scale names how this entry's comparison was put on the latent
// scale. Empty — the ordinary case — means the generated value was
// inverted through latentFor, a point inverse, and the recovered
// coefficient is a direct estimate. RecoveryScaleProbitScore means
// the target's Q is a step or staircase with no point inverse, so
// the recovery ran through the CALIBRATED interval-midpoint probit
// score (synth/fidelity_score.go): still latent-scale and still
// unbiased, but divided by a retention factor that widens the
// standard error, so the same numeric gap carries less evidence.
// A score-scale entry reports no intercept comparison — see
// RecoveredIntercept.
Scale string `json:"scale,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.
//
// On a Scale == RecoveryScaleProbitScore entry the recovered
// intercept is ABSENT and InterceptDelta with it. A staircase's cut
// points come from the captured MARGINAL, which generation holds
// exactly by construction, so the score-scale intercept is pinned by
// the marginal rather than by the model and carries no independent
// information about whether the coefficients were applied. Reporting
// a calibrated one would put a number where there is no measurement;
// the captured figure still ships, because the reader needs to see
// what the model declared.
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"`
// ScoreRetention is the fraction of this term's captured latent
// effect that survives onto the interval-midpoint probit score,
// present only on a Scale == RecoveryScaleProbitScore entry and
// absent (0) otherwise.
//
// It is computed, not estimated: the same score is projected onto
// the same design once from the generated values and once from the
// conditional mean the CAPTURED model implies, and their ratio is
// this number. Both the recovered coefficient and its standard error
// have been divided by it, so a small retention means a wide band
// rather than a suspect estimate — which is the whole reason it is
// on the wire. Two levels retain the least of any K; a long scale
// retains the most.
ScoreRetention float64 `json:"score_retention,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"`
// Discrete carries the EXACT per-level histogram of an
// integer-quantized column (u4/u8/u16/u32/u64 — see
// isIntegerQuantizedFieldType), captured unconditionally on the same
// single scan and present whenever the column carried at most
// maxDiscreteLevels distinct values.
//
// Additive + omitempty, so a document captured before this key
// existed is unchanged and SpecFromProfile's clamped-normal fallback
// still applies to it verbatim — the same silent-fallback shape
// Shape and conditional.numeric_pairs already use.
//
// The key's ABSENCE on an integer field is itself the record that the
// column was too wide and the histogram was ABANDONED rather than
// truncated: presence/absence is exactly the "which reconstruction
// will this field get" answer, so nothing is hidden by there being no
// warning for it. (Structural rather than conventional, for the same
// reason ResidualCorrelationProfile keeps `unmeasured` out of
// `pairs`.)
//
// It sits BESIDE Min/Max/Mean/Std/Percentiles and replaces none of
// them: they remain the field's summary for every reader, and
// reconstructing from the histogram recomputes its own exact moments
// rather than trusting these (see parseDiscreteLevels).
Discrete *DiscreteProfile `json:"discrete,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"`
// RunContinuation carries the measured per-field run-continuation,
// captured only when ProfileOptions.RunContinuation was set. Additive
// and omitempty; SpecFromProfile never reads it. See
// RunContinuationProfile.
RunContinuation *RunContinuationProfile `json:"run_continuation,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
// RuleCandidates carries the structural rules
// ProfileOptions.SuggestRules proposed, in the exact shape
// `synth from-profile --rules` consumes. It is `json:"-"` ON
// PURPOSE: these are a PROPOSAL for a human to read, edit and feed
// back, not a captured statistic, and writing them into the profile
// document would put an unreviewed structural claim inside the
// document that drives generation. It matches FieldModel.Residuals'
// precedent: a capture product whose only consumer runs in the same
// process, absent from a document read back from disk.
//
// The CLI writes them to the `--suggest-rules` path via
// WriteRuleCandidates.
RuleCandidates []RuleSpec `json:"-"`
}
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 ¶
ProfileFile reads a .pulse cohort from fs and produces a Profile. A shard archive is profiled as the concatenation of its shards, in archive order, decoded against the canonical `_schema.pulse` schema — the same reading the query engine's shard iterator applies.
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 ¶
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
// SuggestRules enables the structural-rule DETECTION pass
// (`profile create --suggest-rules <path>`): for every
// low-cardinality field and every other field, measure
// P(target null | field = level) off the same single scan and
// propose a rule for each field whose levels split a target's null
// rate into ~1 and ~0.
//
// Off by default, and ABSENT THE FLAG the profile document is
// byte-identical — detection is a proposal pass, not a capture.
//
// WITH the flag the document gains no SECTION: the candidates reach
// the caller on Profile.RuleCandidates, which is `json:"-"`, because
// an unreviewed structural claim must not ride inside the document
// that drives generation. The one thing that does move is
// `warnings`, which gains the detector's own lines (what it could
// not consider, what it counted rather than proposed, which
// candidates rest on thin levels) — deliberately, so those survive
// in a document rather than only on a terminal, which is how this
// package's last three silent defects survived. Nothing here is read
// back by SpecFromProfile and no generated byte moves;
// TestSuggestRules_DocumentMovesOnlyItsWarnings pins exactly that
// boundary. The candidates are written as a standalone rules file by
// the CLI (see WriteRuleCandidates) for the analyst to edit and feed
// to `synth from-profile --rules`.
SuggestRules bool
// RunContinuation enables the run-continuation measurement
// (`profile create --run-continuation`): per field, the fraction of
// adjacent row pairs whose on-wire bytes and null bit repeat — the
// exact hit rate of the run-skip decode — written as the additive
// `run_continuation` section (Profile.RunContinuation).
//
// Off by default and ABSENT THE FLAG the document is byte-identical
// and the scan pays nothing: the whole-row read that feeds it is only
// installed when the flag is set. With it, the measurement rides the
// existing scan (TestRunContinuation_RidesTheExistingScan counts the
// bytes pulled). SampleLimit bounds it like every other section.
RunContinuation 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 ¶
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.
type RuleEvidence ¶ added in v0.34.0
type RuleEvidence struct {
// Detector names the measurement that proposed the rule. One value
// per detector kind, so a reader can tell a gating candidate apart
// from the co-missing-block and exact-dependency candidates later
// detectors add to the same file.
Detector string `json:"detector"`
// Note is the prose caveat, interpolated with this candidate's own
// gate. It says what the measurement IS and what it is not, because
// detection finds the STATISTICAL gate and a human knows the
// SEMANTIC one, and those are routinely different fields that move
// together.
Note string `json:"note"`
// GateField is the field whose levels split the targets' null rate.
//
// This and the four slots below are the GATING detector's own, and
// carry `omitempty` so a co-missing candidate — which has no gate
// field, no levels and no targets — does not write five keys spelled
// like measurements with nothing measured in them. A gating
// candidate populates every one of them by construction (a gate has
// at least one level on each side and at least one target), so the
// wire shape of a gating candidate is unchanged by the tags; the
// keys that are shared with the co-missing detector deliberately do
// NOT carry omitempty, because a real 0 there is a real measurement.
GateField string `json:"gate_field,omitempty"`
// GatedLevels and OpenLevels are that split, as the level spellings
// the `when` predicate tests. The null pseudo-level is spelled
// "(null)" here and reaches the predicate as isnull(field).
GatedLevels []string `json:"gated_levels,omitempty"`
OpenLevels []string `json:"open_levels,omitempty"`
// Levels carries the per-level support and the mean conditional null
// rate across this candidate's targets — the shape of the split, at
// a glance, before a reader descends into Targets.
Levels []RuleEvidenceLevel `json:"levels,omitempty"`
// Targets carries the per-target conditional null rates: what the
// candidate actually measured, one row per field it proposes to
// null.
Targets []RuleEvidenceTarget `json:"targets,omitempty"`
// Block carries the CO-MISSING detector's members: the fields a
// null_together candidate names, each with the type, null count and
// null rate that admitted it.
//
// It is a slot on the one evidence type rather than a second
// evidence type because a reader of the candidate file walks one
// shape and reads `detector` to know which half of it is populated —
// and because the inertness contract (RuleEvidence's doc, and
// TestRuleEvidence_IsInert) is a property of the whole struct, so a
// parallel type would need its own copy of that guarantee and could
// drift out of it silently.
//
// Agreement is each member's null-set overlap with the FIRST member
// and is 1 for every member of an emitted block, by the admission
// rule. It is carried anyway, and the constant is the point: it is
// the detector's central claim — identical PATTERN, not merely an
// identical rate — stated on the wire where a reader can check it
// against the null counts beside it, rather than left implicit in
// the fact that the candidate was emitted at all.
Block []RuleEvidenceBlockMember `json:"block,omitempty"`
// SourceField and SourceLevels are the EXACT-DEPENDENCY detector's
// own: the single field every target of the candidate is a function
// of, and the number of levels it was observed to take.
//
// It is deliberately NOT GateField reused. A gate field's levels
// decide whether a target is ABSENT; a source field's levels decide
// what a present target's VALUE is. They are different claims about
// different halves of the record, and one key carrying both would
// make `detector` the only thing standing between a reader and the
// wrong reading.
SourceField string `json:"source_field,omitempty"`
SourceLevels int `json:"source_levels,omitempty"`
// Dependency carries the measured lookup, one entry per target: the
// source level to target value table the expression was READ OFF,
// and the rendering form chosen.
//
// It is a slot on the one evidence type rather than a third evidence
// type, exactly as Block is, so a reader walks one shape and reads
// `detector` to know which part of it is populated — and so the
// inertness contract (this type's own doc, and
// TestRuleEvidence_IsInert) stays a property of the whole struct
// rather than something a parallel type could drift out of.
Dependency []RuleEvidenceDependency `json:"dependency,omitempty"`
// RowsObserved is the cohort rows the detection saw (every row lands
// in exactly one level, the null pseudo-level included).
RowsObserved int `json:"rows_observed"`
// RowsAffected is the rows the `when` would select — the row count
// this rule would change. For a co-missing candidate, which carries
// no `when`, it is the rows on which the block is null. For an
// exact-dependency candidate, which also carries no `when`, it is
// the CO-PRESENT rows: the rows the lookup was measured on, which
// the admission rule (identical null patterns) makes exactly the
// rows either field is present on.
RowsAffected int `json:"rows_affected"`
// compare against each target's own null_rate in the profile
// document: for a real gate the two agree, because a target null on
// every gated row and present on every open one has exactly the
// gated share as its marginal null rate. That agreement is how this
// cohort's gate was first identified by hand.
GatedShare float64 `json:"gated_share"`
// MaxNullRateDeviation is the largest |null_rate - GatedShare| over
// the targets: how far the agreement above falls short.
//
// The figure that identified the motivating cohort's gate BY HAND
// was exactly this agreement — `1 - mean(aware)` = 0.2526093295989762
// matching 50 fields' captured null_rate to ten digits — and the
// obvious move is to make it the detector's lead ranking signal. It
// is carried, and it is deliberately NOT the lead signal, because it
// is ARITHMETICALLY IMPLIED by the split test rather than
// independent of it: if P(null | gated) >= gateHighNullRate and
// P(null | open) <= gateLowNullRate then the marginal null rate is
// GatedShare x ~1 + (1 - GatedShare) x ~0, so it lies within the
// thresholds of GatedShare for EVERY candidate the detector admits.
// Ranking by it would be ranking by rounding error, and
// TestSuggestRules_DeviationIsImpliedByTheSplitTest pins that.
//
// What it is genuinely for is CHECKING WITHOUT RE-MEASURING. A
// reader holding the profile document compares GatedShare against
// the targets' own null_rate, which is how the relationship was
// found in the first place; and a deviation that is unexpectedly
// large — several targets sitting near rather than at the open
// boundary — is the signal that a candidate is association wearing
// a gate's clothes.
MaxNullRateDeviation float64 `json:"max_null_rate_deviation"`
// MinLevelSupport is the thinnest level's row count, and ThinSupport
// says whether it fell below minGateLevelSupport. A thin candidate
// still SHIPS — suppressing it would hide the finding rather than
// qualify it — so this is the qualification.
//
// A co-missing candidate has two arms rather than levels — the rows
// where the block is null and the rows where it is present — and
// this is the thinner of them, which asks the same question of the
// same threshold: a block observed present on four rows is not a
// measurement of a question block. An exact-dependency candidate's
// is the thinnest SOURCE level: a mapping arm resting on four rows
// is a coincidence, not a derivation.
MinLevelSupport int `json:"min_level_support"`
ThinSupport bool `json:"thin_support,omitempty"`
}
RuleEvidence is the one slot on RuleSpec that DOES NOTHING.
Read that first, because every other member of RuleSpec executes and this is exactly the place a later reader will assume execution. The rule pass (synth/rules_apply.go) never looks at it, compileRules never compiles it, validateRules never reads a field of it, and nothing in it can name a field, an expression or a value that changes a generated byte. It is carried so that a PROPOSED rule and the measurement that proposed it cannot be separated.
Why it lives on RuleSpec rather than beside it ¶
`profile create --suggest-rules` writes a file that `synth from-profile --rules` must consume WITHOUT MODIFICATION, and E1-S2 fixed that format as a BARE JSON ARRAY of rule objects. That rules out a sibling metadata block: wrapping the array in {"rules": [...], "evidence": [...]} is precisely the shape E2-S1's loader refuses, deliberately, because silently reading zero rules out of a wrapper object is the failure the eager-validation rule exists to remove. JSON has no comments, so the third option does not exist.
A sidecar file was the remaining alternative and was rejected on the editing workflow: the file exists to be EDITED — an analyst deletes the candidates they do not believe and corrects the `when` of the ones they do — and evidence in a second document drifts from its rule on the first deletion, silently, in the direction of describing a rule that is no longer there. Carried on the rule, a deleted candidate takes its evidence with it.
Why the key is spelled `_evidence` ¶
The leading underscore is this repo's existing marker for a block that is documentation rather than payload — `examples/<dir>/*.json` carries `_meta` for the same reason, and the template renderer refuses a body that still has one attached. A reader scanning the file sees four executable keys spelled like verbs and one spelled like a note.
What it is NOT allowed to become ¶
A slot generation reads. If a future detector needs to influence generation it declares an ordinary executable slot and validates it; widening this one would make a document's meaning depend on a block authors are told they may delete freely.
Hand-authoring it is harmless and unvalidated: an author who writes nonsense here gets a rule that behaves exactly as it would with the key absent. A rule carrying ONLY this key still fails PULSE_SYNTH_RULE_EMPTY, because it declares no action — see validateRule, which counts the four action slots and not this one.
type RuleEvidenceBlockMember ¶ added in v0.34.0
type RuleEvidenceBlockMember struct {
Field string `json:"field"`
// Type is the field's `.pulse` type. It is carried because a block
// mixing a u4 with three packed_bool flags is a derived-partition
// shape and a block of twenty categoricals is a question battery,
// and a reader deciding whether to believe a proposal wants that
// without opening the profile document.
Type string `json:"type"`
// NullCount and NullRate are the member's own marginal. They are
// identical across an emitted block by the admission rule, which is
// what makes the member order arbitrary and what makes
// max_null_rate_deviation 0.
NullCount int `json:"null_count"`
NullRate float64 `json:"null_rate"`
// Agreement is the overlap of this member's null set with the FIRST
// member's: of the rows where either is null, the share where both
// are. 1 exactly when the two are null on the same rows.
Agreement float64 `json:"agreement"`
}
RuleEvidenceBlockMember is one field of a co-missing block.
type RuleEvidenceDependency ¶ added in v0.34.0
type RuleEvidenceDependency struct {
Field string `json:"field"`
// Type is the target's `.pulse` type, carried for Block's reason: a
// reader deciding whether to believe a derived-flag proposal wants
// to know whether the target is one bit or a small integer without
// opening the profile document.
Type string `json:"type"`
// Form names the RENDERING judgement — which reading of the lookup
// below the expression is. It is the one thing the expression itself
// cannot say, and the thing most worth checking: `threshold` and
// `threshold_chain` are TOTAL functions of the source (an
// unobserved value lands in the nearest band), while `membership`,
// `membership_complement` and `enumeration_chain` fall to a default
// arm.
Form string `json:"form"`
// Mapping is the measured lookup itself, in the source's own level
// order, with the rows behind each arm. Exceptions is 0 for every
// emitted candidate BY THE ADMISSION RULE, and is carried anyway for
// the reason Block's Agreement is: it is the detector's central
// claim — this is a FUNCTION, not a tendency — stated on the wire
// where a reader can check it against the supports beside it rather
// than left implicit in the fact that the candidate was emitted.
Mapping []RuleEvidenceMapValue `json:"mapping"`
Exceptions int `json:"exceptions"`
}
RuleEvidenceDependency is one target of an exact-dependency candidate, with the measured lookup that proposed it.
The rendered EXPRESSION is deliberately absent: it is already the rule's own `set_expr` entry for this field, and a second copy would go stale the first time an analyst edits the one that executes — in the direction of describing a rule that is no longer there, which is the exact failure the whole `_evidence` design (see RuleEvidence) exists to avoid.
type RuleEvidenceLevel ¶ added in v0.34.0
type RuleEvidenceLevel struct {
// Level is the level's spelling, matching an entry of
// RuleEvidence.GatedLevels or .OpenLevels.
Level string `json:"level"`
// Side is "gated" or "open".
Side string `json:"side"`
// N is the rows on which the gate held this level: the support
// behind every conditional rate computed at it.
N int `json:"n"`
// MeanTargetNullRate is the mean of P(target null | this level)
// across this candidate's targets. At a gated level it is ~1 and at
// an open one ~0, by the admission rule.
MeanTargetNullRate float64 `json:"mean_target_null_rate"`
}
RuleEvidenceLevel is one level of the gate field.
type RuleEvidenceMapValue ¶ added in v0.34.0
type RuleEvidenceMapValue struct {
Level string `json:"level"`
// Value is the target's value in its own JSON shape — a bool for a
// packed_bool, a number for a u4 — rather than a uniform string, so
// a reader can compare it against the profile document's own
// marginal for the field without decoding a convention.
Value any `json:"value"`
N int `json:"n"`
}
RuleEvidenceMapValue is one arm of a measured lookup: a level of the source, the value the target took at it, and the rows behind it.
type RuleEvidenceTarget ¶ added in v0.34.0
type RuleEvidenceTarget struct {
Field string `json:"field"`
// NullRate is the field's own marginal null rate over RowsObserved —
// the same figure the profile document carries for it, so a reader
// can check the candidate against the document without re-measuring.
NullRate float64 `json:"null_rate"`
// GatedNullRate and OpenNullRate are P(null | gated levels) and
// P(null | open levels), pooled over the levels on each side.
GatedNullRate float64 `json:"gated_null_rate"`
OpenNullRate float64 `json:"open_null_rate"`
}
RuleEvidenceTarget is one field the candidate proposes to null, with the conditional rates that proposed it.
type RuleSpec ¶ added in v0.34.0
type RuleSpec struct {
// When is an optional expr-lang predicate over the row. The rule
// applies to rows satisfying it; ABSENT MEANS EVERY ROW, which is
// not the same as "never" and is the form a derived-field rule
// (`set_expr` computing a value from other columns) normally takes.
// Compiled against the same environment Spec.Constraints uses
// (rowExprEnv / rowExprOptions), so the two cannot diverge: every
// scalar including packed_bool is a number, a categorical is a
// string, a set_* is a map[string]bool, and absence is tested with
// isnull(field). Must return a bool.
When string `json:"when,omitempty"`
// SetNull names fields to null on a matching row. It removes a value
// rather than supplying one, so the field's own generation still
// matters on every row the rule does not gate — which is why a
// set_null target is never pre-claimed away from its model.
SetNull []string `json:"set_null,omitempty"`
// OwnsNulls declares that THIS RULE is the only source of absence
// for the fields it names in SetNull: their own `null_rate` draw is
// DISCARDED and the rule's gate becomes the single thing that can
// null them. Scoped to SetNull and to nothing else — a rule
// declaring it without a SetNull field is refused
// (PULSE_SYNTH_RULE_OWNERSHIP_INVALID).
//
// # The arithmetic it fixes
//
// A captured `null_rate` is a MARGINAL: it already includes whatever
// the gate removed. Left in place beneath a `set_null` the two
// compose — a field is null when the gate fires OR when its own
// draw says so — and the generated rate comes out at
// g + (1-g)*r rather than r. Measured on the motivating 122-field
// survey profile, `regard` declares 0.2526 and generated 0.4418
// behind a `{"when": "aware == 0"}` gate that is exactly right about
// WHICH rows are absent. Declaring ownership lands it on the gate's
// own firing rate, which is what the captured number was measuring.
//
// # Zero, not a residual, and why
//
// The exactly-correct suppression is the RESIDUAL rate conditional
// on the rule not firing, (r - g) / (1 - g). It needs g, the rule's
// firing PROBABILITY, and a `when` is an arbitrary predicate over a
// row: nothing in a Spec knows how often it will be true, and
// estimating it would need a pre-pass whose own rows are drawn from
// the rates it is trying to correct. So the declaration zeroes the
// draw and the gap is MEASURED at generation instead of guessed —
// ownershipWarnings reports any owned field whose realised null rate
// misses its declared one (synth/rules_ownership.go).
//
// Where g IS knowable the residual is bounded rather than unknown:
// `profile create --suggest-rules` admits a gate only when the
// target is null on at least gateHighNullRate of gated rows and at
// most gateLowNullRate (0.02) of open rows, and that second
// threshold IS the residual. Every candidate detection can emit is
// therefore within 0.02 of exact under a zeroed draw — the same
// distance nullRateDivergenceThreshold already calls immaterial —
// which is why detection emits this flag on every gating candidate.
//
// # Two rules may own one field
//
// Ownership is a UNION, not an exclusive claim, and deliberately not
// routed through resolveConflicts' claim(): `set_null` REMOVES a
// value rather than supplying one, so it never claims a field away
// from its own generation (E2-S2), and "two gates can each account
// for this field's absence" is a coherent statement needing no
// arbitration. The draw is suppressed once.
//
// # Its relationship to NullTogether
//
// A block already does this for its non-gate members, by COPY rather
// than by suppression: applyNullTogether overwrites every member's
// decision with the gate's, so their own null_rate is discarded
// exactly as this flag discards it. The gated-block idiom — name a
// never-null field first so the copy clears the block's own MCAR
// nulls, then supply the real gate with a `set_null` — is ownership
// obtained as a SIDE EFFECT of that copy. OwnsNulls is the same
// statement said directly, and on the rule that makes it true. The
// block is still required for the shape it alone expresses: a
// co-missing block with NO gating field, where one member's own draw
// is the block's only source of absence.
OwnsNulls bool `json:"owns_nulls,omitempty"`
// Set assigns LITERAL values: a number or bool for a scalar target,
// a declared category string for a categorical_*, an array of
// declared options for a set_*. Never an expression — see SetExpr,
// and see the type doc for why the two are separate keys.
Set map[string]any `json:"set,omitempty"`
// SetExpr assigns the RESULT of an expression evaluated over the row
// at the moment this rule runs. The expressions compile here; the
// per-target coercion of the result (bool to 1/0 for a numeric
// target, string to a dictionary entry for a categorical) is applied
// by the rule pass.
SetExpr map[string]string `json:"set_expr,omitempty"`
// NullTogether nulls a block of fields as ONE decision rather than
// per field, the co-missingness shape a survey question block has:
// the FIRST named field's null state is copied to the rest, so the
// block is all null or all present and never a mixture. Every other
// member's own null_rate is consequently IGNORED — the cost of the
// only resolution that adds no randomness, reported by
// nullTogetherWarnings when the discarded rate is materially
// different. Requires at least two distinct fields — a one-field
// block is set_null spelled less clearly. Applied LAST within its
// rule; see applyNullTogether.
NullTogether []string `json:"null_together,omitempty"`
// Evidence is the ONE INERT SLOT: the measurement that proposed this
// rule, when `profile create --suggest-rules` wrote it. Nothing in
// generation reads it — not compileRules, not the rule pass, not
// validateRules — and it cannot change a generated byte. It rides
// the rule rather than a sibling document so a candidate an analyst
// deletes takes its evidence with it, and so the file stays the bare
// array `--rules` consumes unmodified. Read RuleEvidence's own doc
// before touching this; the inertness is the contract, and
// TestRuleEvidence_IsInert is the gate on it.
Evidence *RuleEvidence `json:"_evidence,omitempty"`
}
RuleSpec is one STRUCTURAL rule: a statement about which fields a row may carry a value for, and what that value is, imposed on top of whatever the distributions, conditional pairs, correlations and models produced. It is the mechanism for the class of survey fact no statistical summary can express — "the perception block is not asked of a respondent who has never heard of the brand", "promoter is nps >= 9" — which generation otherwise reproduces as soft variation with a plausible-looking marginal and no gate at all.
Five slots, exactly one of which (`when`) is a predicate and four of which are actions, plus one MODIFIER (`owns_nulls`) that changes what a `set_null` means for the fields it names rather than adding an action of its own:
{"when": "familiarity == 1",
"set_null": ["perception_1", "perception_2"],
"owns_nulls": true,
"set": {"segment": "unaware"},
"set_expr": {"promoter": "nps >= 9"},
"null_together": ["nps", "nps_reason"]}
`set` and `set_expr` are SIBLING KEYS rather than one map carrying a {"$expr": ...} marker, and that is a settled decision rather than an accident of implementation: one map leaves {"set": {"region": "west"}} undecidable between a categorical literal and a bare identifier, while two keys make the question impossible to ask. Do not merge them.
Application semantics ¶
Rules apply in DECLARATION ORDER, sequentially, LAST WRITE WINS. An expression reads whatever the row holds at the moment its rule runs, which a later rule may still change — so a `set_expr` reading a field an EARLIER rule wrote sees the new value, and one reading a field a LATER rule will write sees the old one. That is correct and surprising, and it is why the order is the author's to see: the rules are deliberately NOT topologically sorted, because a sort would make the applied order implicit and declaration order is the one ordering an author can read off the document.
The pass runs ONCE per row, LAST — after the model stage, the final thing to touch a row before it is encoded. The semantics that buys are `if gate then null else inferred`: a gated field is still drawn normally, through its own sampler and whatever model or conditional pair owns it, and the rule then masks or replaces it on the rows the `when` selects, leaving the inferred value everywhere else.
The pass consumes no RNG, so a spec declaring no rules generates byte-identical output to the same spec with the slot absent.
Validation is EAGER — every fault below is refused at spec parse, not at row 400,000 (see validateRules).
The standalone rules-file format is this array itself, so an inline `rules` declaration and a `--rules` file are the same JSON.
func ParseRules ¶ added in v0.34.0
ParseRules decodes a standalone rules document — the bare Spec.Rules array — WITHOUT validating it against any spec. Validation needs the field declarations, so it belongs to ApplyRulesFile; this is the decode step alone, exported for callers holding the bytes already.
An empty array is accepted and means "no rules", which is a legal statement rather than a malformed document. A JSON object is refused: wrapping the array in {"rules": [...]} is the most likely author mistake and silently reading zero rules out of it would produce a run with every gate missing — the exact failure the eager-validation rule exists to remove.
type RunContinuationProfile ¶ added in v0.39.0
type RunContinuationProfile struct {
// Pairs is the number of within-shard adjacent row pairs compared.
Pairs int `json:"pairs"`
// Shards is the number of payloads scanned.
Shards int `json:"shards"`
// Overall is the mean of the per-field rates — the fraction of all
// (pair, field) comparisons that repeated, i.e. the share of field
// writes the run-skip decode avoids on a full-row read.
Overall float64 `json:"overall"`
// HighThreshold is the rate at or above which a field is listed in
// HighFields (RunContinuationHighThreshold).
HighThreshold float64 `json:"high_threshold"`
// HighFields lists, in schema order, the fields at or above
// HighThreshold: the high-continuation block a sort key holds
// constant (on a denormalised join, the parent block).
HighFields []string `json:"high_fields"`
// Advice is a one-sentence reading of Overall for a user deciding
// whether to sort the source upstream.
Advice string `json:"advice"`
// Fields carries every schema field's rate, in schema order.
Fields []FieldContinuation `json:"fields"`
}
RunContinuationProfile is the measured run-continuation of a cohort (`pulse profile create --run-continuation`, ProfileOptions.RunContinuation): per field, the fraction of adjacent row pairs whose on-wire bytes for that field — and, for a nullable field, its null bit — are identical.
It is the exact per-field hit rate of the run-skip decode (encoding/reader_runskip.go), which rewrites only the fields whose bytes or null bit changed since the previous row. Because the optimisation is a property of the DATA rather than the format, a cohort re-imported without its upstream ORDER BY loses it with nothing on the wire saying so; this section is how it becomes visible.
Pairs never span a shard boundary. Each shard of an archive is a separately written payload and the decode restarts its comparison baseline at each one, so a pair across two shards measures nothing the optimisation can use. Shards counts the payloads scanned (1 for a single-file cohort); Pairs is the sum over shards of (rows − 1).
Additive and omitempty: absent from every document captured without the flag. SpecFromProfile never reads it — it describes the row ORDER of the source, which generation does not reproduce.
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"`
// Bernoulli is RETIRED: accepted so every document written before
// the retirement still parses, and READ BY NOTHING.
//
// What it used to declare — the numeric target is a BOOLEAN
// marginal, so the cell's captured Mean is a prevalence and the
// draw is Bernoulli(Mean) rather than Normal(Mean, Std) — is now
// DERIVED from the target's own FieldSpec at generate() setup
// (buildBernoulliConditionals), exactly as the `discrete` staircase
// always was.
//
// The retirement is the point, not a tidy-up. A flag can DISAGREE
// with the field's own reconstruction, and a marginal disagreeing
// with the draw that overwrites it is the failure class the boolean
// and small-integer work exists to remove — invisible, because every
// cell still renders a plausible prevalence and only the number is
// wrong. It was not hypothetical: SpecFromProfile's SET-numeric arm
// admitted a DistBernoulli target, said in a comment that the flag
// was "carried the same way" as the categorical-numeric arm's, and
// did not set it, so a profile-derived set-numeric pair over a
// boolean target reproduced the clamped-normal defect once PER CELL.
// Deriving makes that omission unrepresentable.
//
// A spec still declaring it over a target whose marginal is NOT
// bernoulli gets a warning naming the pair (bernoulliFlagWarnings),
// never a refusal and never silence; declare the FIELD `bernoulli`
// instead. Do not reintroduce a read of this slot.
//
// Deprecated: declare the target field's distribution as
// DistBernoulli; this slot is ignored.
Bernoulli bool `json:"bernoulli,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"`
// Rules lists optional STRUCTURAL rules — statements about which
// fields a row may carry a value for and what that value is, imposed
// on top of whatever the distributions, conditional pairs,
// correlations and models produced. Additive and omitempty: absent
// (nil, the zero value) reproduces today's behaviour exactly, and
// because the rule pass consumes no RNG, a spec declaring no rules
// is byte-identical to one written before the slot existed.
//
// This is the slot for the class of fact no statistical summary can
// express, because it is not variation at all: a question block that
// is not ASKED of a respondent who failed a screener, a flag that is
// a band of another column. Reconstructed from marginals those come
// back as soft noise with a plausible rate and no gate.
//
// Rules apply in DECLARATION ORDER, sequentially, LAST WRITE WINS,
// in one pass at the very end of the row — see RuleSpec for the full
// semantics and for why they are deliberately not topologically
// sorted. The standalone rules-file format is this array itself, so
// an inline declaration and a `--rules` file are the same JSON.
//
// Validation is EAGER: validateRules refuses every malformed rule at
// spec parse with a PULSE_SYNTH_RULE_* code naming the rule index.
Rules []RuleSpec `json:"rules,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 SpecFromProfile ¶
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 `_`.
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.
Source Files
¶
- augment.go
- conditional_sample.go
- conflict.go
- constraints.go
- copula.go
- discrete.go
- distributions.go
- doc.go
- fidelity.go
- fidelity_models.go
- fidelity_residual.go
- fidelity_score.go
- hash.go
- mixture_quantile.go
- model_draw.go
- moments.go
- profile.go
- profile_comissing.go
- profile_continuation.go
- profile_dependency.go
- profile_gating.go
- profile_models.go
- profile_models_select.go
- profile_models_shrink.go
- profile_rule_order.go
- regression_record.go
- residual_corr.go
- residual_draw.go
- rules.go
- rules_apply.go
- rules_claim.go
- rules_coerce.go
- rules_evidence.go
- rules_firing.go
- rules_firing_cause.go
- rules_ownership.go
- shape.go
- spec.go
- spec_io.go
- synth.go
- warning_summary.go
- writer.go