@indexnetwork/protocol 5.0.0 → 5.1.0-rc.367.1
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/CHANGELOG.md +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/opportunity/discriminator/discriminator.miner.d.ts +3 -1
- package/dist/opportunity/discriminator/discriminator.miner.d.ts.map +1 -1
- package/dist/opportunity/discriminator/discriminator.miner.js +48 -18
- package/dist/opportunity/discriminator/discriminator.miner.js.map +1 -1
- package/dist/opportunity/outcome/outcome.env.d.ts +61 -0
- package/dist/opportunity/outcome/outcome.env.d.ts.map +1 -0
- package/dist/opportunity/outcome/outcome.env.js +64 -0
- package/dist/opportunity/outcome/outcome.env.js.map +1 -0
- package/dist/opportunity/outcome/outcome.hypotheses.d.ts +30 -0
- package/dist/opportunity/outcome/outcome.hypotheses.d.ts.map +1 -0
- package/dist/opportunity/outcome/outcome.hypotheses.js +118 -0
- package/dist/opportunity/outcome/outcome.hypotheses.js.map +1 -0
- package/dist/opportunity/outcome/outcome.shadow.d.ts +57 -0
- package/dist/opportunity/outcome/outcome.shadow.d.ts.map +1 -0
- package/dist/opportunity/outcome/outcome.shadow.js +88 -0
- package/dist/opportunity/outcome/outcome.shadow.js.map +1 -0
- package/dist/opportunity/outcome/outcome.types.d.ts +111 -0
- package/dist/opportunity/outcome/outcome.types.d.ts.map +1 -0
- package/dist/opportunity/outcome/outcome.types.js +27 -0
- package/dist/opportunity/outcome/outcome.types.js.map +1 -0
- package/dist/shared/interfaces/database.interface.d.ts +28 -2
- package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/database.interface.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized accessors for Lens B outcome-question environment variables
|
|
3
|
+
* (IND-434).
|
|
4
|
+
*
|
|
5
|
+
* OUTCOME_QUESTIONS_MODE off | shadow | on — Lens B "explicit opportunity
|
|
6
|
+
* outcomes → grounded questions" pipeline.
|
|
7
|
+
*
|
|
8
|
+
* off (default): no outcome feedback events are
|
|
9
|
+
* captured and no hypothesis mining runs. Zero
|
|
10
|
+
* behavior change.
|
|
11
|
+
* shadow: capture one idempotent append-only
|
|
12
|
+
* feedback event per explicit owner action, mine
|
|
13
|
+
* neutral trade-off hypotheses for the exact
|
|
14
|
+
* recipient + intent + fingerprint, and emit
|
|
15
|
+
* aggregate telemetry only. No question, ranking,
|
|
16
|
+
* intent, premise, memory, newborn-stamp, or push
|
|
17
|
+
* writes.
|
|
18
|
+
* on: reserved for a later phase (IND-438) that
|
|
19
|
+
* turns a grounded hypothesis into a user-facing
|
|
20
|
+
* question. This slice treats "on" exactly like
|
|
21
|
+
* "shadow" for capture + mining; it never emits
|
|
22
|
+
* questions.
|
|
23
|
+
*
|
|
24
|
+
* All reads go through this module — do not read this variable via
|
|
25
|
+
* `process.env` elsewhere. Values are read on every call (no caching) so tests
|
|
26
|
+
* and long-lived processes observe changes.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Current OUTCOME_QUESTIONS_MODE (default off). Only the exact trimmed literals
|
|
30
|
+
* "shadow" and "on" activate; every other value (including unset/empty) is off.
|
|
31
|
+
*/
|
|
32
|
+
export function outcomeQuestionsMode() {
|
|
33
|
+
const value = process.env.OUTCOME_QUESTIONS_MODE?.trim();
|
|
34
|
+
return value === "shadow" || value === "on" ? value : "off";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* True when Lens B capture + mining should run at all. Both "shadow" and "on"
|
|
38
|
+
* activate the shadow pipeline; only the future question-emitting behavior is
|
|
39
|
+
* gated on "on" (not part of this slice).
|
|
40
|
+
*/
|
|
41
|
+
export function isOutcomeQuestionsActivated() {
|
|
42
|
+
return outcomeQuestionsMode() !== "off";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Independent-support threshold (k). Every compared group (discriminator side)
|
|
46
|
+
* must have at least this many INDEPENDENT (related-opportunity-deduplicated)
|
|
47
|
+
* examples before the hypothesis is eligible. Enforced per side, so no side
|
|
48
|
+
* can be traced back to a small handful of individuals.
|
|
49
|
+
*/
|
|
50
|
+
export const OUTCOME_MIN_INDEPENDENT_SUPPORT = 5;
|
|
51
|
+
/** Minimum number of qualified sides (each ≥ k) for a hypothesis to compare. */
|
|
52
|
+
export const OUTCOME_MIN_COMPARED_SIDES = 2;
|
|
53
|
+
/**
|
|
54
|
+
* k-anonymity floor for attempting a mining pass at all: the number of
|
|
55
|
+
* distinct independent examples (after related-opportunity dedup) required
|
|
56
|
+
* before the miner is invoked. Set to k × minimum compared sides so a pass can
|
|
57
|
+
* never produce an eligible hypothesis below the aggregate floor.
|
|
58
|
+
*/
|
|
59
|
+
export const OUTCOME_MIN_INDEPENDENT_EXAMPLES = OUTCOME_MIN_INDEPENDENT_SUPPORT * OUTCOME_MIN_COMPARED_SIDES;
|
|
60
|
+
/** Max independent examples sent to the miner LLM (most recent first). */
|
|
61
|
+
export const OUTCOME_MAX_CANDIDATES = 48;
|
|
62
|
+
/** Max chars of presentation-safe candidate snapshot stored/sent per example. */
|
|
63
|
+
export const OUTCOME_MAX_PUBLIC_CONTEXT_CHARS = 400;
|
|
64
|
+
//# sourceMappingURL=outcome.env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.env.js","sourceRoot":"/","sources":["opportunity/outcome/outcome.env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,CAAC;IACzD,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,oBAAoB,EAAE,KAAK,KAAK,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC;AAEjD,gFAAgF;AAChF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAC3C,+BAA+B,GAAG,0BAA0B,CAAC;AAE/D,0EAA0E;AAC1E,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC,iFAAiF;AACjF,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,CAAC","sourcesContent":["/**\n * Centralized accessors for Lens B outcome-question environment variables\n * (IND-434).\n *\n * OUTCOME_QUESTIONS_MODE off | shadow | on — Lens B \"explicit opportunity\n * outcomes → grounded questions\" pipeline.\n *\n * off (default): no outcome feedback events are\n * captured and no hypothesis mining runs. Zero\n * behavior change.\n * shadow: capture one idempotent append-only\n * feedback event per explicit owner action, mine\n * neutral trade-off hypotheses for the exact\n * recipient + intent + fingerprint, and emit\n * aggregate telemetry only. No question, ranking,\n * intent, premise, memory, newborn-stamp, or push\n * writes.\n * on: reserved for a later phase (IND-438) that\n * turns a grounded hypothesis into a user-facing\n * question. This slice treats \"on\" exactly like\n * \"shadow\" for capture + mining; it never emits\n * questions.\n *\n * All reads go through this module — do not read this variable via\n * `process.env` elsewhere. Values are read on every call (no caching) so tests\n * and long-lived processes observe changes.\n */\n\n/** Lens B outcome-question mode. */\nexport type OutcomeQuestionsMode = \"off\" | \"shadow\" | \"on\";\n\n/**\n * Current OUTCOME_QUESTIONS_MODE (default off). Only the exact trimmed literals\n * \"shadow\" and \"on\" activate; every other value (including unset/empty) is off.\n */\nexport function outcomeQuestionsMode(): OutcomeQuestionsMode {\n const value = process.env.OUTCOME_QUESTIONS_MODE?.trim();\n return value === \"shadow\" || value === \"on\" ? value : \"off\";\n}\n\n/**\n * True when Lens B capture + mining should run at all. Both \"shadow\" and \"on\"\n * activate the shadow pipeline; only the future question-emitting behavior is\n * gated on \"on\" (not part of this slice).\n */\nexport function isOutcomeQuestionsActivated(): boolean {\n return outcomeQuestionsMode() !== \"off\";\n}\n\n/**\n * Independent-support threshold (k). Every compared group (discriminator side)\n * must have at least this many INDEPENDENT (related-opportunity-deduplicated)\n * examples before the hypothesis is eligible. Enforced per side, so no side\n * can be traced back to a small handful of individuals.\n */\nexport const OUTCOME_MIN_INDEPENDENT_SUPPORT = 5;\n\n/** Minimum number of qualified sides (each ≥ k) for a hypothesis to compare. */\nexport const OUTCOME_MIN_COMPARED_SIDES = 2;\n\n/**\n * k-anonymity floor for attempting a mining pass at all: the number of\n * distinct independent examples (after related-opportunity dedup) required\n * before the miner is invoked. Set to k × minimum compared sides so a pass can\n * never produce an eligible hypothesis below the aggregate floor.\n */\nexport const OUTCOME_MIN_INDEPENDENT_EXAMPLES =\n OUTCOME_MIN_INDEPENDENT_SUPPORT * OUTCOME_MIN_COMPARED_SIDES;\n\n/** Max independent examples sent to the miner LLM (most recent first). */\nexport const OUTCOME_MAX_CANDIDATES = 48;\n\n/** Max chars of presentation-safe candidate snapshot stored/sent per example. */\nexport const OUTCOME_MAX_PUBLIC_CONTEXT_CHARS = 400;\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens B outcome join + threshold — pure functions, no LLM, no I/O (IND-434).
|
|
3
|
+
*
|
|
4
|
+
* Given discriminators whose candidate → side assignments were produced BLIND
|
|
5
|
+
* to outcome (the miner never saw which side the user chose), this module joins
|
|
6
|
+
* the explicit owner-outcome labels and produces aggregate-only telemetry:
|
|
7
|
+
*
|
|
8
|
+
* - Independence: capture admits only one unique counterpart per opportunity;
|
|
9
|
+
* the caller then deduplicates by that recipient-scoped counterpart hash.
|
|
10
|
+
* Every retained entry therefore represents one distinct counterpart.
|
|
11
|
+
* - Threshold: a discriminator side is "qualified" only when it holds at
|
|
12
|
+
* least `minIndependentSupport` (k) independent examples. A hypothesis is
|
|
13
|
+
* eligible only when at least `minComparedSides` sides qualify.
|
|
14
|
+
* - Small-cell suppression: only qualified sides (≥ k) are ever emitted, so
|
|
15
|
+
* no aggregate row can be traced to a small handful of individuals.
|
|
16
|
+
*
|
|
17
|
+
* The outcome label is joined here and ONLY here — the miner and the side
|
|
18
|
+
* assignments upstream are independent of it, so association can never leak
|
|
19
|
+
* into classification.
|
|
20
|
+
*/
|
|
21
|
+
import type { JoinOutcomeHypothesesInput, OutcomeShadowResult } from "./outcome.types.js";
|
|
22
|
+
/**
|
|
23
|
+
* Join outcome labels onto blind side assignments and keep only hypotheses that
|
|
24
|
+
* clear the independent-support threshold on at least `minComparedSides` sides.
|
|
25
|
+
*
|
|
26
|
+
* @returns Aggregate telemetry: eligible hypotheses sorted by (min support desc,
|
|
27
|
+
* label asc) for deterministic ordering, each carrying only qualified sides.
|
|
28
|
+
*/
|
|
29
|
+
export declare function joinOutcomeHypotheses(input: JoinOutcomeHypothesesInput): OutcomeShadowResult;
|
|
30
|
+
//# sourceMappingURL=outcome.hypotheses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.hypotheses.d.ts","sourceRoot":"/","sources":["opportunity/outcome/outcome.hypotheses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,0BAA0B,EAAqB,mBAAmB,EAAsB,MAAM,oBAAoB,CAAC;AAOjI;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,mBAAmB,CAsF5F"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens B outcome join + threshold — pure functions, no LLM, no I/O (IND-434).
|
|
3
|
+
*
|
|
4
|
+
* Given discriminators whose candidate → side assignments were produced BLIND
|
|
5
|
+
* to outcome (the miner never saw which side the user chose), this module joins
|
|
6
|
+
* the explicit owner-outcome labels and produces aggregate-only telemetry:
|
|
7
|
+
*
|
|
8
|
+
* - Independence: capture admits only one unique counterpart per opportunity;
|
|
9
|
+
* the caller then deduplicates by that recipient-scoped counterpart hash.
|
|
10
|
+
* Every retained entry therefore represents one distinct counterpart.
|
|
11
|
+
* - Threshold: a discriminator side is "qualified" only when it holds at
|
|
12
|
+
* least `minIndependentSupport` (k) independent examples. A hypothesis is
|
|
13
|
+
* eligible only when at least `minComparedSides` sides qualify.
|
|
14
|
+
* - Small-cell suppression: only qualified sides (≥ k) are ever emitted, so
|
|
15
|
+
* no aggregate row can be traced to a small handful of individuals.
|
|
16
|
+
*
|
|
17
|
+
* The outcome label is joined here and ONLY here — the miner and the side
|
|
18
|
+
* assignments upstream are independent of it, so association can never leak
|
|
19
|
+
* into classification.
|
|
20
|
+
*/
|
|
21
|
+
import { OUTCOME_MIN_COMPARED_SIDES, OUTCOME_MIN_INDEPENDENT_SUPPORT } from "./outcome.env.js";
|
|
22
|
+
/** Round a rate to 0.01 for telemetry (never a raw count). */
|
|
23
|
+
function roundRate(n) {
|
|
24
|
+
return Math.round(n * 100) / 100;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Join outcome labels onto blind side assignments and keep only hypotheses that
|
|
28
|
+
* clear the independent-support threshold on at least `minComparedSides` sides.
|
|
29
|
+
*
|
|
30
|
+
* @returns Aggregate telemetry: eligible hypotheses sorted by (min support desc,
|
|
31
|
+
* label asc) for deterministic ordering, each carrying only qualified sides.
|
|
32
|
+
*/
|
|
33
|
+
export function joinOutcomeHypotheses(input) {
|
|
34
|
+
const minSupport = input.minIndependentSupport ?? OUTCOME_MIN_INDEPENDENT_SUPPORT;
|
|
35
|
+
const minSides = input.minComparedSides ?? OUTCOME_MIN_COMPARED_SIDES;
|
|
36
|
+
// Distinct independent examples with a joinable outcome label.
|
|
37
|
+
const poolSize = input.examples.size;
|
|
38
|
+
const hypotheses = [];
|
|
39
|
+
for (const discriminator of input.discriminators) {
|
|
40
|
+
// Normalize sides by trimming, then reject the ENTIRE discriminator if any
|
|
41
|
+
// side is empty or if labels are not unique after normalization. Silently
|
|
42
|
+
// deduplicating malformed model output would turn an ambiguous comparison
|
|
43
|
+
// into apparently valid evidence.
|
|
44
|
+
const normalizedSides = discriminator.sides.map((side) => side.trim());
|
|
45
|
+
if (normalizedSides.some((side) => side.length === 0))
|
|
46
|
+
continue;
|
|
47
|
+
if (new Set(normalizedSides).size !== normalizedSides.length)
|
|
48
|
+
continue;
|
|
49
|
+
if (normalizedSides.length < minSides)
|
|
50
|
+
continue;
|
|
51
|
+
const validSides = new Set(normalizedSides);
|
|
52
|
+
// First normalize VERIFIED, labelled assignments by candidate id. If the
|
|
53
|
+
// same independent example is assigned to different sides, mark it
|
|
54
|
+
// ambiguous and exclude it from every side; it must never support two cells.
|
|
55
|
+
const sideById = new Map();
|
|
56
|
+
const ambiguousIds = new Set();
|
|
57
|
+
for (const assignment of discriminator.assignments) {
|
|
58
|
+
if (assignment.side === null || !assignment.verified)
|
|
59
|
+
continue;
|
|
60
|
+
const side = assignment.side.trim();
|
|
61
|
+
if (!validSides.has(side) || !input.examples.has(assignment.id))
|
|
62
|
+
continue;
|
|
63
|
+
const previous = sideById.get(assignment.id);
|
|
64
|
+
if (previous !== undefined && previous !== side)
|
|
65
|
+
ambiguousIds.add(assignment.id);
|
|
66
|
+
else if (previous === undefined)
|
|
67
|
+
sideById.set(assignment.id, side);
|
|
68
|
+
}
|
|
69
|
+
// Tally DISTINCT independent example ids per unambiguous side. Repeating
|
|
70
|
+
// the same assignment never increases support.
|
|
71
|
+
const idsBySide = new Map();
|
|
72
|
+
const acceptedIdsBySide = new Map();
|
|
73
|
+
for (const [id, side] of sideById) {
|
|
74
|
+
if (ambiguousIds.has(id))
|
|
75
|
+
continue;
|
|
76
|
+
const label = input.examples.get(id);
|
|
77
|
+
if (label === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
if (!idsBySide.has(side))
|
|
80
|
+
idsBySide.set(side, new Set());
|
|
81
|
+
idsBySide.get(side).add(id);
|
|
82
|
+
if (label === "accepted") {
|
|
83
|
+
if (!acceptedIdsBySide.has(side))
|
|
84
|
+
acceptedIdsBySide.set(side, new Set());
|
|
85
|
+
acceptedIdsBySide.get(side).add(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Keep only sides that clear the independent-support threshold (>= k
|
|
89
|
+
// genuinely distinct independent examples).
|
|
90
|
+
const qualifiedSides = [];
|
|
91
|
+
for (const side of normalizedSides) {
|
|
92
|
+
const support = idsBySide.get(side)?.size ?? 0;
|
|
93
|
+
if (support < minSupport)
|
|
94
|
+
continue; // small-cell: never emitted
|
|
95
|
+
const accepted = acceptedIdsBySide.get(side)?.size ?? 0;
|
|
96
|
+
qualifiedSides.push({
|
|
97
|
+
side,
|
|
98
|
+
independentSupport: support,
|
|
99
|
+
acceptRate: roundRate(accepted / support),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (qualifiedSides.length < minSides)
|
|
103
|
+
continue; // not enough to compare
|
|
104
|
+
// Deterministic side ordering: support desc, then label asc.
|
|
105
|
+
qualifiedSides.sort((a, b) => b.independentSupport - a.independentSupport || a.side.localeCompare(b.side));
|
|
106
|
+
hypotheses.push({
|
|
107
|
+
label: discriminator.label,
|
|
108
|
+
questionSeed: discriminator.questionSeed,
|
|
109
|
+
sides: qualifiedSides,
|
|
110
|
+
evidenceRate: discriminator.evidenceRate,
|
|
111
|
+
minIndependentSupport: Math.min(...qualifiedSides.map((s) => s.independentSupport)),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// Deterministic hypothesis ordering: strongest support first, then label.
|
|
115
|
+
hypotheses.sort((a, b) => b.minIndependentSupport - a.minIndependentSupport || a.label.localeCompare(b.label));
|
|
116
|
+
return { poolSize, eligibleCount: hypotheses.length, hypotheses };
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=outcome.hypotheses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.hypotheses.js","sourceRoot":"/","sources":["opportunity/outcome/outcome.hypotheses.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lens B outcome join + threshold — pure functions, no LLM, no I/O (IND-434).\n *\n * Given discriminators whose candidate → side assignments were produced BLIND\n * to outcome (the miner never saw which side the user chose), this module joins\n * the explicit owner-outcome labels and produces aggregate-only telemetry:\n *\n * - Independence: capture admits only one unique counterpart per opportunity;\n * the caller then deduplicates by that recipient-scoped counterpart hash.\n * Every retained entry therefore represents one distinct counterpart.\n * - Threshold: a discriminator side is \"qualified\" only when it holds at\n * least `minIndependentSupport` (k) independent examples. A hypothesis is\n * eligible only when at least `minComparedSides` sides qualify.\n * - Small-cell suppression: only qualified sides (≥ k) are ever emitted, so\n * no aggregate row can be traced to a small handful of individuals.\n *\n * The outcome label is joined here and ONLY here — the miner and the side\n * assignments upstream are independent of it, so association can never leak\n * into classification.\n */\n\nimport { OUTCOME_MIN_COMPARED_SIDES, OUTCOME_MIN_INDEPENDENT_SUPPORT } from \"./outcome.env.js\";\nimport type { JoinOutcomeHypothesesInput, OutcomeHypothesis, OutcomeShadowResult, OutcomeSideSupport } from \"./outcome.types.js\";\n\n/** Round a rate to 0.01 for telemetry (never a raw count). */\nfunction roundRate(n: number): number {\n return Math.round(n * 100) / 100;\n}\n\n/**\n * Join outcome labels onto blind side assignments and keep only hypotheses that\n * clear the independent-support threshold on at least `minComparedSides` sides.\n *\n * @returns Aggregate telemetry: eligible hypotheses sorted by (min support desc,\n * label asc) for deterministic ordering, each carrying only qualified sides.\n */\nexport function joinOutcomeHypotheses(input: JoinOutcomeHypothesesInput): OutcomeShadowResult {\n const minSupport = input.minIndependentSupport ?? OUTCOME_MIN_INDEPENDENT_SUPPORT;\n const minSides = input.minComparedSides ?? OUTCOME_MIN_COMPARED_SIDES;\n\n // Distinct independent examples with a joinable outcome label.\n const poolSize = input.examples.size;\n\n const hypotheses: OutcomeHypothesis[] = [];\n\n for (const discriminator of input.discriminators) {\n // Normalize sides by trimming, then reject the ENTIRE discriminator if any\n // side is empty or if labels are not unique after normalization. Silently\n // deduplicating malformed model output would turn an ambiguous comparison\n // into apparently valid evidence.\n const normalizedSides = discriminator.sides.map((side) => side.trim());\n if (normalizedSides.some((side) => side.length === 0)) continue;\n if (new Set(normalizedSides).size !== normalizedSides.length) continue;\n if (normalizedSides.length < minSides) continue;\n const validSides = new Set(normalizedSides);\n\n // First normalize VERIFIED, labelled assignments by candidate id. If the\n // same independent example is assigned to different sides, mark it\n // ambiguous and exclude it from every side; it must never support two cells.\n const sideById = new Map<string, string>();\n const ambiguousIds = new Set<string>();\n for (const assignment of discriminator.assignments) {\n if (assignment.side === null || !assignment.verified) continue;\n const side = assignment.side.trim();\n if (!validSides.has(side) || !input.examples.has(assignment.id)) continue;\n const previous = sideById.get(assignment.id);\n if (previous !== undefined && previous !== side) ambiguousIds.add(assignment.id);\n else if (previous === undefined) sideById.set(assignment.id, side);\n }\n\n // Tally DISTINCT independent example ids per unambiguous side. Repeating\n // the same assignment never increases support.\n const idsBySide = new Map<string, Set<string>>();\n const acceptedIdsBySide = new Map<string, Set<string>>();\n for (const [id, side] of sideById) {\n if (ambiguousIds.has(id)) continue;\n const label = input.examples.get(id);\n if (label === undefined) continue;\n if (!idsBySide.has(side)) idsBySide.set(side, new Set());\n idsBySide.get(side)!.add(id);\n if (label === \"accepted\") {\n if (!acceptedIdsBySide.has(side)) acceptedIdsBySide.set(side, new Set());\n acceptedIdsBySide.get(side)!.add(id);\n }\n }\n\n // Keep only sides that clear the independent-support threshold (>= k\n // genuinely distinct independent examples).\n const qualifiedSides: OutcomeSideSupport[] = [];\n for (const side of normalizedSides) {\n const support = idsBySide.get(side)?.size ?? 0;\n if (support < minSupport) continue; // small-cell: never emitted\n const accepted = acceptedIdsBySide.get(side)?.size ?? 0;\n qualifiedSides.push({\n side,\n independentSupport: support,\n acceptRate: roundRate(accepted / support),\n });\n }\n\n if (qualifiedSides.length < minSides) continue; // not enough to compare\n\n // Deterministic side ordering: support desc, then label asc.\n qualifiedSides.sort(\n (a, b) => b.independentSupport - a.independentSupport || a.side.localeCompare(b.side),\n );\n\n hypotheses.push({\n label: discriminator.label,\n questionSeed: discriminator.questionSeed,\n sides: qualifiedSides,\n evidenceRate: discriminator.evidenceRate,\n minIndependentSupport: Math.min(...qualifiedSides.map((s) => s.independentSupport)),\n });\n }\n\n // Deterministic hypothesis ordering: strongest support first, then label.\n hypotheses.sort(\n (a, b) => b.minIndependentSupport - a.minIndependentSupport || a.label.localeCompare(b.label),\n );\n\n return { poolSize, eligibleCount: hypotheses.length, hypotheses };\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens B shadow orchestrator (IND-434): deduplicate related opportunities,
|
|
3
|
+
* mine neutral axes over the deduplicated historical pool BLIND to outcome,
|
|
4
|
+
* then join the owner-outcome labels and threshold the result.
|
|
5
|
+
*
|
|
6
|
+
* No persistence, no questions, no UI. The output is consumed only by aggregate
|
|
7
|
+
* telemetry for human review.
|
|
8
|
+
*
|
|
9
|
+
* Pipeline:
|
|
10
|
+
* 1. Dedup related opportunities: collapse examples sharing a dedup key to a
|
|
11
|
+
* single representative (most recent), so a cluster of related opps can
|
|
12
|
+
* neither dominate the miner nor inflate a side's independent support.
|
|
13
|
+
* 2. Cap to the most recent OUTCOME_MAX_CANDIDATES for a bounded LLM pass.
|
|
14
|
+
* 3. Build the miner pool — PoolCandidate carries id + publicContext + score
|
|
15
|
+
* and, critically, NO outcome label. The miner is structurally blind.
|
|
16
|
+
* 4. Mine neutral axes + assign candidates (evidence-verified).
|
|
17
|
+
* 5. Join outcome labels (only now) and apply the k-support threshold.
|
|
18
|
+
*/
|
|
19
|
+
import type { PoolDiscriminatorMiner } from "../discriminator/discriminator.miner.js";
|
|
20
|
+
import type { OutcomeExample, OutcomeShadowResult } from "./outcome.types.js";
|
|
21
|
+
/** Input for one Lens B shadow mining+join pass. */
|
|
22
|
+
export interface OutcomeShadowInput {
|
|
23
|
+
/**
|
|
24
|
+
* Intent payload (+ summary) text that owns this scope. Used only as miner
|
|
25
|
+
* context; never mixed with outcome labels.
|
|
26
|
+
*/
|
|
27
|
+
intentText: string;
|
|
28
|
+
/**
|
|
29
|
+
* Captured owner-outcome examples for exactly one recipient + intent +
|
|
30
|
+
* fingerprint scope. Each carries a presentation-safe snapshot and a dedup
|
|
31
|
+
* key; outcome labels live here but are withheld from the miner.
|
|
32
|
+
*/
|
|
33
|
+
examples: OutcomeExample[];
|
|
34
|
+
miner: Pick<PoolDiscriminatorMiner, "mine">;
|
|
35
|
+
/** Override the independent-support threshold (defaults to k). */
|
|
36
|
+
minIndependentSupport?: number;
|
|
37
|
+
/** Override the minimum qualified sides. */
|
|
38
|
+
minComparedSides?: number;
|
|
39
|
+
/** Override the LLM candidate cap. */
|
|
40
|
+
maxCandidates?: number;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Collapse related opportunities to independent examples: one representative
|
|
45
|
+
* (most recent by occurredAt) per distinct dedup key. Deterministic — ties on
|
|
46
|
+
* occurredAt fall back to opportunityId ordering.
|
|
47
|
+
*/
|
|
48
|
+
export declare function deduplicateOutcomeExamples(examples: OutcomeExample[]): OutcomeExample[];
|
|
49
|
+
/**
|
|
50
|
+
* Run the Lens B shadow pipeline for one scope.
|
|
51
|
+
*
|
|
52
|
+
* Throws only when *mining* fails (callers are fire-and-forget and must catch).
|
|
53
|
+
* A pool that is empty or below the compare floor after dedup returns an empty
|
|
54
|
+
* result rather than throwing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runOutcomeShadow(input: OutcomeShadowInput): Promise<OutcomeShadowResult>;
|
|
57
|
+
//# sourceMappingURL=outcome.shadow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.shadow.d.ts","sourceRoot":"/","sources":["opportunity/outcome/outcome.shadow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AAItF,OAAO,KAAK,EAAE,cAAc,EAAgB,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI5F,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,KAAK,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAC;IAC5C,kEAAkE;IAClE,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,4CAA4C;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAgBvF;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAgD9F"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens B shadow orchestrator (IND-434): deduplicate related opportunities,
|
|
3
|
+
* mine neutral axes over the deduplicated historical pool BLIND to outcome,
|
|
4
|
+
* then join the owner-outcome labels and threshold the result.
|
|
5
|
+
*
|
|
6
|
+
* No persistence, no questions, no UI. The output is consumed only by aggregate
|
|
7
|
+
* telemetry for human review.
|
|
8
|
+
*
|
|
9
|
+
* Pipeline:
|
|
10
|
+
* 1. Dedup related opportunities: collapse examples sharing a dedup key to a
|
|
11
|
+
* single representative (most recent), so a cluster of related opps can
|
|
12
|
+
* neither dominate the miner nor inflate a side's independent support.
|
|
13
|
+
* 2. Cap to the most recent OUTCOME_MAX_CANDIDATES for a bounded LLM pass.
|
|
14
|
+
* 3. Build the miner pool — PoolCandidate carries id + publicContext + score
|
|
15
|
+
* and, critically, NO outcome label. The miner is structurally blind.
|
|
16
|
+
* 4. Mine neutral axes + assign candidates (evidence-verified).
|
|
17
|
+
* 5. Join outcome labels (only now) and apply the k-support threshold.
|
|
18
|
+
*/
|
|
19
|
+
import { protocolLogger } from "../../shared/observability/protocol.logger.js";
|
|
20
|
+
import { OUTCOME_MAX_CANDIDATES, OUTCOME_MIN_COMPARED_SIDES, OUTCOME_MIN_INDEPENDENT_SUPPORT } from "./outcome.env.js";
|
|
21
|
+
import { joinOutcomeHypotheses } from "./outcome.hypotheses.js";
|
|
22
|
+
const logger = protocolLogger("OutcomeQuestionShadow");
|
|
23
|
+
/**
|
|
24
|
+
* Collapse related opportunities to independent examples: one representative
|
|
25
|
+
* (most recent by occurredAt) per distinct dedup key. Deterministic — ties on
|
|
26
|
+
* occurredAt fall back to opportunityId ordering.
|
|
27
|
+
*/
|
|
28
|
+
export function deduplicateOutcomeExamples(examples) {
|
|
29
|
+
const byKey = new Map();
|
|
30
|
+
for (const example of examples) {
|
|
31
|
+
const existing = byKey.get(example.dedupKey);
|
|
32
|
+
if (existing === undefined) {
|
|
33
|
+
byKey.set(example.dedupKey, example);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const newer = example.occurredAt > existing.occurredAt ||
|
|
37
|
+
(example.occurredAt === existing.occurredAt && example.opportunityId > existing.opportunityId);
|
|
38
|
+
if (newer)
|
|
39
|
+
byKey.set(example.dedupKey, example);
|
|
40
|
+
}
|
|
41
|
+
return [...byKey.values()].sort((a, b) => b.occurredAt.localeCompare(a.occurredAt) || a.opportunityId.localeCompare(b.opportunityId));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run the Lens B shadow pipeline for one scope.
|
|
45
|
+
*
|
|
46
|
+
* Throws only when *mining* fails (callers are fire-and-forget and must catch).
|
|
47
|
+
* A pool that is empty or below the compare floor after dedup returns an empty
|
|
48
|
+
* result rather than throwing.
|
|
49
|
+
*/
|
|
50
|
+
export async function runOutcomeShadow(input) {
|
|
51
|
+
const minSupport = input.minIndependentSupport ?? OUTCOME_MIN_INDEPENDENT_SUPPORT;
|
|
52
|
+
const minSides = input.minComparedSides ?? OUTCOME_MIN_COMPARED_SIDES;
|
|
53
|
+
const maxCandidates = input.maxCandidates ?? OUTCOME_MAX_CANDIDATES;
|
|
54
|
+
// 1–2. Dedup related opportunities, then cap to a bounded, recent pool.
|
|
55
|
+
const independent = deduplicateOutcomeExamples(input.examples).slice(0, maxCandidates);
|
|
56
|
+
if (independent.length < minSupport * minSides) {
|
|
57
|
+
return { poolSize: independent.length, eligibleCount: 0, hypotheses: [] };
|
|
58
|
+
}
|
|
59
|
+
// 3. Build the miner pool with RUN-LOCAL aliases (c0, c1, …) as candidate ids
|
|
60
|
+
// — raw opportunity ids are never sent to the LLM. The alias→outcome map is
|
|
61
|
+
// kept internally for the join. PoolCandidate deliberately excludes the
|
|
62
|
+
// outcome label, so the miner cannot condition on which side was chosen.
|
|
63
|
+
const aliased = independent.map((example, index) => ({ alias: `c${index}`, example }));
|
|
64
|
+
const candidates = aliased.map(({ alias, example }) => ({
|
|
65
|
+
id: alias,
|
|
66
|
+
publicContext: example.publicContext,
|
|
67
|
+
score: example.score ?? 1,
|
|
68
|
+
}));
|
|
69
|
+
// 4. Blind assignment (assignments come back keyed by the run-local alias).
|
|
70
|
+
const mined = await input.miner.mine({ intentText: input.intentText, candidates }, input.signal ? { signal: input.signal } : undefined);
|
|
71
|
+
if (mined.length === 0) {
|
|
72
|
+
return { poolSize: independent.length, eligibleCount: 0, hypotheses: [] };
|
|
73
|
+
}
|
|
74
|
+
// 5. Join outcome labels (only now) by alias, and threshold.
|
|
75
|
+
const examples = new Map(aliased.map(({ alias, example }) => [alias, example.label]));
|
|
76
|
+
const result = joinOutcomeHypotheses({
|
|
77
|
+
discriminators: mined,
|
|
78
|
+
examples,
|
|
79
|
+
minIndependentSupport: minSupport,
|
|
80
|
+
minComparedSides: minSides,
|
|
81
|
+
});
|
|
82
|
+
logger.debug("shadow join complete", {
|
|
83
|
+
poolSize: result.poolSize,
|
|
84
|
+
eligibleCount: result.eligibleCount,
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=outcome.shadow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.shadow.js","sourceRoot":"/","sources":["opportunity/outcome/outcome.shadow.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lens B shadow orchestrator (IND-434): deduplicate related opportunities,\n * mine neutral axes over the deduplicated historical pool BLIND to outcome,\n * then join the owner-outcome labels and threshold the result.\n *\n * No persistence, no questions, no UI. The output is consumed only by aggregate\n * telemetry for human review.\n *\n * Pipeline:\n * 1. Dedup related opportunities: collapse examples sharing a dedup key to a\n * single representative (most recent), so a cluster of related opps can\n * neither dominate the miner nor inflate a side's independent support.\n * 2. Cap to the most recent OUTCOME_MAX_CANDIDATES for a bounded LLM pass.\n * 3. Build the miner pool — PoolCandidate carries id + publicContext + score\n * and, critically, NO outcome label. The miner is structurally blind.\n * 4. Mine neutral axes + assign candidates (evidence-verified).\n * 5. Join outcome labels (only now) and apply the k-support threshold.\n */\n\nimport { protocolLogger } from \"../../shared/observability/protocol.logger.js\";\nimport type { PoolDiscriminatorMiner } from \"../discriminator/discriminator.miner.js\";\nimport type { PoolCandidate } from \"../discriminator/discriminator.types.js\";\nimport { OUTCOME_MAX_CANDIDATES, OUTCOME_MIN_COMPARED_SIDES, OUTCOME_MIN_INDEPENDENT_SUPPORT } from \"./outcome.env.js\";\nimport { joinOutcomeHypotheses } from \"./outcome.hypotheses.js\";\nimport type { OutcomeExample, OutcomeLabel, OutcomeShadowResult } from \"./outcome.types.js\";\n\nconst logger = protocolLogger(\"OutcomeQuestionShadow\");\n\n/** Input for one Lens B shadow mining+join pass. */\nexport interface OutcomeShadowInput {\n /**\n * Intent payload (+ summary) text that owns this scope. Used only as miner\n * context; never mixed with outcome labels.\n */\n intentText: string;\n /**\n * Captured owner-outcome examples for exactly one recipient + intent +\n * fingerprint scope. Each carries a presentation-safe snapshot and a dedup\n * key; outcome labels live here but are withheld from the miner.\n */\n examples: OutcomeExample[];\n miner: Pick<PoolDiscriminatorMiner, \"mine\">;\n /** Override the independent-support threshold (defaults to k). */\n minIndependentSupport?: number;\n /** Override the minimum qualified sides. */\n minComparedSides?: number;\n /** Override the LLM candidate cap. */\n maxCandidates?: number;\n signal?: AbortSignal;\n}\n\n/**\n * Collapse related opportunities to independent examples: one representative\n * (most recent by occurredAt) per distinct dedup key. Deterministic — ties on\n * occurredAt fall back to opportunityId ordering.\n */\nexport function deduplicateOutcomeExamples(examples: OutcomeExample[]): OutcomeExample[] {\n const byKey = new Map<string, OutcomeExample>();\n for (const example of examples) {\n const existing = byKey.get(example.dedupKey);\n if (existing === undefined) {\n byKey.set(example.dedupKey, example);\n continue;\n }\n const newer =\n example.occurredAt > existing.occurredAt ||\n (example.occurredAt === existing.occurredAt && example.opportunityId > existing.opportunityId);\n if (newer) byKey.set(example.dedupKey, example);\n }\n return [...byKey.values()].sort(\n (a, b) => b.occurredAt.localeCompare(a.occurredAt) || a.opportunityId.localeCompare(b.opportunityId),\n );\n}\n\n/**\n * Run the Lens B shadow pipeline for one scope.\n *\n * Throws only when *mining* fails (callers are fire-and-forget and must catch).\n * A pool that is empty or below the compare floor after dedup returns an empty\n * result rather than throwing.\n */\nexport async function runOutcomeShadow(input: OutcomeShadowInput): Promise<OutcomeShadowResult> {\n const minSupport = input.minIndependentSupport ?? OUTCOME_MIN_INDEPENDENT_SUPPORT;\n const minSides = input.minComparedSides ?? OUTCOME_MIN_COMPARED_SIDES;\n const maxCandidates = input.maxCandidates ?? OUTCOME_MAX_CANDIDATES;\n\n // 1–2. Dedup related opportunities, then cap to a bounded, recent pool.\n const independent = deduplicateOutcomeExamples(input.examples).slice(0, maxCandidates);\n if (independent.length < minSupport * minSides) {\n return { poolSize: independent.length, eligibleCount: 0, hypotheses: [] };\n }\n\n // 3. Build the miner pool with RUN-LOCAL aliases (c0, c1, …) as candidate ids\n // — raw opportunity ids are never sent to the LLM. The alias→outcome map is\n // kept internally for the join. PoolCandidate deliberately excludes the\n // outcome label, so the miner cannot condition on which side was chosen.\n const aliased = independent.map((example, index) => ({ alias: `c${index}`, example }));\n const candidates: PoolCandidate[] = aliased.map(({ alias, example }) => ({\n id: alias,\n publicContext: example.publicContext,\n score: example.score ?? 1,\n }));\n\n // 4. Blind assignment (assignments come back keyed by the run-local alias).\n const mined = await input.miner.mine(\n { intentText: input.intentText, candidates },\n input.signal ? { signal: input.signal } : undefined,\n );\n if (mined.length === 0) {\n return { poolSize: independent.length, eligibleCount: 0, hypotheses: [] };\n }\n\n // 5. Join outcome labels (only now) by alias, and threshold.\n const examples = new Map<string, OutcomeLabel>(\n aliased.map(({ alias, example }) => [alias, example.label]),\n );\n const result = joinOutcomeHypotheses({\n discriminators: mined,\n examples,\n minIndependentSupport: minSupport,\n minComparedSides: minSides,\n });\n\n logger.debug(\"shadow join complete\", {\n poolSize: result.poolSize,\n eligibleCount: result.eligibleCount,\n });\n\n return result;\n}\n"]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens B outcome-question mining — shared types (IND-434).
|
|
3
|
+
*
|
|
4
|
+
* Lens B learns whether a user's OWN past explicit opportunity decisions
|
|
5
|
+
* (accept / reject) suggest a useful clarification question, WITHOUT treating
|
|
6
|
+
* mutable statuses, counterparty actions, or system transitions as
|
|
7
|
+
* preferences. It is the historical-outcome analogue of the live-pool Lens A
|
|
8
|
+
* discriminator (see ../discriminator/*).
|
|
9
|
+
*
|
|
10
|
+
* The two lenses share the neutral-axis mining machinery (PoolDiscriminatorMiner
|
|
11
|
+
* assigns candidates to axis sides from presentation-safe context only). Lens B
|
|
12
|
+
* adds the crucial privacy/correctness discipline on top:
|
|
13
|
+
*
|
|
14
|
+
* 1. Candidates are assigned to sides BLIND to their outcome label — the
|
|
15
|
+
* classifier never sees which side the user chose (the miner input carries
|
|
16
|
+
* no outcome field at all).
|
|
17
|
+
* 2. Outcome labels are joined only AFTER assignment.
|
|
18
|
+
* 3. Related opportunities are deduplicated (one independent example per
|
|
19
|
+
* distinct counterpart) before support is counted.
|
|
20
|
+
* 4. Every compared group (side) must clear the independent-support
|
|
21
|
+
* threshold k, so aggregate telemetry never exposes a small cell.
|
|
22
|
+
*
|
|
23
|
+
* P7 (shadow) uses these types for aggregate-telemetry output only: no
|
|
24
|
+
* questions, ranking, intent, premise, memory, newborn-stamp, or push writes.
|
|
25
|
+
*/
|
|
26
|
+
import type { MinedDiscriminator } from "../discriminator/discriminator.types.js";
|
|
27
|
+
/**
|
|
28
|
+
* The only two explicit owner actions Lens B treats as preference labels.
|
|
29
|
+
* Non-action, delivery, expiry, timeout, merge, cascade, screening, and every
|
|
30
|
+
* counterparty/agent/system transition are deliberately excluded upstream.
|
|
31
|
+
*/
|
|
32
|
+
export type OutcomeLabel = "accepted" | "rejected";
|
|
33
|
+
/**
|
|
34
|
+
* One captured owner-outcome example, as consumed by the mining orchestrator.
|
|
35
|
+
* Built from an append-only feedback event; carries only presentation-safe
|
|
36
|
+
* text plus join/dedup keys — never raw ids, vectors, or model reasoning.
|
|
37
|
+
*/
|
|
38
|
+
export interface OutcomeExample {
|
|
39
|
+
/**
|
|
40
|
+
* Opportunity id — the join key between blind assignment and outcome label.
|
|
41
|
+
* Never emitted in telemetry.
|
|
42
|
+
*/
|
|
43
|
+
opportunityId: string;
|
|
44
|
+
/** Presentation-safe candidate snapshot the miner assigns sides from. */
|
|
45
|
+
publicContext: string;
|
|
46
|
+
/** Explicit owner action. Joined AFTER assignment, never seen by the miner. */
|
|
47
|
+
label: OutcomeLabel;
|
|
48
|
+
/**
|
|
49
|
+
* Related-opportunity dedup key: a stable, non-reversible identifier of the
|
|
50
|
+
* counterpart (or opportunity when no counterpart). Two examples with the
|
|
51
|
+
* same key count as ONE independent example.
|
|
52
|
+
*/
|
|
53
|
+
dedupKey: string;
|
|
54
|
+
/** Sort key for recency-based capping/representative selection (ISO-8601). */
|
|
55
|
+
occurredAt: string;
|
|
56
|
+
/** Optional confidence mass for score-weighted assignment; defaults to 1. */
|
|
57
|
+
score?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Aggregate support for one discriminator side, after dedup + threshold. */
|
|
60
|
+
export interface OutcomeSideSupport {
|
|
61
|
+
/** Neutral side label carried verbatim from the miner. */
|
|
62
|
+
side: string;
|
|
63
|
+
/** Distinct independent (deduplicated) examples assigned to this side (≥ k). */
|
|
64
|
+
independentSupport: number;
|
|
65
|
+
/**
|
|
66
|
+
* Independent accepted examples ÷ independentSupport, rounded to 0.01. A
|
|
67
|
+
* rate, never a raw small-cell count.
|
|
68
|
+
*/
|
|
69
|
+
acceptRate: number;
|
|
70
|
+
}
|
|
71
|
+
/** One eligible neutral hypothesis with aggregate-only stats. */
|
|
72
|
+
export interface OutcomeHypothesis {
|
|
73
|
+
/** Discriminator label, e.g. "Hands-on builders vs strategic advisors". */
|
|
74
|
+
label: string;
|
|
75
|
+
/** A neutral question the intent owner could answer to resolve the axis. */
|
|
76
|
+
questionSeed: string;
|
|
77
|
+
/** Only the qualified sides (each ≥ k independent examples). */
|
|
78
|
+
sides: OutcomeSideSupport[];
|
|
79
|
+
/** Evidence-verification rate carried from the miner (hallucination health). */
|
|
80
|
+
evidenceRate: number;
|
|
81
|
+
/** Minimum independentSupport across the qualified sides (≥ k). */
|
|
82
|
+
minIndependentSupport: number;
|
|
83
|
+
}
|
|
84
|
+
/** Result of one shadow mining pass (the aggregate telemetry payload shape). */
|
|
85
|
+
export interface OutcomeShadowResult {
|
|
86
|
+
/** Distinct independent examples considered after dedup + capping. */
|
|
87
|
+
poolSize: number;
|
|
88
|
+
/** Count of eligible hypotheses (meets k + ≥ minComparedSides qualified). */
|
|
89
|
+
eligibleCount: number;
|
|
90
|
+
/**
|
|
91
|
+
* Eligible hypotheses only, sorted deterministically. Aggregate stats only —
|
|
92
|
+
* no opportunity ids, no candidate text, no small-cell counts.
|
|
93
|
+
*/
|
|
94
|
+
hypotheses: OutcomeHypothesis[];
|
|
95
|
+
}
|
|
96
|
+
/** Input to the pure outcome-join step (assignment already done, blind). */
|
|
97
|
+
export interface JoinOutcomeHypothesesInput {
|
|
98
|
+
/** Mined discriminators with verified side assignments (blind to outcome). */
|
|
99
|
+
discriminators: MinedDiscriminator[];
|
|
100
|
+
/**
|
|
101
|
+
* Outcome labels keyed by opportunity id. Each example is already
|
|
102
|
+
* independent (deduplicated by the orchestrator), so one entry = one
|
|
103
|
+
* independent example.
|
|
104
|
+
*/
|
|
105
|
+
examples: Map<string, OutcomeLabel>;
|
|
106
|
+
/** Independent-support threshold per side. Defaults to k. */
|
|
107
|
+
minIndependentSupport?: number;
|
|
108
|
+
/** Minimum qualified sides. Defaults to the module constant. */
|
|
109
|
+
minComparedSides?: number;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=outcome.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.types.d.ts","sourceRoot":"/","sources":["opportunity/outcome/outcome.types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAElF;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,UAAU,CAAC;AAEnD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,KAAK,EAAE,YAAY,CAAC;IACpB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,UAAU,EAAE,iBAAiB,EAAE,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,WAAW,0BAA0B;IACzC,8EAA8E;IAC9E,cAAc,EAAE,kBAAkB,EAAE,CAAC;IACrC;;;;OAIG;IACH,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,6DAA6D;IAC7D,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens B outcome-question mining — shared types (IND-434).
|
|
3
|
+
*
|
|
4
|
+
* Lens B learns whether a user's OWN past explicit opportunity decisions
|
|
5
|
+
* (accept / reject) suggest a useful clarification question, WITHOUT treating
|
|
6
|
+
* mutable statuses, counterparty actions, or system transitions as
|
|
7
|
+
* preferences. It is the historical-outcome analogue of the live-pool Lens A
|
|
8
|
+
* discriminator (see ../discriminator/*).
|
|
9
|
+
*
|
|
10
|
+
* The two lenses share the neutral-axis mining machinery (PoolDiscriminatorMiner
|
|
11
|
+
* assigns candidates to axis sides from presentation-safe context only). Lens B
|
|
12
|
+
* adds the crucial privacy/correctness discipline on top:
|
|
13
|
+
*
|
|
14
|
+
* 1. Candidates are assigned to sides BLIND to their outcome label — the
|
|
15
|
+
* classifier never sees which side the user chose (the miner input carries
|
|
16
|
+
* no outcome field at all).
|
|
17
|
+
* 2. Outcome labels are joined only AFTER assignment.
|
|
18
|
+
* 3. Related opportunities are deduplicated (one independent example per
|
|
19
|
+
* distinct counterpart) before support is counted.
|
|
20
|
+
* 4. Every compared group (side) must clear the independent-support
|
|
21
|
+
* threshold k, so aggregate telemetry never exposes a small cell.
|
|
22
|
+
*
|
|
23
|
+
* P7 (shadow) uses these types for aggregate-telemetry output only: no
|
|
24
|
+
* questions, ranking, intent, premise, memory, newborn-stamp, or push writes.
|
|
25
|
+
*/
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=outcome.types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.types.js","sourceRoot":"/","sources":["opportunity/outcome/outcome.types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG","sourcesContent":["/**\n * Lens B outcome-question mining — shared types (IND-434).\n *\n * Lens B learns whether a user's OWN past explicit opportunity decisions\n * (accept / reject) suggest a useful clarification question, WITHOUT treating\n * mutable statuses, counterparty actions, or system transitions as\n * preferences. It is the historical-outcome analogue of the live-pool Lens A\n * discriminator (see ../discriminator/*).\n *\n * The two lenses share the neutral-axis mining machinery (PoolDiscriminatorMiner\n * assigns candidates to axis sides from presentation-safe context only). Lens B\n * adds the crucial privacy/correctness discipline on top:\n *\n * 1. Candidates are assigned to sides BLIND to their outcome label — the\n * classifier never sees which side the user chose (the miner input carries\n * no outcome field at all).\n * 2. Outcome labels are joined only AFTER assignment.\n * 3. Related opportunities are deduplicated (one independent example per\n * distinct counterpart) before support is counted.\n * 4. Every compared group (side) must clear the independent-support\n * threshold k, so aggregate telemetry never exposes a small cell.\n *\n * P7 (shadow) uses these types for aggregate-telemetry output only: no\n * questions, ranking, intent, premise, memory, newborn-stamp, or push writes.\n */\n\nimport type { MinedDiscriminator } from \"../discriminator/discriminator.types.js\";\n\n/**\n * The only two explicit owner actions Lens B treats as preference labels.\n * Non-action, delivery, expiry, timeout, merge, cascade, screening, and every\n * counterparty/agent/system transition are deliberately excluded upstream.\n */\nexport type OutcomeLabel = \"accepted\" | \"rejected\";\n\n/**\n * One captured owner-outcome example, as consumed by the mining orchestrator.\n * Built from an append-only feedback event; carries only presentation-safe\n * text plus join/dedup keys — never raw ids, vectors, or model reasoning.\n */\nexport interface OutcomeExample {\n /**\n * Opportunity id — the join key between blind assignment and outcome label.\n * Never emitted in telemetry.\n */\n opportunityId: string;\n /** Presentation-safe candidate snapshot the miner assigns sides from. */\n publicContext: string;\n /** Explicit owner action. Joined AFTER assignment, never seen by the miner. */\n label: OutcomeLabel;\n /**\n * Related-opportunity dedup key: a stable, non-reversible identifier of the\n * counterpart (or opportunity when no counterpart). Two examples with the\n * same key count as ONE independent example.\n */\n dedupKey: string;\n /** Sort key for recency-based capping/representative selection (ISO-8601). */\n occurredAt: string;\n /** Optional confidence mass for score-weighted assignment; defaults to 1. */\n score?: number;\n}\n\n/** Aggregate support for one discriminator side, after dedup + threshold. */\nexport interface OutcomeSideSupport {\n /** Neutral side label carried verbatim from the miner. */\n side: string;\n /** Distinct independent (deduplicated) examples assigned to this side (≥ k). */\n independentSupport: number;\n /**\n * Independent accepted examples ÷ independentSupport, rounded to 0.01. A\n * rate, never a raw small-cell count.\n */\n acceptRate: number;\n}\n\n/** One eligible neutral hypothesis with aggregate-only stats. */\nexport interface OutcomeHypothesis {\n /** Discriminator label, e.g. \"Hands-on builders vs strategic advisors\". */\n label: string;\n /** A neutral question the intent owner could answer to resolve the axis. */\n questionSeed: string;\n /** Only the qualified sides (each ≥ k independent examples). */\n sides: OutcomeSideSupport[];\n /** Evidence-verification rate carried from the miner (hallucination health). */\n evidenceRate: number;\n /** Minimum independentSupport across the qualified sides (≥ k). */\n minIndependentSupport: number;\n}\n\n/** Result of one shadow mining pass (the aggregate telemetry payload shape). */\nexport interface OutcomeShadowResult {\n /** Distinct independent examples considered after dedup + capping. */\n poolSize: number;\n /** Count of eligible hypotheses (meets k + ≥ minComparedSides qualified). */\n eligibleCount: number;\n /**\n * Eligible hypotheses only, sorted deterministically. Aggregate stats only —\n * no opportunity ids, no candidate text, no small-cell counts.\n */\n hypotheses: OutcomeHypothesis[];\n}\n\n/** Input to the pure outcome-join step (assignment already done, blind). */\nexport interface JoinOutcomeHypothesesInput {\n /** Mined discriminators with verified side assignments (blind to outcome). */\n discriminators: MinedDiscriminator[];\n /**\n * Outcome labels keyed by opportunity id. Each example is already\n * independent (deduplicated by the orchestrator), so one entry = one\n * independent example.\n */\n examples: Map<string, OutcomeLabel>;\n /** Independent-support threshold per side. Defaults to k. */\n minIndependentSupport?: number;\n /** Minimum qualified sides. Defaults to the module constant. */\n minComparedSides?: number;\n}\n"]}
|
|
@@ -1179,9 +1179,11 @@ export interface Database {
|
|
|
1179
1179
|
*
|
|
1180
1180
|
* @param id - Opportunity ID
|
|
1181
1181
|
* @param status - New status
|
|
1182
|
+
* @param acceptedBy - Required when `status === 'accepted'`
|
|
1183
|
+
* @param outbox - Optional IND-434 atomic outcome-capture (same-txn insert)
|
|
1182
1184
|
* @returns The updated opportunity or null if not found
|
|
1183
1185
|
*/
|
|
1184
|
-
updateOpportunityStatus(id: string, status: OpportunityStatus, acceptedBy?: string): Promise<Opportunity | null>;
|
|
1186
|
+
updateOpportunityStatus(id: string, status: OpportunityStatus, acceptedBy?: string, outbox?: OutcomeOutbox): Promise<Opportunity | null>;
|
|
1185
1187
|
/**
|
|
1186
1188
|
* Stamp `actedAt` on the actor matching `actorUserId` and update the
|
|
1187
1189
|
* opportunity's status atomically (row-lock + JSONB merge in one txn).
|
|
@@ -1195,9 +1197,10 @@ export interface Database {
|
|
|
1195
1197
|
* @param actorUserId - The user whose actor entry should be stamped
|
|
1196
1198
|
* @param status - New opportunity status
|
|
1197
1199
|
* @param acceptedBy - Required when `status === 'accepted'`
|
|
1200
|
+
* @param outbox - Optional IND-434 atomic outcome-capture (same-txn insert)
|
|
1198
1201
|
* @returns The updated opportunity, or null if not found
|
|
1199
1202
|
*/
|
|
1200
|
-
stampOpportunityActorAction(id: string, actorUserId: string, status: OpportunityStatus, acceptedBy?: string): Promise<Opportunity | null>;
|
|
1203
|
+
stampOpportunityActorAction(id: string, actorUserId: string, status: OpportunityStatus, acceptedBy?: string, outbox?: OutcomeOutbox): Promise<Opportunity | null>;
|
|
1201
1204
|
/**
|
|
1202
1205
|
* Update the `approved` field on an opportunity's introducer actor.
|
|
1203
1206
|
* Fetches the opportunity, patches the matching actor in JS, and writes
|
|
@@ -1951,6 +1954,29 @@ export type NegotiationGraphDatabase = Pick<Database, 'getOrCreateDM' | 'getUser
|
|
|
1951
1954
|
*
|
|
1952
1955
|
* Access layer: Both UserDatabase + SystemDatabase (API handles auth)
|
|
1953
1956
|
*/
|
|
1957
|
+
/**
|
|
1958
|
+
* Optional atomic outbox for Lens B outcome capture (IND-434). Passed to a
|
|
1959
|
+
* winning owner-action transition so the append-only outcome event is written
|
|
1960
|
+
* in the SAME transaction as the status change:
|
|
1961
|
+
* - a rolled-back action leaves NO event;
|
|
1962
|
+
* - a committed eligible action produces EXACTLY one event;
|
|
1963
|
+
* - `result.inserted` is set to true by the adapter only when a NEW row was
|
|
1964
|
+
* written (idempotent retries / duplicates set it false), so the caller can
|
|
1965
|
+
* gate post-commit mining on a genuine first insert.
|
|
1966
|
+
*
|
|
1967
|
+
* `event` is typed `unknown` (the api-side outcome-event insert row, cast by the
|
|
1968
|
+
* adapter) to keep the protocol layer free of database-schema imports. The
|
|
1969
|
+
* actor-resolution mode is a transaction-time precondition: selected-intent
|
|
1970
|
+
* captures require that exact actor intent, while unscoped captures require the
|
|
1971
|
+
* recipient to still have one unambiguous actor-intent scope.
|
|
1972
|
+
*/
|
|
1973
|
+
export interface OutcomeOutbox {
|
|
1974
|
+
event: unknown;
|
|
1975
|
+
actorResolution: 'selected_intent' | 'unique_owned_scope';
|
|
1976
|
+
result: {
|
|
1977
|
+
inserted: boolean;
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1954
1980
|
export type OpportunityControllerDatabase = Pick<Database, 'getOpportunity' | 'getOpportunitiesByIds' | 'findEnrichedReplacementOpportunities' | 'getOpportunitiesForUser' | 'getOpportunitiesForNetwork' | 'resolveOpportunityId' | 'updateOpportunityStatus' | 'createOpportunity' | 'createOpportunityAndExpireIds' | 'opportunityExistsBetweenActors' | 'findOpportunitiesByActors' | 'acceptSiblingOpportunities' | 'isIndexOwner' | 'isNetworkMember' | 'getUser' | 'getNetwork' | 'getNetworkMemberships' | 'getProfile' | 'getActiveIntents' | 'upsertContactMembership' | 'getOrCreateDM' | 'unhideConversation' | 'updateOpportunityActorApproval' | 'stampOpportunityActorAction'>;
|
|
1955
1981
|
/**
|
|
1956
1982
|
* Database interface narrowed for Intent Graph operations.
|