@higher.archi/boe 1.0.3 → 1.0.5

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.
Files changed (59) hide show
  1. package/dist/engines/scoring/strategy.d.ts.map +1 -1
  2. package/dist/engines/scoring/strategy.js +51 -0
  3. package/dist/engines/scoring/strategy.js.map +1 -1
  4. package/dist/engines/scoring/types.d.ts +1 -0
  5. package/dist/engines/scoring/types.d.ts.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/qfacts/collapse.d.ts +6 -0
  11. package/dist/qfacts/collapse.d.ts.map +1 -0
  12. package/dist/qfacts/collapse.js +95 -0
  13. package/dist/qfacts/collapse.js.map +1 -0
  14. package/dist/qfacts/index.d.ts +5 -0
  15. package/dist/qfacts/index.d.ts.map +1 -0
  16. package/dist/qfacts/index.js +20 -0
  17. package/dist/qfacts/index.js.map +1 -0
  18. package/dist/qfacts/simulate.d.ts +7 -0
  19. package/dist/qfacts/simulate.d.ts.map +1 -0
  20. package/dist/qfacts/simulate.js +19 -0
  21. package/dist/qfacts/simulate.js.map +1 -0
  22. package/dist/qfacts/strategies/crypto.d.ts +2 -0
  23. package/dist/qfacts/strategies/crypto.d.ts.map +1 -0
  24. package/dist/qfacts/strategies/crypto.js +21 -0
  25. package/dist/qfacts/strategies/crypto.js.map +1 -0
  26. package/dist/qfacts/strategies/deterministic.d.ts +2 -0
  27. package/dist/qfacts/strategies/deterministic.d.ts.map +1 -0
  28. package/dist/qfacts/strategies/deterministic.js +15 -0
  29. package/dist/qfacts/strategies/deterministic.js.map +1 -0
  30. package/dist/qfacts/strategies/index.d.ts +8 -0
  31. package/dist/qfacts/strategies/index.d.ts.map +1 -0
  32. package/dist/qfacts/strategies/index.js +29 -0
  33. package/dist/qfacts/strategies/index.js.map +1 -0
  34. package/dist/qfacts/strategies/quasi-random.d.ts +2 -0
  35. package/dist/qfacts/strategies/quasi-random.d.ts.map +1 -0
  36. package/dist/qfacts/strategies/quasi-random.js +26 -0
  37. package/dist/qfacts/strategies/quasi-random.js.map +1 -0
  38. package/dist/qfacts/strategies/seeded.d.ts +2 -0
  39. package/dist/qfacts/strategies/seeded.d.ts.map +1 -0
  40. package/dist/qfacts/strategies/seeded.js +34 -0
  41. package/dist/qfacts/strategies/seeded.js.map +1 -0
  42. package/dist/qfacts/types.d.ts +36 -0
  43. package/dist/qfacts/types.d.ts.map +1 -0
  44. package/dist/qfacts/types.js +3 -0
  45. package/dist/qfacts/types.js.map +1 -0
  46. package/package.json +1 -1
  47. package/src/engines/scoring/strategy.ts +54 -0
  48. package/src/engines/scoring/types.ts +1 -0
  49. package/src/index.ts +6 -0
  50. package/src/qfacts/README.md +243 -0
  51. package/src/qfacts/collapse.ts +111 -0
  52. package/src/qfacts/index.ts +27 -0
  53. package/src/qfacts/simulate.ts +25 -0
  54. package/src/qfacts/strategies/crypto.ts +20 -0
  55. package/src/qfacts/strategies/deterministic.ts +11 -0
  56. package/src/qfacts/strategies/index.ts +25 -0
  57. package/src/qfacts/strategies/quasi-random.ts +23 -0
  58. package/src/qfacts/strategies/seeded.ts +32 -0
  59. package/src/qfacts/types.ts +44 -0
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.seededCollapse = seededCollapse;
4
+ function createRng(seed) {
5
+ let state = seed;
6
+ return () => {
7
+ state |= 0;
8
+ state = (state + 0x6d2b79f5) | 0;
9
+ let t = Math.imul(state ^ (state >>> 15), 1 | state);
10
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
11
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
12
+ };
13
+ }
14
+ function djb2Hash(str) {
15
+ let hash = 5381;
16
+ for (let i = 0; i < str.length; i++) {
17
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
18
+ }
19
+ return hash >>> 0;
20
+ }
21
+ function seededCollapse(amplitudes, seed, fieldName) {
22
+ const fieldSeed = (seed ^ djb2Hash(fieldName)) >>> 0;
23
+ const rng = createRng(fieldSeed);
24
+ const roll = rng();
25
+ const keys = Object.keys(amplitudes);
26
+ let cumulative = 0;
27
+ for (const key of keys) {
28
+ cumulative += amplitudes[key];
29
+ if (roll < cumulative)
30
+ return key;
31
+ }
32
+ return keys[keys.length - 1];
33
+ }
34
+ //# sourceMappingURL=seeded.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seeded.js","sourceRoot":"","sources":["../../../src/qfacts/strategies/seeded.ts"],"names":[],"mappings":";;AAmBA,wCAYC;AA/BD,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,KAAK,GAAG,IAAI,CAAC;IACjB,OAAO,GAAG,EAAE;QACV,KAAK,IAAI,CAAC,CAAC;QACX,KAAK,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC;QACrD,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,UAAU,CAAC;IAC/C,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW;IAC3B,IAAI,IAAI,GAAG,IAAI,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC;AAED,SAAgB,cAAc,CAAC,UAAkC,EAAE,IAAY,EAAE,SAAiB;IAChG,MAAM,SAAS,GAAG,CAAC,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC;IAEnB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACrC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,UAAU,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,IAAI,GAAG,UAAU;YAAE,OAAO,GAAG,CAAC;IACpC,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { FactInput } from '../core';
2
+ export type AmplitudeValue = number | `${number}%`;
3
+ export type Superposition = {
4
+ state: 'superposition';
5
+ amplitudes: Record<string, number>;
6
+ };
7
+ export type QValue = any | Superposition;
8
+ export type QFactInput = {
9
+ id?: string;
10
+ type: string;
11
+ data: Record<string, QValue>;
12
+ };
13
+ export type CollapseStrategy = 'seeded' | 'crypto' | 'deterministic' | 'quasi-random';
14
+ export type CollapseOptions = {
15
+ strategy?: CollapseStrategy;
16
+ seed?: number;
17
+ coerce?: boolean;
18
+ };
19
+ export type CollapseRecord = {
20
+ field: string;
21
+ selectedValue: any;
22
+ amplitudes: Record<string, number>;
23
+ strategy: CollapseStrategy;
24
+ seed?: number;
25
+ };
26
+ export type CollapsedFact = {
27
+ fact: FactInput;
28
+ collapseLog: CollapseRecord[];
29
+ };
30
+ export type SimulationResult<T = any> = {
31
+ runs: number;
32
+ results: T[];
33
+ collapseHistory: CollapseRecord[][];
34
+ strategy: CollapseStrategy;
35
+ };
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/qfacts/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC;AAEnD,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,eAAe,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG,GAAG,GAAG,aAAa,CAAC;AAEzC,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,GAAG,eAAe,GAAG,cAAc,CAAC;AAEtF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,GAAG,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,WAAW,EAAE,cAAc,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,GAAG,IAAI;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,eAAe,EAAE,cAAc,EAAE,EAAE,CAAC;IACpC,QAAQ,EAAE,gBAAgB,CAAC;CAC5B,CAAC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/qfacts/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higher.archi/boe",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A multi-strategy rule engine supporting forward chaining, backward chaining, scoring, sequential, fuzzy logic, and Bayesian inference",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -135,6 +135,58 @@ export class ScoringStrategy {
135
135
  }
136
136
  }
137
137
 
138
+ // Phase 2.75: Compute confidence score (signal coverage ratio)
139
+ let confidence = 0;
140
+ const allRules = ruleSet.rules;
141
+ if (allRules.length === 0) {
142
+ confidence = 1;
143
+ } else if (config.categories?.length) {
144
+ // Category-aware confidence: per-category weighted confidence combined by category weights
145
+ const categories = config.categories;
146
+ let totalCatWeight = 0;
147
+ let weightedConfidenceSum = 0;
148
+
149
+ for (const cat of categories) {
150
+ const catRules = allRules.filter(r => r.category === cat.id);
151
+ if (catRules.length === 0) continue;
152
+
153
+ let allSignalWeight = 0;
154
+ let firedSignalWeight = 0;
155
+ for (const rule of catRules) {
156
+ const w = (rule.action as CompiledScoringAction).weight ?? 1;
157
+ allSignalWeight += w;
158
+ if (fired.includes(rule.id)) {
159
+ firedSignalWeight += w;
160
+ }
161
+ }
162
+
163
+ const totalSignals = catRules.length;
164
+ const firedCount = catRules.filter(r => fired.includes(r.id)).length;
165
+ const minRatio = cat.minSignalRatio ?? 0.5;
166
+ const excluded = totalSignals > 0 && (firedCount / totalSignals) < minRatio;
167
+
168
+ if (!excluded) {
169
+ const catConfidence = allSignalWeight > 0 ? firedSignalWeight / allSignalWeight : 0;
170
+ weightedConfidenceSum += cat.weight * catConfidence;
171
+ totalCatWeight += cat.weight;
172
+ }
173
+ }
174
+
175
+ confidence = totalCatWeight > 0 ? weightedConfidenceSum / totalCatWeight : 0;
176
+ } else {
177
+ // Global weighted confidence: sum(fired weights) / sum(all weights)
178
+ let allWeight = 0;
179
+ let firedWeight = 0;
180
+ for (const rule of allRules) {
181
+ const w = (rule.action as CompiledScoringAction).weight ?? 1;
182
+ allWeight += w;
183
+ if (fired.includes(rule.id)) {
184
+ firedWeight += w;
185
+ }
186
+ }
187
+ confidence = allWeight > 0 ? firedWeight / allWeight : 0;
188
+ }
189
+
138
190
  // Phase 3: Apply global strategies
139
191
  let categoryBreakdown: CategoryResult[] | undefined;
140
192
 
@@ -366,6 +418,7 @@ export class ScoringStrategy {
366
418
  const executionTimeMs = Math.round((performance.now() - startTime) * 100) / 100;
367
419
  return {
368
420
  totalScore: 0,
421
+ confidence,
369
422
  contributions: {},
370
423
  fired: [],
371
424
  iterations: 1,
@@ -416,6 +469,7 @@ export class ScoringStrategy {
416
469
 
417
470
  return {
418
471
  totalScore,
472
+ confidence,
419
473
  contributions,
420
474
  fired,
421
475
  iterations: 1,
@@ -376,6 +376,7 @@ export type ScoringOptions = {
376
376
  */
377
377
  export type ScoringResult = {
378
378
  totalScore: number;
379
+ confidence: number; // 0-1, signal coverage ratio (weighted if weights present)
379
380
  contributions: Record<string, number>; // Which rules contributed what scores
380
381
  fired: string[]; // Rules that matched and contributed
381
382
  iterations: 1; // Scoring is always one-pass
package/src/index.ts CHANGED
@@ -374,3 +374,9 @@ export type {
374
374
  // ========================================
375
375
 
376
376
  export { soundex, nysiis, caverphone2, cosineSimilarity } from './functions';
377
+
378
+ // ========================================
379
+ // QFacts - Probabilistic Fact Hydration
380
+ // ========================================
381
+
382
+ export * from './qfacts';
@@ -0,0 +1,243 @@
1
+ # QFacts - Probabilistic Fact Hydration
2
+
3
+ QFacts is a pre-processing layer that handles **uncertain data** before it reaches any BOE engine. It accepts facts where some fields have multiple possible values (probability distributions), picks a concrete value for each one, and hands clean `FactInput` objects to the engine. BOE stays deterministic and auditable. QFacts extends the audit trail upstream.
4
+
5
+ ## What Problem Does This Solve?
6
+
7
+ The real world doesn't arrive in clean rows. An NLP model says "this is 80% positive, 15% neutral, 5% negative." A sensor reads a temperature but with a margin of error. A classifier returns probabilities across several categories. Right now, something upstream has to commit to a single value before BOE ever sees it — and that throws away the uncertainty silently.
8
+
9
+ QFacts keeps the uncertainty visible. You describe the distribution, QFacts collapses it to a single value, and the collapse is recorded — what the options were, what was picked, and how.
10
+
11
+ ```
12
+ Uncertain World → QFacts (collapse) → FactInput → BOE Engine → Result
13
+ ```
14
+
15
+ ## When to Use QFacts
16
+
17
+ Use QFacts when your input data has uncertainty that you want to preserve, collapse deliberately, or simulate across:
18
+
19
+ - **ML/NLP output** - Classifiers return probability distributions, not hard labels
20
+ - **Sensor data** - Readings have confidence intervals or error margins
21
+ - **Survey data** - Responses mapped to weighted categories
22
+ - **Monte Carlo analysis** - Run thousands of collapses to see how uncertainty affects outcomes
23
+ - **Reproducible pipelines** - Same seed, same collapse, every time
24
+
25
+ If your facts are already concrete values, you don't need QFacts. Just use `FactInput` directly.
26
+
27
+ ## Core Concepts
28
+
29
+ ### Superposition
30
+
31
+ A superposition is a field that hasn't been decided yet. It holds multiple possible values, each with a weight (amplitude) representing how likely that value is.
32
+
33
+ Think of it like a coin that hasn't been flipped. It's not heads or tails — it's both, with weights. A fair coin would be `{ heads: 0.5, tails: 0.5 }`. A loaded coin might be `{ heads: 0.8, tails: 0.2 }`.
34
+
35
+ You create a superposition using the `superposition()` factory:
36
+
37
+ ```typescript
38
+ superposition({ high: 0.8, moderate: 0.15, low: 0.05 })
39
+ ```
40
+
41
+ This returns an object with `state: 'superposition'` and normalized amplitudes that always sum to 1.0.
42
+
43
+ ### Amplitude Formats
44
+
45
+ The `superposition()` factory is flexible about how you express weights. All three of these mean exactly the same thing:
46
+
47
+ ```typescript
48
+ superposition({ high: 0.9, moderate: 0.1 }) // decimals
49
+ superposition({ high: 90, moderate: 10 }) // whole numbers
50
+ superposition({ high: '90%', moderate: '10%' }) // percentage strings
51
+ ```
52
+
53
+ You can even mix formats:
54
+
55
+ ```typescript
56
+ superposition({ high: '90%', moderate: 10 }) // mixed — fine
57
+ ```
58
+
59
+ Values don't need to sum to 100 or 1.0. QFacts normalizes everything automatically:
60
+
61
+ ```typescript
62
+ superposition({ high: 120, moderate: 80 })
63
+ // total = 200 → stored as { high: 0.6, moderate: 0.4 }
64
+ ```
65
+
66
+ Internally, amplitudes are always stored as numbers between 0 and 1 that sum to 1.0. The flexible input is only accepted by the factory — the `Superposition` type itself is always normalized.
67
+
68
+ ### QFactInput
69
+
70
+ A `QFactInput` looks just like a `FactInput`, but any field in `data` can be a superposition instead of a concrete value:
71
+
72
+ ```typescript
73
+ const qfact: QFactInput = {
74
+ type: 'SentimentAnalysis',
75
+ data: {
76
+ text: 'Great product, minor issues', // concrete
77
+ sentiment: superposition({ positive: 0.8, mixed: 0.15, negative: 0.05 }), // uncertain
78
+ confidence: 0.92 // concrete
79
+ }
80
+ };
81
+ ```
82
+
83
+ Fields that are already concrete pass through untouched. Only superpositions get collapsed.
84
+
85
+ ### Collapse
86
+
87
+ Collapsing is the act of picking one concrete value from a superposition. The `collapse()` function walks through every field in a `QFactInput`, collapses any superpositions it finds, and returns a clean `FactInput` plus a log of what happened.
88
+
89
+ The collapse log records each decision: which field, what the options were, what was picked, which strategy was used, and (for seeded strategies) what seed was used. This is your audit trail.
90
+
91
+ ### Strategies
92
+
93
+ QFacts supports four collapse strategies. Each one answers the same question — "given these weighted options, pick one" — but they differ in how they pick.
94
+
95
+ | Strategy | Randomness | Best For |
96
+ |----------|-----------|----------|
97
+ | **seeded** | Pseudo-random (reproducible) | Default. Same seed always gives same result. |
98
+ | **deterministic** | None | Always picks the highest-weighted option. No surprises. |
99
+ | **crypto** | True random | When you need cryptographic randomness. Not reproducible. |
100
+ | **quasi-random** | Low-discrepancy sequence | Monte Carlo simulations. Better coverage with fewer runs. |
101
+
102
+ #### Seeded (default)
103
+
104
+ Uses a fast pseudo-random number generator (Mulberry32). Given the same seed, it always produces the same result. This is the default because it follows the principle of least surprise — calling `collapse(qfact)` with no options always returns the same output.
105
+
106
+ Each field gets its own derived seed based on the field name, so the collapse of one field doesn't affect another. Reordering fields won't change results.
107
+
108
+ #### Deterministic
109
+
110
+ Always picks the option with the highest amplitude. On ties, picks the first one in insertion order. No randomness at all. Useful when you just want the most likely value and don't care about simulating alternatives.
111
+
112
+ #### Crypto
113
+
114
+ Uses `crypto.getRandomValues()` for true randomness. Not reproducible — every call gives a different result. Throws a descriptive error if crypto isn't available (Node < 19 without the global).
115
+
116
+ #### Quasi-Random
117
+
118
+ Uses a Halton sequence instead of pseudo-random numbers. Halton sequences are "low-discrepancy" — they spread out more evenly than random numbers. This means Monte Carlo simulations converge faster. You get better coverage of the probability space with fewer runs.
119
+
120
+ Takes an `index` parameter (typically the simulation loop counter) to determine position in the sequence.
121
+
122
+ ## API
123
+
124
+ ### `superposition(amplitudes)`
125
+
126
+ Creates a superposition from weighted options. Accepts numbers, percentages, or percentage strings. Always normalizes to 0-1 internally.
127
+
128
+ ### `isSuperposition(value)`
129
+
130
+ Type guard. Returns `true` if the value is a `Superposition` object (has `state: 'superposition'` and an `amplitudes` record).
131
+
132
+ ### `collapse(qfact, options?)`
133
+
134
+ Collapses a single `QFactInput` into a `CollapsedFact`, which contains a clean `FactInput` and a `collapseLog` array.
135
+
136
+ **Options:**
137
+
138
+ | Option | Type | Default | Description |
139
+ |--------|------|---------|-------------|
140
+ | `strategy` | `CollapseStrategy` | `'seeded'` | Which collapse strategy to use |
141
+ | `seed` | `number` | `0` | Base seed for the `'seeded'` strategy |
142
+ | `coerce` | `boolean` | `false` | Parse string keys to numbers/booleans via `JSON.parse` |
143
+
144
+ When `coerce` is `true`, QFacts attempts to parse the selected key back to its original type. Since amplitude keys are always strings (`Record<string, number>`), a superposition like `{ "100": 0.7, "200": 0.3 }` would normally collapse to the string `"100"`. With `coerce: true`, it becomes the number `100`.
145
+
146
+ ### `collapseAll(qfacts, options?)`
147
+
148
+ Collapses an array of `QFactInput` objects. Each fact gets its own derived seed (base seed + index) so collapses are independent of each other and of array order.
149
+
150
+ ### `simulate(qfacts, engineFn, options?)`
151
+
152
+ Runs a Monte Carlo simulation. Collapses the same set of `QFactInput` objects many times with different seeds, passes each set of collapsed facts through your engine function, and collects the results.
153
+
154
+ **Options:**
155
+
156
+ | Option | Type | Default | Description |
157
+ |--------|------|---------|-------------|
158
+ | `runs` | `number` | `1000` | Number of simulation runs |
159
+ | `strategy` | `CollapseStrategy` | `'seeded'` | Strategy for each run |
160
+
161
+ Returns a `SimulationResult` with the array of engine results, the full collapse history for every run, and metadata.
162
+
163
+ ## Types
164
+
165
+ ### `Superposition`
166
+
167
+ ```typescript
168
+ type Superposition = {
169
+ state: 'superposition';
170
+ amplitudes: Record<string, number>; // normalized 0-1, sums to 1.0
171
+ };
172
+ ```
173
+
174
+ ### `QFactInput`
175
+
176
+ ```typescript
177
+ type QFactInput = {
178
+ id?: string;
179
+ type: string;
180
+ data: Record<string, QValue>; // QValue = any concrete value OR a Superposition
181
+ };
182
+ ```
183
+
184
+ ### `CollapseRecord`
185
+
186
+ ```typescript
187
+ type CollapseRecord = {
188
+ field: string; // which field was collapsed
189
+ selectedValue: any; // what it collapsed to
190
+ amplitudes: Record<string, number>; // what the options were
191
+ strategy: CollapseStrategy; // how it was picked
192
+ seed?: number; // seed used (seeded strategy only)
193
+ };
194
+ ```
195
+
196
+ ### `CollapsedFact`
197
+
198
+ ```typescript
199
+ type CollapsedFact = {
200
+ fact: FactInput; // clean, ready for any BOE engine
201
+ collapseLog: CollapseRecord[];
202
+ };
203
+ ```
204
+
205
+ ### `SimulationResult<T>`
206
+
207
+ ```typescript
208
+ type SimulationResult<T> = {
209
+ runs: number;
210
+ results: T[]; // one engine result per run
211
+ collapseHistory: CollapseRecord[][]; // one collapse log per run
212
+ strategy: CollapseStrategy;
213
+ };
214
+ ```
215
+
216
+ ## Structure
217
+
218
+ ```
219
+ src/qfacts/
220
+ ├── types.ts # All type definitions
221
+ ├── collapse.ts # Core collapse functions + superposition factory
222
+ ├── simulate.ts # Monte Carlo simulation wrapper
223
+ ├── strategies/
224
+ │ ├── seeded.ts # Mulberry32 PRNG collapse
225
+ │ ├── deterministic.ts # Argmax collapse (no randomness)
226
+ │ ├── crypto.ts # crypto.getRandomValues collapse
227
+ │ ├── quasi-random.ts # Halton sequence collapse
228
+ │ └── index.ts # Strategy barrel + dispatcher
229
+ └── index.ts # Module barrel exports
230
+ ```
231
+
232
+ ## Design Decisions
233
+
234
+ | Decision | Choice | Why |
235
+ |----------|--------|-----|
236
+ | BOE stays untouched | QFacts is pre-processing only | BOE remains deterministic and auditable. QFacts extends the audit trail upstream, not inside. |
237
+ | Default seed is `0`, not random | Reproducibility by default | First call always gives the same result. No surprises. Opt into randomness explicitly. |
238
+ | Per-field seed derivation | `baseSeed ^ hash(fieldName)` | Collapse of one field doesn't affect another. Reordering fields doesn't change results. |
239
+ | Amplitude normalization | Accept 0.9, 90, or '90%' | All three forms mean the same thing. Don't make users do math. |
240
+ | Duplicate Mulberry32 PRNG | Copied, not imported from Monte Carlo engine | 14 lines. Avoids coupling QFacts to a specific engine. |
241
+ | Top-level fields only | No nested superposition (v1) | Recursive traversal complicates types significantly. Can add later without breaking the API. |
242
+ | Pure functions | No classes, no state | Matches the existing functions module pattern. Composable and testable. |
243
+ | Coercion is opt-in | Off by default | Amplitude keys are strings. Automatic type guessing is risky. User opts in when they know their keys are parseable. |
@@ -0,0 +1,111 @@
1
+ import type { FactInput } from '../core';
2
+ import type {
3
+ Superposition,
4
+ QFactInput,
5
+ CollapseOptions,
6
+ CollapseRecord,
7
+ CollapsedFact,
8
+ AmplitudeValue,
9
+ } from './types';
10
+ import { resolveStrategy } from './strategies';
11
+
12
+ export function isSuperposition(value: unknown): value is Superposition {
13
+ return (
14
+ typeof value === 'object' &&
15
+ value !== null &&
16
+ (value as any).state === 'superposition' &&
17
+ typeof (value as any).amplitudes === 'object' &&
18
+ (value as any).amplitudes !== null
19
+ );
20
+ }
21
+
22
+ function normalizeAmplitudes(amplitudes: Record<string, AmplitudeValue>): Record<string, number> {
23
+ const parsed: Record<string, number> = {};
24
+
25
+ for (const [key, raw] of Object.entries(amplitudes)) {
26
+ if (typeof raw === 'string' && raw.endsWith('%')) {
27
+ parsed[key] = parseFloat(raw.slice(0, -1));
28
+ } else {
29
+ parsed[key] = Number(raw);
30
+ }
31
+ }
32
+
33
+ const values = Object.values(parsed);
34
+ const total = values.reduce((sum, v) => sum + v, 0);
35
+ const anyAboveOne = values.some((v) => v > 1);
36
+
37
+ if (anyAboveOne || total > 1.001) {
38
+ // Treat as raw numbers, normalize against total
39
+ for (const key of Object.keys(parsed)) {
40
+ parsed[key] = parsed[key] / total;
41
+ }
42
+ } else if (Math.abs(total - 1.0) > 0.001) {
43
+ // All ≤ 1 but doesn't sum to 1 — normalize
44
+ for (const key of Object.keys(parsed)) {
45
+ parsed[key] = parsed[key] / total;
46
+ }
47
+ }
48
+ // else: all ≤ 1 and total ≈ 1.0 — use as-is
49
+
50
+ return parsed;
51
+ }
52
+
53
+ export function superposition(amplitudes: Record<string, AmplitudeValue>): Superposition {
54
+ return {
55
+ state: 'superposition',
56
+ amplitudes: normalizeAmplitudes(amplitudes),
57
+ };
58
+ }
59
+
60
+ export function collapse(qfact: QFactInput, options?: CollapseOptions): CollapsedFact {
61
+ const strategy = options?.strategy ?? 'seeded';
62
+ const collapseLog: CollapseRecord[] = [];
63
+ const data: Record<string, any> = {};
64
+
65
+ let fieldIndex = 0;
66
+ for (const [field, value] of Object.entries(qfact.data)) {
67
+ if (isSuperposition(value)) {
68
+ const resolve = resolveStrategy(strategy, options ?? {}, fieldIndex);
69
+ const selectedKey = resolve(value.amplitudes, field);
70
+ let selectedValue: any = selectedKey;
71
+
72
+ if (options?.coerce) {
73
+ try {
74
+ selectedValue = JSON.parse(selectedKey);
75
+ } catch {
76
+ // keep as string
77
+ }
78
+ }
79
+
80
+ data[field] = selectedValue;
81
+ collapseLog.push({
82
+ field,
83
+ selectedValue,
84
+ amplitudes: { ...value.amplitudes },
85
+ strategy,
86
+ ...(strategy === 'seeded' ? { seed: options?.seed ?? 0 } : {}),
87
+ });
88
+ } else {
89
+ data[field] = value;
90
+ }
91
+ fieldIndex++;
92
+ }
93
+
94
+ const fact: FactInput = {
95
+ ...(qfact.id != null ? { id: qfact.id } : {}),
96
+ type: qfact.type,
97
+ data,
98
+ };
99
+
100
+ return { fact, collapseLog };
101
+ }
102
+
103
+ export function collapseAll(qfacts: QFactInput[], options?: CollapseOptions): CollapsedFact[] {
104
+ const baseSeed = options?.seed ?? 0;
105
+ return qfacts.map((qfact, index) =>
106
+ collapse(qfact, {
107
+ ...options,
108
+ seed: (baseSeed + index) >>> 0,
109
+ })
110
+ );
111
+ }
@@ -0,0 +1,27 @@
1
+ // Types
2
+ export type {
3
+ AmplitudeValue,
4
+ Superposition,
5
+ QValue,
6
+ QFactInput,
7
+ CollapseStrategy,
8
+ CollapseOptions,
9
+ CollapseRecord,
10
+ CollapsedFact,
11
+ SimulationResult,
12
+ } from './types';
13
+
14
+ // Core collapse functions
15
+ export { isSuperposition, superposition, collapse, collapseAll } from './collapse';
16
+
17
+ // Simulation
18
+ export { simulate } from './simulate';
19
+
20
+ // Strategies
21
+ export {
22
+ seededCollapse,
23
+ cryptoCollapse,
24
+ deterministicCollapse,
25
+ quasiRandomCollapse,
26
+ resolveStrategy,
27
+ } from './strategies';
@@ -0,0 +1,25 @@
1
+ import type { FactInput } from '../core';
2
+ import type { QFactInput, CollapseStrategy, CollapseRecord, SimulationResult } from './types';
3
+ import { collapseAll } from './collapse';
4
+
5
+ export function simulate<T>(
6
+ qfacts: QFactInput[],
7
+ engineFn: (facts: FactInput[]) => T,
8
+ options?: { runs?: number; strategy?: CollapseStrategy }
9
+ ): SimulationResult<T> {
10
+ const runs = options?.runs ?? 1000;
11
+ const strategy = options?.strategy ?? 'seeded';
12
+ const results: T[] = [];
13
+ const collapseHistory: CollapseRecord[][] = [];
14
+
15
+ for (let i = 0; i < runs; i++) {
16
+ const collapsed = collapseAll(qfacts, { strategy, seed: i });
17
+ const facts = collapsed.map((c) => c.fact);
18
+ const logs = collapsed.flatMap((c) => c.collapseLog);
19
+
20
+ results.push(engineFn(facts));
21
+ collapseHistory.push(logs);
22
+ }
23
+
24
+ return { runs, results, collapseHistory, strategy };
25
+ }
@@ -0,0 +1,20 @@
1
+ export function cryptoCollapse(amplitudes: Record<string, number>): string {
2
+ if (!globalThis.crypto?.getRandomValues) {
3
+ throw new Error(
4
+ 'crypto.getRandomValues is not available. Requires Node >= 19 or a browser environment. ' +
5
+ 'Use strategy "seeded" or "deterministic" as an alternative.'
6
+ );
7
+ }
8
+
9
+ const arr = new Uint32Array(1);
10
+ globalThis.crypto.getRandomValues(arr);
11
+ const roll = arr[0] / 4294967296;
12
+
13
+ const keys = Object.keys(amplitudes);
14
+ let cumulative = 0;
15
+ for (const key of keys) {
16
+ cumulative += amplitudes[key];
17
+ if (roll < cumulative) return key;
18
+ }
19
+ return keys[keys.length - 1];
20
+ }
@@ -0,0 +1,11 @@
1
+ export function deterministicCollapse(amplitudes: Record<string, number>): string {
2
+ let maxKey = '';
3
+ let maxVal = -Infinity;
4
+ for (const [key, val] of Object.entries(amplitudes)) {
5
+ if (val > maxVal) {
6
+ maxVal = val;
7
+ maxKey = key;
8
+ }
9
+ }
10
+ return maxKey;
11
+ }
@@ -0,0 +1,25 @@
1
+ export { seededCollapse } from './seeded';
2
+ export { cryptoCollapse } from './crypto';
3
+ export { deterministicCollapse } from './deterministic';
4
+ export { quasiRandomCollapse } from './quasi-random';
5
+
6
+ import { seededCollapse } from './seeded';
7
+ import { cryptoCollapse } from './crypto';
8
+ import { deterministicCollapse } from './deterministic';
9
+ import { quasiRandomCollapse } from './quasi-random';
10
+ import type { CollapseStrategy, CollapseOptions } from '../types';
11
+
12
+ type CollapseFunction = (amplitudes: Record<string, number>, fieldName: string) => string;
13
+
14
+ export function resolveStrategy(strategy: CollapseStrategy, options: CollapseOptions, fieldIndex?: number): CollapseFunction {
15
+ switch (strategy) {
16
+ case 'seeded':
17
+ return (amplitudes, fieldName) => seededCollapse(amplitudes, options.seed ?? 0, fieldName);
18
+ case 'crypto':
19
+ return (amplitudes) => cryptoCollapse(amplitudes);
20
+ case 'deterministic':
21
+ return (amplitudes) => deterministicCollapse(amplitudes);
22
+ case 'quasi-random':
23
+ return (amplitudes) => quasiRandomCollapse(amplitudes, fieldIndex ?? 0);
24
+ }
25
+ }