Skip to content

Sense 14.2: Typed Signals — Dimensions, Units, and Standards

Sense 14.2 · Folded in · Last touched 2026-08-20

Folded into Sense sense-14-2.

  • last_verified: 2026-07-27
  • superseded_by: sense_14_2.md

Synced from docs/design/sense_14_2_typed_signals.md in the engine repo — that’s the source; this page is a build-time mirror.

Superseded 2026-05-20. The current authoritative Sense 14.2 reference is sense_14_2.md, rewritten to reflect the shipped Atelier, the shipped Sense 15, the diamond, the economic-lens sweep, and the lens-declarativity payoff. Decisive on the nine open questions this doc closed with. Preserved here as the conceptual archive — the five base types, the four standards, the contextual unit registry anchored to Subject Matter, and the currency-conversion-as-provenance principle all originated here. Read this for the thinking; read sense_14_2.md for what we’re building.

A signal without a type is a claim without a unit. A unit without a standard is a convention waiting to break.

Status: proposed Date: 2026-04-17 Depends on: Sense 14 (The Signal)


Sense 14 gave signals polarity, score, direction, and impact. But the types are informal:

  • impact_unit: CHF — a string, not a dimensional type
  • money_at_risk — a magic field, not a typed output
  • time_bucket — a free-form VARCHAR, not a temporal standard
  • evidence — a JSON blob hiding structured values

This means:

  1. No compile-time safety — a perspective can SUM kilograms and Swiss francs. The compiler doesn’t know the difference.
  2. No aggregation rules — what does AVG mean for a category? What does SUM mean across currencies? Nobody enforces this.
  3. No unit conversion — adding CHF and EUR silently produces nonsense.
  4. No temporal precision — “trailing 12 months” is prose buried in SQL, invisible to the Ledger and JinDesk.
  5. No rendering hints — JinDesk doesn’t know whether a column is a price, a weight, a ratio, or a date range.

Every signal output is typed, and every type is grounded in an international standard. The compiler validates types at compile time. JinDesk renders them correctly. The Ledger narrates them precisely. Perspectives aggregate them safely.

No invented vocabulary for units, currencies, or time.

Four standards cover everything jinflow signals will ever measure:

StandardGovernsExamples
UCUMUnits of measure, compound unitskg, m, d, %, {items}, CHF/{item}
ISO 4217Currency codes (subset of UCUM)CHF, EUR, USD
ISO 8601Time points, intervals, durations2026-04, P12M, 2025-04/2026-04
ISO 80000Quantity kind taxonomymass, length, time, dimensionless

The machine-readable encoding for all units. Designed for software systems to do dimensional analysis. Key properties:

  • Every SI unit has a code: kg, m, s, mol, K, cd, A
  • Compound units are algebraic: kg/m2, CHF/{item}, {items}/mo
  • Dimensionless quantities: %, {score}, {count}
  • ISO 4217 currency codes are valid UCUM atoms: CHF, EUR, USD
  • Annotations in braces: {items}, {cases}, {findings}

This means currency is not a special case. CHF is a UCUM unit. CHF/kg is a compound UCUM unit (price per kilogram). The type system treats money and physical quantities uniformly.

Reference: https://ucum.org/ucum

The conceptual layer above UCUM. Defines quantity kinds — the abstract dimensions that units measure:

Quantity kindUnitsWhat it covers
masskg, g, tWeight of materials
lengthm, cm, mmPhysical dimensions
times, min, h, d, mo, aDurations, ages
currencyCHF, EUR, USDMonetary values
dimensionless%, {score}, {count}Ratios, scores, counts

Two values can be aggregated (SUM, AVG) only if they share the same quantity kind. You can convert kg to g (same kind, different unit). You cannot add kg to CHF (different kinds).

The standard for temporal expressions. Covers everything jinflow needs:

ConceptISO 8601Current jinflow
Time point2026-04-17time_bucket as VARCHAR
Month2026-04time_bucket = '2026-04'
Quarter2026-Q2ad hoc strings
Year2026ad hoc strings
Interval2025-04/2026-04”trailing 12 months” as prose
DurationP12Mhardcoded in SQL
RecurrenceR12/P1Mnot expressible

Already well-known. Three-letter codes for currencies. jinflow currently assumes CHF everywhere. Multi-currency support requires explicit currency typing with conversion as a traceable step.

Five base types. No more, no less.

A numeric value with a unit and a quantity kind.

margin_per_unit:
type: quantity
unit: CHF/{item} # UCUM code
kind: currency # ISO 80000 quantity kind
range: [-50, 50] # expected domain (for rendering + severity)
polarity: higher_better # higher_better | lower_better | closer_to_zero

DuckDB storage: DOUBLE + metadata in the signal registry.

Aggregation: SUM / AVG / MIN / MAX within same kind. Cross-unit aggregation within the same kind requires an explicit conversion factor. Cross-kind aggregation is a compile error.

Examples:

  • Weight: unit: kg, kind: mass
  • Count: unit: {items}, kind: dimensionless
  • Ratio: unit: %, kind: dimensionless
  • Rate: unit: {items}/mo, kind: dimensionless
  • Score: unit: {score}, kind: dimensionless, range: [0, 100]
  • Duration: unit: d, kind: time

A monetary amount with an ISO 4217 currency code and a reference date.

exposure:
type: currency
currency: CHF # ISO 4217
ref_date: latest # or explicit ISO 8601 date
polarity: closer_to_zero

DuckDB storage: DOUBLE + VARCHAR (currency) + DATE (ref_date).

Aggregation: SUM only within the same currency code. Cross-currency aggregation requires an explicit conversion step that becomes a traceable node in the Ledger:

CHF 500 + EUR 300
conversion: EUR → CHF at 0.93 (2026-04-17, source: SNB)
CHF 500 + CHF 279 = CHF 779

The conversion rate, its source, and its date are provenance — they belong in the Ledger (Sense 18) as an explicit assumption.

money_at_risk is no longer a magic field. It becomes a well-known output name of type currency that signals can declare — or not.

A temporal value grounded in ISO 8601.

observation_window:
type: time
form: interval # point | interval | duration
granularity: P1M # ISO 8601 duration (bucket size)

Forms:

FormISO 8601ExampleUse case
point2026-04-17Single dateEvent date, delivery date
interval2025-04/2026-04Date rangeObservation window
durationP12MLength of timeHow far back to look

DuckDB storage: VARCHAR (ISO 8601 string) or DATE/TIMESTAMP for points.

This replaces the informal time_bucket VARCHAR. A signal declares its temporal window:

window:
duration: P12M # how far back
granularity: P1M # monthly buckets
anchor: latest # from the most recent data point

The Ledger can render: “This number covers 2025-03 through 2026-02 (12 months ending at the latest delivery).“

An enumerated value with a defined set of options.

compliance_status:
type: category
values: [compliant, partial, non_compliant]
ordered: true # implies natural ordering of values list

DuckDB storage: VARCHAR.

Aggregation: MODE / COUNT / DISTINCT. No arithmetic operations.

When ordered: true, the values list defines the ordering. This enables “worst-of” aggregation in perspectives (e.g., if any material is non_compliant, the entity-level status is non_compliant).

Boolean — the degenerate case. Today’s binary signal.

is_duplicate:
type: flag

DuckDB storage: BOOLEAN.

Aggregation: COUNT / ANY / ALL.

Every existing signal is implicitly flag type: the finding exists (true) or doesn’t. This is the migration baseline — existing signals keep working unchanged.

A signal declares its outputs — typed values that become real columns in the findings table.

signal_id: signal_negative_margin
type: balance
contract: "gold.v1"
entity_type: BillingEvent
outputs:
margin:
type: quantity
unit: CHF/{item}
kind: currency
range: [-50, 50]
polarity: higher_better
exposure:
type: currency
currency: CHF
polarity: closer_to_zero
observation_period:
type: time
form: interval
window:
duration: P12M
granularity: P1M
anchor: latest

Each output compiles to a column in signal_findings__signal_negative_margin:

ColumnDuckDB typeSource
out_marginDOUBLEcomputed by signal SQL
out_exposureDOUBLEcomputed by signal SQL
out_exposure_currencyVARCHARfrom output declaration (CHF)
out_exposure_ref_dateDATEfrom window anchor
out_observation_periodVARCHARISO 8601 interval

The out_ prefix distinguishes typed outputs from framework columns (finding_id, tenant_id, severity, etc.).

Existing framework columns remain:

ColumnStatusDerivation
severitykeptderived from score + thresholds (Sense 14)
money_at_riskkeptalias for the first currency-typed output, if any
scorekept0-100 continuous (Sense 14)
evidencekeptJSON blob for unstructured extras

Signals without an outputs: block work exactly as today. The typed system is opt-in, not a breaking change.

The type system defines what operations are valid:

TypeSUMAVGMIN/MAXCOUNTMODE
quantity (same kind+unit)yesyesyesyesno
quantity (same kind, diff unit)convert firstconvert firstconvert firstyesno
quantity (diff kind)compile errorcompile errorcompile erroryesno
currency (same code)yesyesyesyesno
currency (diff code)conversion stepconversion stepconversion stepyesno
timenonoyes (earliest/latest)yesno
categorynonoyes (if ordered)yesyes
flagCOUNT(true)nonoyesno

Perspectives declare their aggregation strategy per output:

perspective_id: perspective_material_health
source_signals:
- signal_negative_margin
- signal_price_stability
aggregation:
margin: { method: avg }
exposure: { method: sum }
compliance_status: { method: worst_of }

The compiler validates that avg is legal for the margin output’s type, and that worst_of is legal for an ordered category.

The signal compiler gains dimensional analysis:

  1. Unit validation — every unit field must be a valid UCUM code. Reject unit: francs (not UCUM). Accept unit: CHF (valid).

  2. Kind inference — the compiler infers quantity kind from the UCUM unit. kg → mass. CHF → currency. % → dimensionless.

  3. Aggregation check — perspectives that aggregate across signals must have compatible types. SUM of kg + CHF → compile error.

  4. Currency consistency — within a single signal, all currency outputs must declare a currency code. Cross-currency signals are legal but must declare a conversion reference.

  5. Temporal consistency — if a signal declares a window, its time-typed outputs must be compatible with the window’s granularity.

ComponentChange
Signal YAMLNew outputs: block with typed declarations
signalcompile.pyGenerate typed columns, validate UCUM codes
signalcheck.pyValidate output types, aggregation compatibility
Signal contractAdd output type schema to signal_contract.v1.json
Perspective compilerValidate aggregation methods against output types
Thesis compilerValidate evidence references against output types
JinDeskRender typed outputs (gauge for quantity, badge for category)
Ledger (Sense 18)Narrate units, ranges, conversion assumptions
Config enginedisplay.units for locale-aware unit formatting

When jinflow encounters cross-currency aggregation (e.g., Interlogic with EUR + USD + GBP shipments), conversion is NOT silent arithmetic. It is a traceable pipeline step:

  1. A reference_data source in pipeline.yml provides exchange rates:

    - id: exchange_rates_snb_2026
    source:
    type: reference_data
    path: "reference/exchange_rates.csv"
    expected:
    required_columns: [from_currency, to_currency, date, rate, source]
  2. The conversion appears in the Gold layer as a join, not a magic constant.

  3. The Ledger traces: “EUR 300 → CHF 279 at rate 0.93 (SNB, 2026-04-17). Rate source: reference_data/exchange_rates_snb_2026.”

  4. The “What could change this number” section lists the exchange rate as an explicit assumption.

Not all units are universal. Some depend on tenant-specific rules:

UnitDefinition depends on…Examples
FTEEmployment rules42 h/wk (RMC), 35 h/wk (France), 40 h/wk (Swiss academia)
bed-dayClinical counting rulesICU vs general ward, day-case included?
caseAdministrative definitionDRG case vs clinical episode vs admin case
supply unitMaterial masterPack size, dispensing unit vs billing unit
weighted caseWeight table versionCMI 2025 vs CMI 2026, SwissDRG vs G-DRG
standard costMethodology + base yearFull cost vs marginal, 2024 base vs 2025

These are contextual units — meaningful only with their definition. The value 2.4 FTE is useless without knowing what 1 FTE means at this tenant.

CategoryDefinition lives in…Conversion
UniversalStandard (UCUM/ISO)Built-in (kg→g, m→cm)
ConvertibleReference data (exchange rates)Pipeline step with provenance
ContextualTenant rules (unit registry)Tenant-specific definition

Each tenant can declare a unit registry — a small table mapping contextual units to their base-unit equivalents:

# In tenant.yml or a dedicated units.yml
units:
FTE:
base_unit: h/wk # UCUM base
factor: 42 # 1 FTE = 42 h/wk at this tenant
source: smebit_rmc_fte_definition # governance link
bed_day:
base_unit: d
factor: 1 # 1 bed-day = 1 calendar day
qualifiers:
- "excludes day-case admissions"
source: smebit_rmc_bed_day_rules

The registry is tenant-scoped (not pack-scoped): the same pack can serve tenants with different FTE definitions. The source field links to a Subject Matter Statement that governs the definition — who decided, when, and why.

The Ledger narrates contextual units with their definition:

“2.4 FTE (= 100.8 hours/week, per RMC employment rules — 1 FTE = 42 hours/week, defined by HR department, 2024-01-15)”

The definition is an assumption that appears in “What could change this number”: changing the FTE definition from 42 to 40 h/wk would change the converted value.

Cross-tenant aggregation of contextual units follows the same rule as cross-currency: convert to base unit first, and trace the conversion. A platform-level perspective that sums FTE across tenants with different definitions must normalize to h/wk before aggregating, and the Ledger documents which definition each tenant contributed.

The unit registry is data. Subject Matter is the governance layer:

  • A Statement documents what 1 FTE means at this tenant, who defined it, and why (employment contract, legal minimum, historical convention)
  • A Check can verify that the definition is still in force (e.g., compare against HR system export)
  • The source link in the unit registry points to the Statement, closing the loop between the type system and institutional knowledge

This is where Subject Matter and the type system meet: the meaning of a unit is expert knowledge, not data.

Every signal declares its observation window — the time range it looks at. Today this is hardcoded in SQL (WHERE date >= ...). With typed time, it becomes declarative:

window:
duration: P12M # ISO 8601 duration
granularity: P1M # bucket size
anchor: latest # latest | fixed(2026-04) | delivery

The compiler generates the SQL WHERE clause from this declaration. The Ledger narrates it. JinDesk can show it. Changing the window is a YAML edit, not a SQL rewrite.

AnchorMeaningUse case
latestEnd at the most recent data pointStandard trailing window
fixed(date)End at a specific dateReproducible analysis
deliveryEnd at the latest delivery dateAligned with data freshness

Sense 14 introduced score (0-100) as the continuous intensity measure. With typed outputs, score has two possible futures:

Option A: Score is derived from the primary output. If a signal emits margin: -5.80 CHF/{item} on a range of [-50, 50], the score is the normalized position in that range: (-5.80 - (-50)) / (50 - (-50)) * 100 = 44.2. Severity is derived from score. No manual scoring needed.

Option B: Score remains independent. The signal author sets score explicitly. Typed outputs are additional structured data alongside the score, not replacing it.

Option A is cleaner (single source of truth), but only works when the signal has a single primary numeric output with a meaningful range. Category and flag signals would still need explicit scores.

Recommendation: Option A as default for quantity and currency outputs with declared ranges. Explicit score for category, flag, and multi-output signals where no single output dominates.

  • Implement UCUM validation library (subset: ~50 common units)
  • Add outputs: parsing to signal compiler (ignored if absent)
  • Add window: parsing to signal compiler (ignored if absent)
  • Signals can declare outputs: — compiler generates typed columns
  • Existing signals unchanged (implicit flag type)
  • JinDesk renders typed columns alongside existing ones
  • Perspective compiler validates aggregation methods against output types
  • Cross-kind aggregation rejected at compile time
  • Severity derivation from range position (Option A)
  • currency type with ISO 4217 enforcement
  • Cross-currency conversion as explicit pipeline step
  • window: declaration generates SQL WHERE clauses
  • Ledger (Sense 18) narrates units, conversions, temporal windows
  • Sense 14 (The Signal): 14.2 is the type system that 14 sketched but left ungrounded. Polarity, score, direction survive. Impact becomes a typed output.
  • Sense 18 (The Ledger): typed outputs make provenance precise. The Ledger can say “CHF −5.80 per item” instead of “money_at_risk: −5.80”. Units, ranges, and conversion assumptions are narrated.
  • Sense 15 (Observations & Explanations): Explanations reference typed outputs. “The margin dropped from CHF 2.30 to CHF −5.80 per item over P6M” — every term is standards-grounded.
  • Sense 13 (The Canvas): typed outputs carry rendering hints. A quantity with range renders as a gauge. A currency renders with locale formatting. A category renders as a badge.
  1. UCUM subset: Do we validate against the full UCUM specification (~300 units), or maintain a curated subset (~50 units common in hospital procurement + logistics)? A curated subset catches typos better. Full UCUM is future-proof.

  2. Compound units: CHF/{item} is a price. {items}/mo is a rate. Should the compiler understand compound dimensional analysis (price × quantity = currency), or is that over-engineering for now?

  3. Quantity kind inference: Should the compiler infer kind from the UCUM unit automatically (kg → mass, CHF → currency), or require explicit declaration? Inference is convenient but may surprise.

  4. Score derivation: Option A (derived from range position) vs Option B (independent). See “The score question” above.

  5. Multi-currency in practice: When does this become real? Interlogic is the first multi-currency pack candidate. Is it worth building the conversion machinery before there’s a concrete use case, or should we stub the currency type now and implement conversion when needed?

  6. Temporal window inheritance: If a perspective aggregates signals with different windows (one P12M, one P6M), what window does the perspective declare? Union? Intersection? Explicit override?

  7. Existing money_at_risk column: Keep as a backward-compatible alias forever, or deprecate after migration? The alias is cheap but creates ambiguity with typed outputs.

  8. Contextual unit discovery: How do we discover which units a tenant uses that need definitions? Scan signal YAMLs for non-UCUM units and require a registry entry? Or maintain a curated list of known contextual units (FTE, bed-day, case, weighted case) that the validator checks against?

  9. Unit versioning: An FTE definition can change (new employment contract, new legal minimum). Should the unit registry carry a valid_from date, making historical analyses use the definition that was in force at the time? Or is one active definition per unit sufficient for now?


Numerical neighbors:Sense 14.2 Phase 5 — Typed Entity Aggregates · Sense 15 — Observation (entity with facets)

jazzisnow jinflow is a jazzisnow product
v0.64.7 · built 2026-09-20 19:48 UTC