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.mdin 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; readsense_14_2.mdfor 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)
What pulled us in
Section titled “What pulled us in”Sense 14 gave signals polarity, score, direction, and impact. But the types are informal:
impact_unit: CHF— a string, not a dimensional typemoney_at_risk— a magic field, not a typed outputtime_bucket— a free-form VARCHAR, not a temporal standardevidence— a JSON blob hiding structured values
This means:
- No compile-time safety — a perspective can SUM kilograms and Swiss francs. The compiler doesn’t know the difference.
- No aggregation rules — what does AVG mean for a category? What does SUM mean across currencies? Nobody enforces this.
- No unit conversion — adding CHF and EUR silently produces nonsense.
- No temporal precision — “trailing 12 months” is prose buried in SQL, invisible to the Ledger and JinDesk.
- No rendering hints — JinDesk doesn’t know whether a column is a price, a weight, a ratio, or a date range.
The vision
Section titled “The vision”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.
The standards
Section titled “The standards”Four standards cover everything jinflow signals will ever measure:
| Standard | Governs | Examples |
|---|---|---|
| UCUM | Units of measure, compound units | kg, m, d, %, {items}, CHF/{item} |
| ISO 4217 | Currency codes (subset of UCUM) | CHF, EUR, USD |
| ISO 8601 | Time points, intervals, durations | 2026-04, P12M, 2025-04/2026-04 |
| ISO 80000 | Quantity kind taxonomy | mass, length, time, dimensionless |
UCUM (Unified Code for Units of Measure)
Section titled “UCUM (Unified Code for Units of Measure)”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
ISO 80000 (Quantities and Units)
Section titled “ISO 80000 (Quantities and Units)”The conceptual layer above UCUM. Defines quantity kinds — the abstract dimensions that units measure:
| Quantity kind | Units | What it covers |
|---|---|---|
| mass | kg, g, t | Weight of materials |
| length | m, cm, mm | Physical dimensions |
| time | s, min, h, d, mo, a | Durations, ages |
| currency | CHF, EUR, USD | Monetary 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).
ISO 8601 (Date and Time)
Section titled “ISO 8601 (Date and Time)”The standard for temporal expressions. Covers everything jinflow needs:
| Concept | ISO 8601 | Current jinflow |
|---|---|---|
| Time point | 2026-04-17 | time_bucket as VARCHAR |
| Month | 2026-04 | time_bucket = '2026-04' |
| Quarter | 2026-Q2 | ad hoc strings |
| Year | 2026 | ad hoc strings |
| Interval | 2025-04/2026-04 | ”trailing 12 months” as prose |
| Duration | P12M | hardcoded in SQL |
| Recurrence | R12/P1M | not expressible |
ISO 4217 (Currency Codes)
Section titled “ISO 4217 (Currency Codes)”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.
The type system
Section titled “The type system”Five base types. No more, no less.
1. quantity
Section titled “1. quantity”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_zeroDuckDB 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
2. currency
Section titled “2. currency”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_zeroDuckDB 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 779The 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.
3. time
Section titled “3. time”A temporal value grounded in ISO 8601.
observation_window: type: time form: interval # point | interval | duration granularity: P1M # ISO 8601 duration (bucket size)Forms:
| Form | ISO 8601 | Example | Use case |
|---|---|---|---|
point | 2026-04-17 | Single date | Event date, delivery date |
interval | 2025-04/2026-04 | Date range | Observation window |
duration | P12M | Length of time | How 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 pointThe Ledger can render: “This number covers 2025-03 through 2026-02 (12 months ending at the latest delivery).“
4. category
Section titled “4. category”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 listDuckDB 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).
5. flag
Section titled “5. flag”Boolean — the degenerate case. Today’s binary signal.
is_duplicate: type: flagDuckDB 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.
Signal outputs
Section titled “Signal outputs”A signal declares its outputs — typed values that become real columns in the findings table.
signal_id: signal_negative_margintype: balancecontract: "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: latestEach output compiles to a column in signal_findings__signal_negative_margin:
| Column | DuckDB type | Source |
|---|---|---|
out_margin | DOUBLE | computed by signal SQL |
out_exposure | DOUBLE | computed by signal SQL |
out_exposure_currency | VARCHAR | from output declaration (CHF) |
out_exposure_ref_date | DATE | from window anchor |
out_observation_period | VARCHAR | ISO 8601 interval |
The out_ prefix distinguishes typed outputs from framework columns
(finding_id, tenant_id, severity, etc.).
Backward compatibility
Section titled “Backward compatibility”Existing framework columns remain:
| Column | Status | Derivation |
|---|---|---|
severity | kept | derived from score + thresholds (Sense 14) |
money_at_risk | kept | alias for the first currency-typed output, if any |
score | kept | 0-100 continuous (Sense 14) |
evidence | kept | JSON blob for unstructured extras |
Signals without an outputs: block work exactly as today. The typed
system is opt-in, not a breaking change.
Aggregation rules
Section titled “Aggregation rules”The type system defines what operations are valid:
| Type | SUM | AVG | MIN/MAX | COUNT | MODE |
|---|---|---|---|---|---|
quantity (same kind+unit) | yes | yes | yes | yes | no |
quantity (same kind, diff unit) | convert first | convert first | convert first | yes | no |
quantity (diff kind) | compile error | compile error | compile error | yes | no |
currency (same code) | yes | yes | yes | yes | no |
currency (diff code) | conversion step | conversion step | conversion step | yes | no |
time | no | no | yes (earliest/latest) | yes | no |
category | no | no | yes (if ordered) | yes | yes |
flag | COUNT(true) | no | no | yes | no |
Perspectives declare their aggregation strategy per output:
perspective_id: perspective_material_healthsource_signals: - signal_negative_margin - signal_price_stabilityaggregation: 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.
Compile-time validation
Section titled “Compile-time validation”The signal compiler gains dimensional analysis:
-
Unit validation — every
unitfield must be a valid UCUM code. Rejectunit: francs(not UCUM). Acceptunit: CHF(valid). -
Kind inference — the compiler infers quantity kind from the UCUM unit.
kg→ mass.CHF→ currency.%→ dimensionless. -
Aggregation check — perspectives that aggregate across signals must have compatible types. SUM of
kg+CHF→ compile error. -
Currency consistency — within a single signal, all
currencyoutputs must declare a currency code. Cross-currency signals are legal but must declare a conversion reference. -
Temporal consistency — if a signal declares a
window, itstime-typed outputs must be compatible with the window’s granularity.
What changes where
Section titled “What changes where”| Component | Change |
|---|---|
| Signal YAML | New outputs: block with typed declarations |
signalcompile.py | Generate typed columns, validate UCUM codes |
signalcheck.py | Validate output types, aggregation compatibility |
| Signal contract | Add output type schema to signal_contract.v1.json |
| Perspective compiler | Validate aggregation methods against output types |
| Thesis compiler | Validate evidence references against output types |
| JinDesk | Render typed outputs (gauge for quantity, badge for category) |
| Ledger (Sense 18) | Narrate units, ranges, conversion assumptions |
| Config engine | display.units for locale-aware unit formatting |
Currency conversion as provenance
Section titled “Currency conversion as provenance”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:
-
A
reference_datasource inpipeline.ymlprovides exchange rates:- id: exchange_rates_snb_2026source:type: reference_datapath: "reference/exchange_rates.csv"expected:required_columns: [from_currency, to_currency, date, rate, source] -
The conversion appears in the Gold layer as a join, not a magic constant.
-
The Ledger traces: “EUR 300 → CHF 279 at rate 0.93 (SNB, 2026-04-17). Rate source: reference_data/exchange_rates_snb_2026.”
-
The “What could change this number” section lists the exchange rate as an explicit assumption.
Contextual units
Section titled “Contextual units”Not all units are universal. Some depend on tenant-specific rules:
| Unit | Definition depends on… | Examples |
|---|---|---|
| FTE | Employment rules | 42 h/wk (RMC), 35 h/wk (France), 40 h/wk (Swiss academia) |
| bed-day | Clinical counting rules | ICU vs general ward, day-case included? |
| case | Administrative definition | DRG case vs clinical episode vs admin case |
| supply unit | Material master | Pack size, dispensing unit vs billing unit |
| weighted case | Weight table version | CMI 2025 vs CMI 2026, SwissDRG vs G-DRG |
| standard cost | Methodology + base year | Full 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.
Three categories of units
Section titled “Three categories of units”| Category | Definition lives in… | Conversion |
|---|---|---|
| Universal | Standard (UCUM/ISO) | Built-in (kg→g, m→cm) |
| Convertible | Reference data (exchange rates) | Pipeline step with provenance |
| Contextual | Tenant rules (unit registry) | Tenant-specific definition |
The unit registry
Section titled “The unit registry”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.ymlunits: 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_rulesThe 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.
Contextual units in the Ledger
Section titled “Contextual units in the Ledger”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.
Contextual units and aggregation
Section titled “Contextual units and aggregation”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.
Subject Matter as governance
Section titled “Subject Matter as governance”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
sourcelink 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.
Temporal windows
Section titled “Temporal windows”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) | deliveryThe 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.
Anchor strategies
Section titled “Anchor strategies”| Anchor | Meaning | Use case |
|---|---|---|
latest | End at the most recent data point | Standard trailing window |
fixed(date) | End at a specific date | Reproducible analysis |
delivery | End at the latest delivery date | Aligned with data freshness |
The score question
Section titled “The score question”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.
Migration path
Section titled “Migration path”Phase 0: Foundation (no signal changes)
Section titled “Phase 0: Foundation (no signal changes)”- 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)
Phase 1: Opt-in typed outputs
Section titled “Phase 1: Opt-in typed outputs”- Signals can declare
outputs:— compiler generates typed columns - Existing signals unchanged (implicit
flagtype) - JinDesk renders typed columns alongside existing ones
Phase 2: Perspectives use types
Section titled “Phase 2: Perspectives use types”- Perspective compiler validates aggregation methods against output types
- Cross-kind aggregation rejected at compile time
- Severity derivation from range position (Option A)
Phase 3: Currency and temporal
Section titled “Phase 3: Currency and temporal”currencytype 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
Connection to other Senses
Section titled “Connection to other Senses”- 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
quantitywith range renders as a gauge. Acurrencyrenders with locale formatting. Acategoryrenders as a badge.
Open questions
Section titled “Open questions”-
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.
-
Compound units:
CHF/{item}is a price.{items}/mois a rate. Should the compiler understand compound dimensional analysis (price × quantity = currency), or is that over-engineering for now? -
Quantity kind inference: Should the compiler infer
kindfrom the UCUM unit automatically (kg→ mass,CHF→ currency), or require explicit declaration? Inference is convenient but may surprise. -
Score derivation: Option A (derived from range position) vs Option B (independent). See “The score question” above.
-
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
currencytype now and implement conversion when needed? -
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?
-
Existing
money_at_riskcolumn: Keep as a backward-compatible alias forever, or deprecate after migration? The alias is cheap but creates ambiguity with typed outputs. -
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?
-
Unit versioning: An FTE definition can change (new employment contract, new legal minimum). Should the unit registry carry a
valid_fromdate, 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) →