@cavelang/fusion 0.27.3 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Authors.md ADDED
@@ -0,0 +1,5 @@
1
+ # Authors
2
+
3
+ By adding their name to this file, all listed authors confirm they have copyright over their contributions to this project and are waiving these rights, placing their work in the public domain in accordance with the CC0 1.0 Universal Public Domain Dedication.
4
+
5
+ - Mirek Rusin (Switzerland)
package/License.md ADDED
@@ -0,0 +1,6 @@
1
+ # CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
2
+
3
+ To the extent possible under law, the authors listed in [Authors.md](./Authors.md) have waived all copyright and related or neighboring rights to this software and associated documentation files (the "Software").
4
+
5
+ For more information, please see:
6
+ https://creativecommons.org/publicdomain/zero/1.0/
package/README.md CHANGED
@@ -26,8 +26,16 @@ For normally distributed estimates with `+/- Δ` at kσ: σ = Δ/k, precision
26
26
 
27
27
  `estimateOf(claim)` extracts `{ mean, sigma, conf }` from any numeric claim
28
28
  carrying `+/-` uncertainty (attribute or metric payload); claims without a
29
- usable estimate are skipped, zero-confidence and zero-σ estimates
30
- contribute nothing. The spec's worked example is a test case verbatim.
29
+ usable estimate and zero-confidence estimates are skipped. Invalid zero,
30
+ negative, or non-finite σ values throw the same typed core uncertainty error
31
+ used by parsed and constructed claims. The spec's worked example is a test
32
+ case verbatim.
33
+
34
+ Fusion preserves units at the library boundary. Missing units combine only
35
+ with missing units, arbitrary units combine only by exact equality, and the
36
+ fixed-duration units `ms`, `s`, `min`, and `h` convert to the first estimate's
37
+ unit before weighting. Every other mixture throws a typed `FusionUnitError`;
38
+ adapters surface that same failure instead of maintaining separate checks.
31
39
 
32
40
  ## Conditional confidence (§10.2)
33
41
 
@@ -17,6 +17,8 @@ import { Claim } from '@cavelang/core';
17
17
  export type Estimate = {
18
18
  readonly mean: number;
19
19
  readonly sigma: number;
20
+ /** Unit shared by the mean and σ. Missing is a distinct dimension. */
21
+ readonly unit?: string;
20
22
  /** Epistemic confidence used as a precision multiplier (default 1). */
21
23
  readonly conf?: number;
22
24
  };
@@ -25,7 +27,14 @@ export type Posterior = {
25
27
  readonly sigma: number;
26
28
  /** Total weighted precision Σ wᵢ. */
27
29
  readonly precision: number;
30
+ /** Unit in which the posterior mean and σ are expressed. */
31
+ readonly unit?: string;
28
32
  };
33
+ /** Typed boundary failure for estimates that do not share a compatible unit. */
34
+ export declare class FusionUnitError extends Error {
35
+ readonly units: readonly (string | undefined)[];
36
+ constructor(units: readonly (string | undefined)[]);
37
+ }
29
38
  /**
30
39
  * Fuses estimates into a posterior. Estimates with `conf` 0 or a
31
40
  * non-positive σ contribute nothing and are skipped.
@@ -1 +1 @@
1
- {"version":3,"file":"fuse.d.ts","sourceRoot":"","sources":["../../src/fuse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAEtC,uEAAuE;AACvE,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,SAAS,QAAQ,EAAE,KAAG,SAAS,GAAG,SAoBjE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,CAAC,CAAC,KAAG,SAAS,GAAG,QAcvD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,QAAQ,SAAS,KAAK,CAAC,CAAC,EAAE,KAAG,SAAS,GAAG,SAI/D,CAAA"}
1
+ {"version":3,"file":"fuse.d.ts","sourceRoot":"","sources":["../../src/fuse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,KAAK,EAAe,MAAM,gBAAgB,CAAA;AAEnD,uEAAuE;AACvE,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAiBD,gFAAgF;AAChF,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAA;gBAEnC,KAAK,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE;CAOnD;AAED;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,WAAW,SAAS,QAAQ,EAAE,KAAG,SAAS,GAAG,SAyBjE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,CAAC,CAAC,KAAG,SAAS,GAAG,QAuBvD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,QAAQ,SAAS,KAAK,CAAC,CAAC,EAAE,KAAG,SAAS,GAAG,SAI/D,CAAA"}
package/dist/src/fuse.js CHANGED
@@ -12,31 +12,63 @@
12
12
  * The math is an implementation layer, not required syntax (spec §10) —
13
13
  * CAVE itself only stores claims and metadata.
14
14
  */
15
- import { Claim } from '@cavelang/core';
15
+ import { Claim, Uncertainty } from '@cavelang/core';
16
+ const durationScale = {
17
+ ms: 0.001,
18
+ s: 1,
19
+ min: 60,
20
+ h: 3_600
21
+ };
22
+ const conversionFactor = (from, to) => {
23
+ if (from === to)
24
+ return 1;
25
+ if (from === undefined || to === undefined)
26
+ return undefined;
27
+ const fromScale = durationScale[from];
28
+ const toScale = durationScale[to];
29
+ return fromScale === undefined || toScale === undefined ? undefined : fromScale / toScale;
30
+ };
31
+ /** Typed boundary failure for estimates that do not share a compatible unit. */
32
+ export class FusionUnitError extends Error {
33
+ units;
34
+ constructor(units) {
35
+ const unique = [...new Set(units)].sort((a, b) => (a ?? '').localeCompare(b ?? ''));
36
+ super('cannot fuse mixed units: ' +
37
+ `${unique.map(unit => unit ?? '(none)').join(', ')} — convert the estimates to one unit first`);
38
+ this.name = 'FusionUnitError';
39
+ this.units = unique;
40
+ }
41
+ }
16
42
  /**
17
43
  * Fuses estimates into a posterior. Estimates with `conf` 0 or a
18
44
  * non-positive σ contribute nothing and are skipped.
19
45
  * @returns `undefined` when no estimate carries usable weight.
20
46
  */
21
47
  export const fuse = (estimates) => {
48
+ estimates.forEach(estimate => Uncertainty.validateSigma(estimate.sigma));
49
+ const usable = estimates.filter(estimate => (estimate.conf ?? 1) > 0);
50
+ if (usable.length === 0)
51
+ return undefined;
52
+ const unit = usable[0].unit;
53
+ const factors = usable.map(estimate => conversionFactor(estimate.unit, unit));
54
+ if (factors.some(factor => factor === undefined)) {
55
+ throw new FusionUnitError(usable.map(estimate => estimate.unit));
56
+ }
22
57
  let totalWeight = 0;
23
58
  let weightedSum = 0;
24
- for (const estimate of estimates) {
59
+ for (const [index, estimate] of usable.entries()) {
25
60
  const conf = estimate.conf ?? 1;
26
- if (!(estimate.sigma > 0) || !(conf > 0)) {
27
- continue;
28
- }
29
- const weight = conf / (estimate.sigma * estimate.sigma);
61
+ const factor = factors[index];
62
+ const sigma = estimate.sigma * factor;
63
+ const weight = conf / (sigma * sigma);
30
64
  totalWeight += weight;
31
- weightedSum += weight * estimate.mean;
32
- }
33
- if (totalWeight === 0) {
34
- return undefined;
65
+ weightedSum += weight * estimate.mean * factor;
35
66
  }
36
67
  return {
37
68
  mean: weightedSum / totalWeight,
38
69
  sigma: 1 / Math.sqrt(totalWeight),
39
- precision: totalWeight
70
+ precision: totalWeight,
71
+ ...unit === undefined ? {} : { unit }
40
72
  };
41
73
  };
42
74
  /**
@@ -56,7 +88,16 @@ export const estimateOf = (claim) => {
56
88
  if (sigma === undefined || !(sigma > 0)) {
57
89
  return undefined;
58
90
  }
59
- return { mean: value.num, sigma, conf: claim.conf };
91
+ const factor = conversionFactor(claim.delta?.unit, value.unit);
92
+ if (factor === undefined) {
93
+ throw new FusionUnitError([value.unit, claim.delta?.unit]);
94
+ }
95
+ return {
96
+ mean: value.num,
97
+ sigma: sigma * factor,
98
+ ...value.unit === undefined ? {} : { unit: value.unit },
99
+ conf: claim.conf
100
+ };
60
101
  };
61
102
  /**
62
103
  * Fuses the numeric estimates found in `claims` (spec §10.1's worked
@@ -1 +1 @@
1
- {"version":3,"file":"fuse.js","sourceRoot":"","sources":["../../src/fuse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAiBtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,SAA8B,EAAyB,EAAE;IAC5E,IAAI,WAAW,GAAG,CAAC,CAAA;IACnB,IAAI,WAAW,GAAG,CAAC,CAAA;IACnB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAA;QAC/B,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC;YACzC,SAAQ;QACV,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvD,WAAW,IAAI,MAAM,CAAA;QACrB,WAAW,IAAI,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAA;IACvC,CAAC;IACD,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO;QACL,IAAI,EAAE,WAAW,GAAG,WAAW;QAC/B,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;QACjC,SAAS,EAAE,WAAW;KACvB,CAAA;AACH,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAc,EAAwB,EAAE;IACjE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAC7B,MAAM,KAAK,GACT,OAAO,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC3C,SAAS,CAAA;IACX,IAAI,KAAK,EAAE,GAAG,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IAClC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;QACxC,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAA;AACrD,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAA0B,EAAyB,EAAE,CAC9E,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;IAC1B,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;IAClC,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;AACjD,CAAC,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"fuse.js","sourceRoot":"","sources":["../../src/fuse.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@
6
6
  * independence assumption (§10.2), and competing-hypothesis helpers
7
7
  * (§10.3). No storage, no I/O.
8
8
  */
9
- export { fuse, fuseClaims, estimateOf } from './fuse.ts';
9
+ export { FusionUnitError, fuse, fuseClaims, estimateOf } from './fuse.ts';
10
10
  export type { Estimate, Posterior } from './fuse.ts';
11
11
  export { noisyAndIndependent, normalizeHypotheses, hypothesisGap } from './conditional.ts';
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACxD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACpD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACzE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACpD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
package/dist/src/index.js CHANGED
@@ -6,6 +6,6 @@
6
6
  * independence assumption (§10.2), and competing-hypothesis helpers
7
7
  * (§10.3). No storage, no I/O.
8
8
  */
9
- export { fuse, fuseClaims, estimateOf } from "./fuse.js";
9
+ export { FusionUnitError, fuse, fuseClaims, estimateOf } from "./fuse.js";
10
10
  export { noisyAndIndependent, normalizeHypotheses, hypothesisGap } from "./conditional.js";
11
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAExD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAEzE,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
package/package.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "name": "@cavelang/fusion",
3
- "version": "0.27.3",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "description": "CAVE probabilistic layer — Bayesian fusion of numeric estimates, noisy-AND conditional confidence, hypothesis helpers.",
6
+ "keywords": [
7
+ "cave",
8
+ "knowledge-graph",
9
+ "knowledge-representation",
10
+ "typescript"
11
+ ],
12
+ "homepage": "https://github.com/mirek/cave#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/mirek/cave/issues"
15
+ },
6
16
  "license": "CC0-1.0",
7
17
  "author": "Mirek Rusin (https://github.com/mirek)",
8
18
  "repository": {
@@ -26,17 +36,18 @@
26
36
  "src",
27
37
  "!src/**/*.test.ts",
28
38
  "README.md",
29
- "License.md"
39
+ "License.md",
40
+ "Authors.md"
30
41
  ],
31
42
  "publishConfig": {
32
43
  "access": "public"
33
44
  },
34
45
  "dependencies": {
35
- "@cavelang/core": "0.27.3"
46
+ "@cavelang/core": "0.29.0"
36
47
  },
37
48
  "scripts": {
38
49
  "build": "tsc -b",
39
- "test": "node --disable-warning=ExperimentalWarning --test 'test/*.test.ts'",
50
+ "test": "node --disable-warning=ExperimentalWarning --test \"test/*.test.ts\"",
40
51
  "typecheck": "tsc -b"
41
52
  }
42
53
  }
package/src/fuse.ts CHANGED
@@ -13,12 +13,14 @@
13
13
  * CAVE itself only stores claims and metadata.
14
14
  */
15
15
 
16
- import { Claim } from '@cavelang/core'
16
+ import { Claim, Uncertainty } from '@cavelang/core'
17
17
 
18
18
  /** One numeric estimate: mean, σ, and confidence weight p ∈ (0, 1]. */
19
19
  export type Estimate = {
20
20
  readonly mean: number
21
21
  readonly sigma: number
22
+ /** Unit shared by the mean and σ. Missing is a distinct dimension. */
23
+ readonly unit?: string
22
24
  /** Epistemic confidence used as a precision multiplier (default 1). */
23
25
  readonly conf?: number
24
26
  }
@@ -28,6 +30,36 @@ export type Posterior = {
28
30
  readonly sigma: number
29
31
  /** Total weighted precision Σ wᵢ. */
30
32
  readonly precision: number
33
+ /** Unit in which the posterior mean and σ are expressed. */
34
+ readonly unit?: string
35
+ }
36
+
37
+ const durationScale: Readonly<Record<string, number>> = {
38
+ ms: 0.001,
39
+ s: 1,
40
+ min: 60,
41
+ h: 3_600
42
+ }
43
+
44
+ const conversionFactor = (from: undefined | string, to: undefined | string): undefined | number => {
45
+ if (from === to) return 1
46
+ if (from === undefined || to === undefined) return undefined
47
+ const fromScale = durationScale[from]
48
+ const toScale = durationScale[to]
49
+ return fromScale === undefined || toScale === undefined ? undefined : fromScale / toScale
50
+ }
51
+
52
+ /** Typed boundary failure for estimates that do not share a compatible unit. */
53
+ export class FusionUnitError extends Error {
54
+ readonly units: readonly (string | undefined)[]
55
+
56
+ constructor(units: readonly (string | undefined)[]) {
57
+ const unique = [...new Set(units)].sort((a, b) => (a ?? '').localeCompare(b ?? ''))
58
+ super('cannot fuse mixed units: ' +
59
+ `${unique.map(unit => unit ?? '(none)').join(', ')} — convert the estimates to one unit first`)
60
+ this.name = 'FusionUnitError'
61
+ this.units = unique
62
+ }
31
63
  }
32
64
 
33
65
  /**
@@ -36,24 +68,29 @@ export type Posterior = {
36
68
  * @returns `undefined` when no estimate carries usable weight.
37
69
  */
38
70
  export const fuse = (estimates: readonly Estimate[]): undefined | Posterior => {
71
+ estimates.forEach(estimate => Uncertainty.validateSigma(estimate.sigma))
72
+ const usable = estimates.filter(estimate => (estimate.conf ?? 1) > 0)
73
+ if (usable.length === 0) return undefined
74
+ const unit = usable[0]!.unit
75
+ const factors = usable.map(estimate => conversionFactor(estimate.unit, unit))
76
+ if (factors.some(factor => factor === undefined)) {
77
+ throw new FusionUnitError(usable.map(estimate => estimate.unit))
78
+ }
39
79
  let totalWeight = 0
40
80
  let weightedSum = 0
41
- for (const estimate of estimates) {
81
+ for (const [index, estimate] of usable.entries()) {
42
82
  const conf = estimate.conf ?? 1
43
- if (!(estimate.sigma > 0) || !(conf > 0)) {
44
- continue
45
- }
46
- const weight = conf / (estimate.sigma * estimate.sigma)
83
+ const factor = factors[index]!
84
+ const sigma = estimate.sigma * factor
85
+ const weight = conf / (sigma * sigma)
47
86
  totalWeight += weight
48
- weightedSum += weight * estimate.mean
49
- }
50
- if (totalWeight === 0) {
51
- return undefined
87
+ weightedSum += weight * estimate.mean * factor
52
88
  }
53
89
  return {
54
90
  mean: weightedSum / totalWeight,
55
91
  sigma: 1 / Math.sqrt(totalWeight),
56
- precision: totalWeight
92
+ precision: totalWeight,
93
+ ...unit === undefined ? {} : { unit }
57
94
  }
58
95
  }
59
96
 
@@ -75,7 +112,16 @@ export const estimateOf = (claim: Claim.t): undefined | Estimate => {
75
112
  if (sigma === undefined || !(sigma > 0)) {
76
113
  return undefined
77
114
  }
78
- return { mean: value.num, sigma, conf: claim.conf }
115
+ const factor = conversionFactor(claim.delta?.unit, value.unit)
116
+ if (factor === undefined) {
117
+ throw new FusionUnitError([value.unit, claim.delta?.unit])
118
+ }
119
+ return {
120
+ mean: value.num,
121
+ sigma: sigma * factor,
122
+ ...value.unit === undefined ? {} : { unit: value.unit },
123
+ conf: claim.conf
124
+ }
79
125
  }
80
126
 
81
127
  /**
package/src/index.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  * (§10.3). No storage, no I/O.
8
8
  */
9
9
 
10
- export { fuse, fuseClaims, estimateOf } from './fuse.ts'
10
+ export { FusionUnitError, fuse, fuseClaims, estimateOf } from './fuse.ts'
11
11
  export type { Estimate, Posterior } from './fuse.ts'
12
12
  export { noisyAndIndependent, normalizeHypotheses, hypothesisGap } from './conditional.ts'