buildDefaultRegistry
fnfunction buildDefaultRegistry(): OpRegistry
Build a registry pre-populated with the built-in ops under the canonical names referenced in processing.json.
stream
A configurable analysis pipeline over tabular data. Ops, tags, and checks — no rendering, no framework.
Load a frame, run it through a graph of named operations described in JSON, and get back rows, verdicts or a cross-tab. Nothing here draws anything: the pipeline is the product, and the host decides what to do with what comes out.
It is the engine underneath GestaltBI, and it runs standalone — in Node, in a worker, in a browser, in a test.
npm install @gestaltbi/stream
on npm →
Three pieces. An op is a unit of computation with a run(df) method. A process graph names
ops and says which must run first. The column directory tells ops what each column is, so no op
ever hard-codes a column name.
import { Processor, StructureDirectory, type ProcessConfig } from '@gestaltbi/stream';
import { firstValueFrom } from 'rxjs';
const columnDirectory = new StructureDirectory(structureJson);
const processes: ProcessConfig = JSON.parse(processingJson);
const proc = new Processor({ columnDirectory, processes });
proc.workOn({ data: rows });
const frame = await firstValueFrom(proc.getProcessed('by_month'));
getProcessed(name, identifier) returns an Observable that re-emits whenever the data or the filter
changes. The identifier keys the filter state, so several views can read the same graph with different
filters and never see each other's — setFilter(filter, identifier) pushes one through.
A graph is JSON, which means a deployment can change what the analysis does without a rebuild. Each
entry names an op, optionally the processes it requires, and its options.
{
"type": "processing",
"version": "1",
"process": {
"format_dates": { "op": "format", "options": { "dateTag": "uatu:date", "numberTag": "uatu:measure" } },
"clear": { "op": "clear", "require": ["format_dates"] },
"localfilter": { "op": "localfilter", "require": ["clear"] },
"margin": {
"op": "enhance",
"require": ["localfilter"],
"options": { "columns": [
{ "column": "calc:margin", "calculate": "expr", "expr": ["-", "revenue", "cost"] }
]}
},
"by_month": { "op": "aggregate", "require": ["margin"], "options": { "groupby": ["uatu:date"] } }
}
}
Fan-out needs no ceremony — point two processes at the same upstream. Fan-in needs an op that asks
for it: require may name several processes, but a frame only reaches an op that declares it reads
more than one.
{
"actuals": { "op": "aggregate", "require": ["clean"], "options": { "groupby": ["sku"] } },
"targets": { "op": "clear", "require": ["target_import"] },
"vs_target": { "op": "join", "require": ["actuals", "targets"],
"options": { "on": "sku", "prefix": "target:" } }
}
join is a left join by default, because enrichment should not drop the rows that failed to match,
and it takes only the first match on a duplicated key — a join that quietly multiplies rows breaks
every sum below it. union stacks frames and can record which branch each row came from, which
turns the origin into a dimension you can group by.
An op that knows a column is called revenue only works on one dataset. Ops ask the directory for
columns carrying a tag instead, so the same graph runs against any structure that describes itself.
| tag | meaning |
|---|---|
uatu:dimension | something to group or split by |
uatu:dimension:time | the time axis; ordering and period logic use it |
uatu:dimension:geo | carries a coordinate or a place |
uatu:dimension:cohort | a cohort bucket |
uatu:measure | a number worth aggregating |
uatu:aggregable | may be rolled up by the aggregate op |
uatu:measure:flow | produced by a period, so it sums across periods |
uatu:measure:stock | a level at a point in time — summing it is meaningless |
uatu:measure:ratio | carries numerator / denominator, so it re-aggregates correctly |
uatu:measure:unit:<unit> | stops covers comparing terabytes with dollars |
uatu:measure:basis:cash / :accrual | which accounting basis a money column is on |
Untagged numeric columns keep behaving as they always did — the vocabulary is additive, and a structure that says nothing extra loses nothing.
The single most common way to get an aggregate wrong is to average a ratio. A success rate over three months is not the mean of three monthly rates — it is the total successes over the total attempts, and the two differ whenever the months are different sizes.
{
"column": "calc:success_rate",
"tags": ["uatu:measure", "uatu:measure:ratio"],
"aggregation": [
{ "type": "ratio", "target": "calc:success_rate", "numerator": "successful", "denominator": "resolved" }
]
}
Declared this way, aggregate, pivot and every roll-up accumulate the numerator and the
denominator separately and divide once at the end. The rate of a group becomes the rate of that group.
On the public Kickstarter set that is the difference between 58.50% and 58.81% — small, and wrong.
aggregate rolls a frame up along one axis. pivot puts a second axis across the top, which is
what it takes to see how two dimensions interact rather than each one's totals separately.
import { Pivot } from '@gestaltbi/stream';
const pivot = new Pivot(
{ rows: ['category'], columns: ['country'], measure: 'pledged', type: 'sum', totals: true },
context,
);
const table = pivot.run([rows, {}]); // one record per category, one field per country
columnLimit caps how many buckets are emitted and folds the tail into otherLabel, so a
high-cardinality dimension degrades into a readable table instead of a thousand columns.
Scores every pair of columns and ranks them: Pearson or Spearman between two measures, eta between a dimension and a measure, Cramér's V between two dimensions.
new Correlate({ method: 'spearman', minCoefficient: 0.5, include: ['measure-measure', 'dimension-measure'] }, context)
A number nobody can contradict is a number nobody should trust. A check is a predicate over a
processed frame that returns a Verdict — pass, fail, warn or skip — naming the periods that broke it.
import { runChecks } from '@gestaltbi/stream';
const verdicts = runChecks(
[
{ id: 'margin', type: 'sign', measure: 'calc:margin', expect: '>0', label: 'Margin was positive' },
{ id: 'covers', type: 'covers', measure: 'revenue', by: 'cost' },
],
frame,
{ columnDirectory: directory, orderBy: 'uatu:date' },
);
| type | asserts |
|---|---|
monotonic | a measure never falls (or never rises) across the ordered periods |
sign | a measure keeps a sign — >0, >=0, <0, <=0 |
covers | one measure is at least as large as another, unit-aware |
divergence | two measures stay within a relative gap of each other |
window_complete | the period at the edge is settled enough to compare |
ratio_bounds | a ratio stays inside an expected band |
The assert op runs them inside a graph and is normally terminal: it returns verdicts rather than
rows, so a host can put pass/fail cards beside the charts and re-run them on every refresh.
Extend AbstractOp, register it, and it is available to the graph under whatever name you give it.
import { AbstractOp, buildDefaultRegistry } from '@gestaltbi/stream';
class Redact extends AbstractOp {
run([rows]) {
const secret = this.columnDirectory.getColumnsFor('pii:name');
return rows.map((r) => ({ ...r, ...Object.fromEntries(secret.map((c) => [c, '—'])) }));
}
}
const registry = buildDefaultRegistry();
registry.register('redact', Redact);
An op that needs to load something first implements getExternal(), which returns an Observable the
processor waits on before every run. That is how geocode fetches its lookup without this package
depending on an HTTP client.
An op that combines frames overrides runAll instead of run, and declares how many inputs it
reads. The number is declared rather than inferred so a bad graph can be caught before anything runs —
and so a visual editor knows how many input sockets to draw.
export class Interleave extends AbstractOp {
public override readonly inputs = 2;
public override runAll([left, right]) {
return left.flatMap((row, i) => (right[i] ? [row, right[i]] : [row]));
}
}
Extracted from src/index.ts when this page was built, so it
cannot drift from what the package actually exports.
function buildDefaultRegistry(): OpRegistry
Build a registry pre-populated with the built-in ops under the canonical names referenced in processing.json.
function dimensionColumns(dir: ColumnDirectory | undefined): string[]
Every column acting as a dimension, in {@link DIMENSION_TAGS} order, deduplicated.
function finalize(type: AggKind | string, target: any): any
Turn an accumulator into the value that lands in the output record.
function neuter(type: AggKind | string): any
Empty accumulator for a kind.
function resolveTimeColumn( dir: ColumnDirectory | undefined, rows?: any[], explicit?: string, ): string | undefined
Find the column carrying time, in order of confidence: 1. an explicit column name from the op's options 2. any of {@link TIME_TAGS} in the column directory 3. a column literally named uatu:date — the canonical code that mapping.json produces, present even when no structure is loaded 4. the first column whose first value is a Date
Returns undefined when nothing matches, so callers can decide whether that is fatal or merely means "leave the order alone".
function runCheck(check: Check, rows: any[], ctx: CheckContext = {}): Verdict
Run one check and return its verdict.
Never throws: a missing column, an empty frame or a unit mismatch comes back as skip with a reason, because a check that cannot run has not failed and must not be reported as though it had.
function step(type: AggKind | string, target: any, value: any, spec?: AggSpec, fact?: any): any
Fold one value into an accumulator.
Non-numeric input is skipped rather than folded in: parseFloat('') is NaN, and one blank cell used to poison a whole column's sum.
class AbstractFilter extends AbstractOp
The predicate shared by the filter ops.
A filter is an object keyed by column: an array means "one of these", and an object means a comparison — between, gt, lt and friends. Absent or empty entries match everything, so a partly-filled filter narrows rather than excludes.
class AbstractOp implements Op
Base class for every op.
Holds the options and the context, and passes the frame straight through until a subclass overrides run. Constructed without a context it falls back to one that knows no columns, so an op built outside a process graph runs rather than throwing on first access.
inputs: numberSee {@link Op.inputs}. Raised by ops that combine frames.
public getExternal(): Observable<any>public setOptions(options: any)public run(df: any): anypublic runAll(inputs: any[], externals: any): anyDefault multi-input behaviour: read the first input, ignore the rest.
Which is what an op written against the single-input contract means. An op that genuinely combines inputs overrides this instead of run.
class Aggregate extends AbstractOp
Rolls the frame up along options.groupby.
How each measure folds comes from the structure, not from here: a column declares its own aggregation specs, so one op produces :sum, :avg, :last and ratio columns together, and a rate is accumulated as numerator and denominator and divided once.
ac: anyrun(df: any): anyneuter(type: string): anyagg(type: string, target: any, value: any, spec?: AggSpec, fact?: any): anyfinalize(type: string, target: any): anyclass Assert extends AbstractOp
Runs validation checks over the upstream frame.
Unlike every other op, assert is normally terminal: it returns an array of {@link Verdict} records rather than data rows, so the host can render them as pass/fail cards. Set passthrough: true to return the rows untouched and read the verdicts off {@link Assert.getVerdicts} instead — useful when you want a check in the middle of a graph.
``json { "op": "assert", "require": ["margin"], "options": { "checks": [ { "id": "gross-margin-positive", "type": "sign", "measure": "margin_accrual", "expect": ">0" } ] } } ``
public run(df: any): anypublic getVerdicts(): Verdict[]Verdicts from the most recent {@link run}.
class ClearEmpty extends AbstractOp
Drops rows with no identity.
A CSV with a trailing newline, or a sheet exported with padding, arrives with rows whose id is empty. They are not observations, and counting them shifts every total.
public run(df: any): anyclass Cohort extends AbstractOp
Cohort axis: bucket rows by the period a subject first appeared, and stamp how many periods have elapsed since.
Retention and conversion curves need "periods since first event" as a real dimension; aggregate can only group by columns that already exist, so this op creates them.
``json { "op": "cohort", "options": { "subject": "user_id", "date": "event_date", "period": "month", "into": { "cohort": "cohort_month", "since": "periods_since" }, "window": { "days": 30, "asOf": "2013-11-06", "mask": ["retained"] } } } ``
window is the guard that stops a half-observed cohort reading as a perfect one: any cohort younger than days as of asOf has the listed measures set to null and is flagged incomplete. Without it a cohort formed yesterday shows 100% 30-day retention, which is an artefact, not a result.
public run(df: any): anyclass Correlate extends AbstractOp
How strongly the columns of a frame move together.
The rest of the package reads a frame along time — deltas, recognition, cohorts. This reads it across dimensions instead, and answers the question a pivot table raises but cannot settle: of everything collected, which pairs actually travel together, and how strongly.
Three relationships, because "correlation" means a different statistic depending on what is being related:
- measure x measure — Pearson's r, or Spearman's rho on ranks when the relationship is monotonic but not straight. Signed: -1 to 1. - dimension x measure — the correlation ratio eta: how much of the measure's variance is explained by which group a row falls in. 0 to 1. - dimension x dimension — Cramer's V over the contingency table. 0 to 1.
``json { "op": "correlate", "require": ["clean"], "options": { "method": "spearman", "minCoefficient": 0.3, "limit": 20 } } ``
Terminal, like assert: it returns {@link Association} records rather than rows, ordered strongest first.
A coefficient is not a cause. Two measures derived from one another — a total and its own component — will correlate near 1 and mean nothing.
public run(df: any): anyclass Deviation
Joins two streams on aggField, optionally pads, then applies an Enhance pass to compute derived fields. Used by the diff/change visualizations.
stream1: Observable<any[]>stream2: Observable<any[]>prefixes: any[]fields: any[]outStream: Observable<any[]>getStream(): Observable<any[]>pimp(prefix: string, list: any[], keep: string): any[]class DiffCalc extends AbstractOp
Period-over-period deltas, and the stock/flow bridge.
For each configured measure emits the absolute change, the relative change, and (for a stock) the implied flow — the first difference of a level. Rows are ordered by the date column; the first period gets null, not 0, so a missing baseline never reads as "no change".
``json { "op": "diffcalc", "options": { "date": "month", "measures": [ { "column": "users", "kind": "stock", "flowInto": "users_added" }, { "column": "revenue", "kind": "flow", "lag": 12, "suffix": "_yoy" } ] } } ``
With no measures, every column tagged uatu:measure:stock or uatu:measure:flow is picked up automatically.
public run(df: any): anyclass Enhance extends AbstractOp
Adds derived columns.
Each entry names the column to write and how to compute it: expr for a Polish-notation expression over other columns, or func for a windowed function such as cumsum accumulated along cumulateOn. With nullSafe a missing input yields an absent result rather than a spurious zero.
public run(df: any): anyoperate(row: any, op: any, df: any): voidfuncCall(df: any[], column: string, func: string, options: any[]): voidhydrate(row: any, field: any): anyneuter(op: string): numberpolish(expr: any[]): number | nullclass Format extends AbstractOp
Parses the frame into the types the rest of the graph assumes.
A CSV arrives as strings. Dates are parsed with options.dateFormat for every column carrying options.dateTag, numbers for every column carrying options.numberTag — by tag, so the same op serves any structure.
public run(df: any): anyparseDate(date: any): DatecleanNumber(num: any): number | anyclass Geocode extends AbstractOp
Attaches coordinates by looking rows up in external geocoding files.
The files are named in options.geocoding and fetched through the context's ExternalFetcher, so this package never depends on an HTTP client of its own.
public getExternal(): Observable<any>public run(data: any[]): anyclass GeoDeviation
Like Deviation but reshapes both upstream GeoJSON FeatureCollections into row-oriented data first, joins, then re-wraps the result as GeoJSON for map rendering.
geocolumn: stringstream1: Observable<any>stream2: Observable<any>prefixes: any[]fields: any[]aggField: stringlatField: stringlonField: stringoutStream: Observable<any>provs1: Map<string, any>provs2: Map<string, any>getStream(): Observable<any[]>class Geojsonify extends AbstractOp
Turns rows into a GeoJSON FeatureCollection.
Coordinates come from the columns tagged gcx:lat and gcx:lon, and every other field rides along as a feature property. Also records each numeric property's range, which is what a choropleth needs to pick a scale.
geoj: anypublic run(df: any): anypublic extractGeoJsonRange(object: any): voidnumberify(row: any): anyclass GlobalFilter extends AbstractFilter
Applies the filter every stream shares.
Use it above the fork point, for the constraints that belong to the whole page — the period under study, the company being looked at.
public run(df: any): anyclass Heatmap extends AbstractOp
Reserved: registered as heatmap, but not implemented.
It inherits the pass-through run, so a graph naming it gets its input back unchanged rather than an error. Named here so a config written against it keeps loading.
class Join extends AbstractOp
Bring the columns of one frame onto the rows of another, matched on a key.
A left join by default, because the usual reason to reach for this is enrichment — targets onto actuals, a lookup table onto transactions — and losing the rows that did not match would quietly change every total computed downstream. inner is there when the match itself is the question.
A right-hand key with several rows is a fan-out waiting to happen, so only the first match is taken: a join that silently multiplies row counts breaks every sum below it, and is the classic way a dashboard starts overstating.
inputs: anypublic override runAll(inputs: any[]): anyclass LocalFilter extends AbstractFilter
Applies the filter belonging to one stream.
Keyed by options.identifier, so two views reading the same graph can narrow their own copy without disturbing each other.
public run(df: any): anyclass OpRegistry
Maps op-name strings (as referenced from processing.json) to op classes. Pre-populated with the eleven built-in ops by Processor's default constructor; callers can register additional custom ops at runtime.
register(name: string, op: OpConstructor): voidhas(name: string): booleanget(name: string): OpConstructor | undefinedinstantiate(name: string, opts: any, ctx: OpContext): Op | nullConstruct an op instance bound to the supplied context.
class Pivot extends AbstractOp
Cross-tabulation: dimensions down the side, dimensions across the top, one aggregated measure in the cells.
aggregate rolls a frame up along a single axis. This puts a second axis across the top, which is what it takes to see how two dimensions interact rather than each one's totals separately.
``json { "op": "pivot", "require": ["clean"], "options": { "rows": ["product_family"], "columns": ["region"], "measure": "revenue", "type": "sum", "totals": true } } ``
Emits one plain record per row key — the row dimensions, then one field per column bucket — so a grid or a chart consumes it without unpacking a nested shape.
Column buckets are capped: pivoting across something high-cardinality would otherwise emit thousands of fields. The tail is collected under otherLabel and counted in {@link Pivot.getOmitted}, never dropped without saying so.
public getColumns(): string[]Column buckets emitted by the last run, in output order.
public getOmitted(): numberHow many distinct column values fell past columnLimit into the tail.
public run(df: any): anyclass Processor
Orchestrates streaming data through a graph of named ops.
Held data: - start: the immutable input dataframe (after workOn) - work: the current "live" dataframe (mutated in place by ops) - one OLAP Cube materialized from the dimension hierarchy
Streams are keyed by an identifier string. Multiple consumers can subscribe to independent processed streams concurrently.
processes: ProcessConfigstart: anywork: anymode: string | undefinedcube: anycubeError: Error | undefinedSet when {@link initializeAggregator} could not build a cube.
workObs: Observable<any> | undefinedlocalFilterObs: anylocalFilterSub: anysetMode(mode: string): voidinitializeAggregator(data: any): voidworkOn(dataframe: any): voidclear(): voidresolvedStreams(): string[]Which streams are currently built, as identifier::process.
The live shape of the graph rather than its declared shape — useful for a debug view, and for an editor that wants to show what a given view actually caused to run.
getProcesses(): string[]process(name: string, identifier = 'default'): voidBuild (or reuse) the stream for one process, and leave it where this identifier's consumer will find it.
getProcessed(processed: string | null = null, identifier = 'default'): Observable<any>clearStreams(): voidgetDimensionMembers(dimension: string): any[]liveCube(): anysetFilter(filter: any, identifier = 'default'): voidgetFilter(identifier = 'default'): anygetProcessInfo(name: string): anycontext(): OpContextThe context ops are constructed with. Exposed so a host can build an op — or a Deviation / GeoDeviation — outside a process graph and still give it the column directory and fetcher the graph-run ops get.
class Recognize extends AbstractOp
Revenue recognition — the matching principle, mechanized.
Spreads an amount booked in one period across the periods it actually serves, so a twelve-month prepayment stops looking like twelve months of margin on the day it lands. Also emits the deferred balance: the obligation still owed.
``json { "op": "recognize", "options": { "amount": "cash_yearly", "date": "month", "term": 12, "into": "recognized_yearly", "deferredInto": "deferred_yearly" } } ``
term is either a constant number of periods or, via termColumn, a column holding a per-row term. Rows are matched on their position in the date order, so the frame must carry one row per period (no gaps) — {@link Cohort} or a prior aggregate normally guarantees that.
Amounts scheduled past the last row are not lost: they stay in the final deferred balance and are counted in {@link Recognize.getSpill}.
public run(df: any): anypublic getSpill(): numberAmount scheduled beyond the end of the frame (still owed, not yet recognizable).
class Regionify extends AbstractOp
Reserved: registered as regionify, but not implemented.
Like {@link Heatmap}, it passes the frame through untouched.
class StructureDirectory implements ColumnDirectory
Reference {@link ColumnDirectory} backed by a structure.json document.
The Angular client wraps its own service; this exists so the package is usable — and testable — on its own, with no host framework.
structure: StructureDocstatic fromJSON(text: string): StructureDirectorygetColumnsFor(tag: string): string[]getDataStructureFor(tag: string): StructureDocgetColumn(code: string): StructureColumn | undefinedFull entry for one column code.
getTags(): string[]Every tag in use, deduplicated — the raw material for an auto-built glossary.
getDimensionHierarchies(): anyDimension hierarchies in the shape olap-cube-js expects: { dimensionHierarchies: [{ dimensionTable: { dimension, keyProps }, level: [] }] }.
One flat hierarchy per dimension column — including the refinements, since uatu:dimension:geo marks a dimension every bit as much as the base tag does. If a structure declares no dimensions at all the cube is given an empty list rather than a malformed one.
class Union extends AbstractOp
Stack several frames into one.
The simplest thing two branches can do once the graph allows them: last year and this year, two regions prepared differently, a forecast beside an actual. Rows keep their own columns — a column missing from one input is simply absent on those rows rather than filled with a zero nobody measured.
sourceInto writes which branch each row came from, which is usually what makes the union worth doing: it turns the origin into a dimension you can then group by.
inputs: anyAnything from two upwards; the graph decides.
public override runAll(inputs: any[]): anyinterface AggSpec
ratio reads its own two columns off the fact rather than a single value.
type?: AggKindnumerator?: stringdenominator?: stringinterface Association
One scored pair: which two columns, how strongly they move together, and by what method.
a: stringb: stringkind: PairKindmethod: stringpearson / spearman for two measures, eta for a split, cramersV for two dimensions.
coefficient: number | nullSigned for measure pairs, 0..1 for the others. Null when it could not be computed.
n: numberComplete observations behind it.
strength: 'none' | 'weak' | 'moderate' | 'strong' | 'undefined'summary: stringOne line, safe to render straight into a card.
reason?: stringSet when the pair was skipped.
interface CheckContext
What a check is run against, beyond the rows themselves.
columnDirectory?: ColumnDirectoryorderBy?: stringFallback ordering column when a check does not name one.
interface ColumnDirectory
Column metadata directory consumed by ops that need to look up which raw columns are tagged a certain way (e.g. all date columns, all geocodable address columns).
Implement this with whatever backing store fits (a JSON document, a dbt manifest, a config file). The Angular adapter wraps a service that loads it from assets/structure.json.
getColumnsFor(tag: string): string[];Returns column codes that carry the given tag.
getDataStructureFor(tag: string): any;Returns the structure document filtered to only columns carrying the tag.
getDimensionHierarchies(): any;Returns OLAP cube dimension hierarchies derived from tagged columns.
interface CorrelateOptions
Which pairs to score, and how strong a result has to be to be worth reporting.
measures?: string[]Numeric columns. Defaults to everything tagged uatu:measure and present in the frame.
dimensions?: string[]Categorical columns. Defaults to every column tagged as a dimension.
method?: 'pearson' | 'spearman'pearson (default) measures a straight-line relationship, spearman a monotonic one.
include?: PairKind[]Which families to compute. Default: all three.
pairs?: Array<[string, string]>Only these pairs, in order. Overrides the generated matrix.
minPairs?: numberFewer complete observations than this and the pair is skipped. Default 3.
maxLevels?: numberA dimension with more levels than this is not summarised. Default 50.
minCoefficient?: numberDrop pairs weaker than this absolute coefficient. Default 0 (keep all).
limit?: numberCap on emitted rows, strongest first.
interface CoversCheck extends BaseCheck
Asserts one measure is at least as large as another, every period.
Unit-aware: comparing a column tagged in terabytes against one in dollars skips rather than answering a question nobody asked.
type: 'covers'measure: stringThe measure that must be at least as large as by.
by: stringatLeast?: number | 'all'interface DivergenceCheck extends BaseCheck
Asserts two measures stay within a relative gap of each other.
For the pairs that should track — a cash and an accrual basis of the same quantity — where the interesting event is them coming apart.
type: 'divergence'a: stringb: stringwarn?: numberRelative gap |a-b| / max(|a|,|b|) above which to warn / fail.
fail?: numberinterface JoinOptions
Which key to match on, which rows to keep, and what to carry across.
on: string | [string, string]Key column. Use [left, right] when the two sides name it differently.
type?: 'left' | 'inner'left keeps every left row; inner keeps only the matched ones. Default left.
prefix?: stringPrefix for columns coming from the right, so a shared name does not overwrite.
columns?: string[]Right-hand columns to bring across. Default: all but the key.
interface MonotonicCheck extends BaseCheck
Asserts a measure never falls (or never rises) across the ordered periods.
type: 'monotonic'measure: stringdirection?: 'increasing' | 'decreasing'strict?: booleanRequire strict change between periods (default false — flat is allowed).
interface Op
The contract an op implements: run over a frame, and say what it needs loaded first.
run(df: any): any;Run the op synchronously over df, where df[0] is upstream data and df[1]+ are external resources.
runAll?(inputs: any[], externals: any): any;Run over every input, for an op that reads more than one process.
Optional, and the reason the single-input contract never had to change: an op that does not implement it is called through run with its first input exactly as before. An op that does — a join, a union — receives the frames in the order its require array named them.
inputs?: numberHow many processes this op reads. One unless it combines frames.
Declared rather than inferred so it can be checked before anything runs — a graph that wires two inputs into an op that reads one is a mistake worth a message, not a silently discarded branch. A visual editor reads the same number to know how many input sockets to draw.
getExternal(): Observable<any>;Return any external resources the op needs combined with upstream data before run.
setOptions?(options: any): void;Optional: replace runtime options (used by the registry on instantiation).
interface OpContext
Context handed to every op at construction. Held on AbstractOp so subclasses can access the column directory, fetch external resources, and ask the host pipeline for filter state.
columnDirectory: ColumnDirectoryfetcher: ExternalFetchergetFilter: (identifier?: string) => anyinterface PivotOptions extends AggSpec
How to lay out a cross-tab, and how to fold the measure in its cells.
rows?: string[]Dimensions down the side. Defaults to every dimension not used as a column.
columns?: string[]Dimensions across the top. Omit for a plain group-by with one measure column.
measure?: stringMeasure to roll up. Not needed for count.
type?: AggKindHow to fold the measure. Default sum.
totals?: booleanAdd a per-row total and a grand-total record.
totalInto?: stringField name for the row total. Default Total.
totalLabel?: stringLabel of the grand-total record in its first row dimension. Default Total.
columnLimit?: numberHighest number of column buckets to emit. Default 50.
otherLabel?: string | nullWhere the tail beyond columnLimit goes. Set null to drop it. Default Other.
emptyLabel?: stringStands in for a null or empty dimension value. Default (blank).
separator?: stringJoins several column dimensions into one header. Default / .
prefix?: stringPrepended to every generated column field.
interface ProcessConfig
The shape of processing.json: a map of process names to specs.
process: Record<string, ProcessSpec>interface ProcessorOptions
Everything a {@link Processor} needs to run a graph.
columnDirectory: ColumnDirectoryColumn metadata source.
processes: ProcessConfigProcess graph (e.g. parsed from processing.json).
fetcher?: ExternalFetcherExternal resource fetcher (defaults to no-op so non-fetching ops still work).
registry?: OpRegistryPre-built op registry. If omitted, a default with the eleven built-in ops is used.
interface ProcessSpec
A single named transformation step in a process graph.
op?: stringOp key registered in the OpRegistry.
require?: string[]Process names that must be wired upstream of this one.
options?: anyOp-specific configuration; merged with {identifier} at runtime.
interface RatioBoundsCheck extends BaseCheck
Asserts a ratio stays inside an expected band.
A conversion rate above 1 or below 0 is a broken denominator, not a result.
type: 'ratio_bounds'measure: stringmin?: numbermax?: numberinterface SignCheck extends BaseCheck
Asserts a measure keeps its sign.
type: 'sign'measure: stringexpect: '>0' | '>=0' | '<0' | '<=0'atLeast?: number | 'all'Periods that must satisfy it: a count, or 'all' (default).
interface StructureColumn
A column entry in a structure.json document.
column: stringtype?: 'string' | 'number' | 'date' | 'boolean'tags?: string[]label?: stringaggregation?: Array<{ target: string; type: 'sum' | 'avg' | 'last' | 'first' | 'min' | 'max' | 'median' | 'concat' | 'ratio'; /** `ratio` only — sum these, then divide once. */ numerator?: string; denominator?: string; }>Consumed by aggregate: how this column rolls up.
interface StructureDoc
The parsed structure.json: what every column is, and how it aggregates.
type?: stringversion?: stringname?: stringcolumns: StructureColumn[]interface UnionOptions
How to stack the frames, and whether to record where each row came from.
sourceInto?: stringColumn to write each input's origin into, when you need to tell them apart.
sourceLabels?: string[]Names for each input, positionally. Defaults to the index.
distinctOn?: string[]Drop rows whose values for these columns have already been seen.
interface Verdict
What a check concluded.
Carries enough to render without re-running anything: a one-line summary, how many periods were evaluated, how many satisfied it, and the rows that broke it.
id: stringtype: stringstatus: CheckStatussummary: stringOne-line, human-readable result. Safe to render straight into a card.
label?: stringmeasure?: stringn?: numberPeriods actually evaluated (nulls excluded).
hits?: numberPeriods satisfying the predicate.
observed?: numberexpected?: stringoffenders?: any[]The rows that broke it, capped by offenderLimit.
reason?: stringSet when the check could not run (missing column, no rows, unit mismatch).
interface WindowCompleteCheck extends BaseCheck
Asserts a period is settled enough to be compared with the others.
A 30-day-active metric for a cohort that started a week ago is not low, it is unfinished. This is what stops the newest bar being read as a decline.
type: 'window_complete'measure: stringcohortDate: stringColumn holding the period/cohort start date.
windowDays: numberMeasurement window in days (e.g. 30 for a 30-day-active metric).
asOf: string | DateObservation date. Cohorts younger than the window as of this date are incomplete.
span?: 'day' | 'week' | 'month' | 'quarter' | 'year' | numberHow wide the cohort bucket is. A monthly cohort is not fully observed until the window has elapsed past the end of the month — someone who signed up on 31 October is only six days old on 6 November even though the bucket starts 36 days back. Default 'day' treats each row as a point in time, which is the old behaviour.
type AggKind = | 'sum' | 'avg' | 'last' | 'first' | 'min' | 'max' | 'median' | 'concat' | 'ratio' | 'count' | 'countDistinct'
Aggregation kinds, shared by every op that rolls values up.
aggregate and pivot accumulate the same way on purpose: a sum in a pivot cell has to mean what a sum in a rolled-up row means, or the two views of the same figure disagree.
type Check = | MonotonicCheck | SignCheck | CoversCheck | DivergenceCheck | WindowCompleteCheck | RatioBoundsCheck
Any check, discriminated by type.
type CheckStatus = 'pass' | 'fail' | 'warn' | 'skip'
The four answers a check can give. skip is not a failure: it means the check could not run.
type ExternalFetcher = (url: string) => Observable<any>
External resource fetcher. Ops that need to load files (e.g. geocoding GeoJSON) request them through this. The Angular adapter wraps HttpClient; in Node you'd pass a function backed by fetch or axios.
type OpConstructor = new (opts: any, ctx: OpContext) => Op
Constructor signature expected by the registry.
type PairKind = 'measure-measure' | 'dimension-measure' | 'dimension-dimension'
What kind of pair a coefficient describes.
const allPassed
True when no verdict failed. Warnings do not fail a suite.
const byDate
Ascending comparator over a date-ish column. Unparseable values compare equal.
const DIMENSION_TAGS
Every tag that marks a column as a dimension.
uatu:dimension:time, :geo and :cohort are refinements of uatu:dimension, so a structure carrying only the refinement still has a dimension. Reading the base tag alone makes a dataset whose only axis is time look like it has no axes at all.
const num
Coerce to a finite number, or null. Blank cells are absent, not zero.
const runChecks
Run a list of checks. Order preserved.
const TIME_TAGS
Tags that mark the time dimension, newest vocabulary last.
Two vocabularies are in circulation. Config repos authored for gestaltbi-core (see GestaltBI/sample-config) rename the date column to the canonical code uatu:date via mapping.json and tag it uatu:timedimension. @gestaltbi/infer emits the newer uatu:dimension / uatu:dimension:time pair instead. Both are accepted everywhere so an op works against either.
namespace tags
Tag vocabulary.
Tags are the contract between structure.json and the ops: an op never hard-codes a column name, it asks the ColumnDirectory for the columns carrying a tag. Everything here is a plain string so a structure document authored by hand (or by @gestaltbi/infer) stays readable.