@indexnetwork/protocol 4.16.0-rc.358.1 → 4.17.0-rc.360.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 +8 -1
- package/dist/contact/contact.inviter.d.ts.map +1 -1
- package/dist/contact/contact.inviter.js +12 -5
- package/dist/contact/contact.inviter.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/opportunity/delivery-card.cache.d.ts.map +1 -1
- package/dist/opportunity/delivery-card.cache.js +7 -3
- package/dist/opportunity/delivery-card.cache.js.map +1 -1
- package/dist/opportunity/discriminator/discriminator.adjustments.d.ts +22 -15
- package/dist/opportunity/discriminator/discriminator.adjustments.d.ts.map +1 -1
- package/dist/opportunity/discriminator/discriminator.adjustments.js +39 -20
- package/dist/opportunity/discriminator/discriminator.adjustments.js.map +1 -1
- package/dist/opportunity/feed/feed.categorizer.d.ts.map +1 -1
- package/dist/opportunity/feed/feed.categorizer.js +6 -0
- package/dist/opportunity/feed/feed.categorizer.js.map +1 -1
- package/dist/opportunity/feed/feed.graph.d.ts +2 -0
- package/dist/opportunity/feed/feed.graph.d.ts.map +1 -1
- package/dist/opportunity/feed/feed.graph.js +53 -28
- package/dist/opportunity/feed/feed.graph.js.map +1 -1
- package/dist/opportunity/feed/feed.state.d.ts +3 -1
- package/dist/opportunity/feed/feed.state.d.ts.map +1 -1
- package/dist/opportunity/feed/feed.state.js.map +1 -1
- package/dist/opportunity/opportunity.claim-safety.d.ts +25 -0
- package/dist/opportunity/opportunity.claim-safety.d.ts.map +1 -0
- package/dist/opportunity/opportunity.claim-safety.js +89 -0
- package/dist/opportunity/opportunity.claim-safety.js.map +1 -0
- package/dist/opportunity/opportunity.discover.d.ts.map +1 -1
- package/dist/opportunity/opportunity.discover.js +6 -1
- package/dist/opportunity/opportunity.discover.js.map +1 -1
- package/dist/opportunity/opportunity.evaluator.d.ts.map +1 -1
- package/dist/opportunity/opportunity.evaluator.js +13 -8
- package/dist/opportunity/opportunity.evaluator.js.map +1 -1
- package/dist/opportunity/opportunity.graph.d.ts +23 -0
- package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
- package/dist/opportunity/opportunity.graph.js +172 -17
- package/dist/opportunity/opportunity.graph.js.map +1 -1
- package/dist/opportunity/opportunity.persist.d.ts +8 -1
- package/dist/opportunity/opportunity.persist.d.ts.map +1 -1
- package/dist/opportunity/opportunity.persist.js +23 -2
- package/dist/opportunity/opportunity.persist.js.map +1 -1
- package/dist/opportunity/opportunity.presentation-cache.d.ts +7 -0
- package/dist/opportunity/opportunity.presentation-cache.d.ts.map +1 -0
- package/dist/opportunity/opportunity.presentation-cache.js +15 -0
- package/dist/opportunity/opportunity.presentation-cache.js.map +1 -0
- package/dist/opportunity/opportunity.presentation.d.ts.map +1 -1
- package/dist/opportunity/opportunity.presentation.js +8 -5
- package/dist/opportunity/opportunity.presentation.js.map +1 -1
- package/dist/opportunity/opportunity.presenter.d.ts +4 -4
- package/dist/opportunity/opportunity.presenter.d.ts.map +1 -1
- package/dist/opportunity/opportunity.presenter.js +55 -14
- package/dist/opportunity/opportunity.presenter.js.map +1 -1
- package/dist/opportunity/opportunity.safe-presentation.d.ts +1 -1
- package/dist/opportunity/opportunity.safe-presentation.d.ts.map +1 -1
- package/dist/opportunity/opportunity.safe-presentation.js +22 -9
- package/dist/opportunity/opportunity.safe-presentation.js.map +1 -1
- package/dist/opportunity/opportunity.tools.d.ts +1 -17
- package/dist/opportunity/opportunity.tools.d.ts.map +1 -1
- package/dist/opportunity/opportunity.tools.js +32 -2
- package/dist/opportunity/opportunity.tools.js.map +1 -1
- package/dist/shared/interfaces/database.interface.d.ts +46 -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
|
@@ -16,29 +16,36 @@
|
|
|
16
16
|
* reasoning (opportunity-presentation-safety).
|
|
17
17
|
*/
|
|
18
18
|
import { POOL_ADJUSTMENT_FACTOR_OTHER, POOL_ADJUSTMENT_FACTOR_UNKNOWN, POOL_ADJUSTMENT_FLOOR } from "./discriminator.env.js";
|
|
19
|
-
/** Reads
|
|
20
|
-
export function readPoolAdjustments(metadata) {
|
|
19
|
+
/** Reads valid adjustments, optionally narrowed to one recipient + intent. */
|
|
20
|
+
export function readPoolAdjustments(metadata, provenance) {
|
|
21
21
|
const raw = metadata?.poolAdjustments;
|
|
22
22
|
if (!Array.isArray(raw))
|
|
23
23
|
return [];
|
|
24
|
-
return raw.filter((a) =>
|
|
25
|
-
typeof a
|
|
26
|
-
|
|
24
|
+
return raw.filter((a) => {
|
|
25
|
+
if (typeof a !== "object" || a === null ||
|
|
26
|
+
typeof a.questionId !== "string" ||
|
|
27
|
+
typeof a.recipientUserId !== "string" ||
|
|
28
|
+
typeof a.intentId !== "string" ||
|
|
29
|
+
typeof a.factor !== "number")
|
|
30
|
+
return false;
|
|
31
|
+
return provenance === undefined || (a.recipientUserId === provenance.recipientUserId &&
|
|
32
|
+
a.intentId === provenance.intentId);
|
|
33
|
+
});
|
|
27
34
|
}
|
|
28
35
|
/**
|
|
29
36
|
* Cumulative adjustment multiplier for an opportunity, floored at
|
|
30
|
-
* {@link POOL_ADJUSTMENT_FLOOR}. 1 when no adjustments exist.
|
|
37
|
+
* {@link POOL_ADJUSTMENT_FLOOR}. 1 when no scoped adjustments exist.
|
|
31
38
|
*/
|
|
32
|
-
export function poolAdjustmentMultiplier(metadata) {
|
|
33
|
-
const adjustments = readPoolAdjustments(metadata);
|
|
39
|
+
export function poolAdjustmentMultiplier(metadata, provenance) {
|
|
40
|
+
const adjustments = readPoolAdjustments(metadata, provenance);
|
|
34
41
|
if (adjustments.length === 0)
|
|
35
42
|
return 1;
|
|
36
43
|
const product = adjustments.reduce((acc, a) => acc * (Number.isFinite(a.factor) && a.factor > 0 ? a.factor : 1), 1);
|
|
37
44
|
return Math.max(POOL_ADJUSTMENT_FLOOR, product);
|
|
38
45
|
}
|
|
39
46
|
/** Adjusted confidence: `confidence × Π factor`, floored. */
|
|
40
|
-
export function adjustedConfidence(confidence, metadata) {
|
|
41
|
-
return confidence * poolAdjustmentMultiplier(metadata);
|
|
47
|
+
export function adjustedConfidence(confidence, metadata, provenance) {
|
|
48
|
+
return confidence * poolAdjustmentMultiplier(metadata, provenance);
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
51
|
* Build one P3-compatible adjustment and signal. This is the only place that
|
|
@@ -53,6 +60,8 @@ export function buildPoolAdjustment(input) {
|
|
|
53
60
|
return {
|
|
54
61
|
adjustment: {
|
|
55
62
|
questionId: input.questionId,
|
|
63
|
+
recipientUserId: input.recipientUserId,
|
|
64
|
+
intentId: input.intentId,
|
|
56
65
|
label: input.label,
|
|
57
66
|
side,
|
|
58
67
|
factor,
|
|
@@ -62,6 +71,8 @@ export function buildPoolAdjustment(input) {
|
|
|
62
71
|
signal: {
|
|
63
72
|
type: "pool_discriminator",
|
|
64
73
|
weight,
|
|
74
|
+
recipientUserId: input.recipientUserId,
|
|
75
|
+
intentId: input.intentId,
|
|
65
76
|
detail: isUnknown ? `${input.label}: unassigned` : `${input.label}: ${input.chosenSide}`,
|
|
66
77
|
questionId: input.questionId,
|
|
67
78
|
},
|
|
@@ -75,9 +86,11 @@ export function buildPoolAdjustment(input) {
|
|
|
75
86
|
* @param discriminator The asked discriminator (from detection.pool).
|
|
76
87
|
* @param chosenSide The selected option label (chip label = side label).
|
|
77
88
|
* @param questionId Reversal key.
|
|
89
|
+
* @param recipientUserId User whose answer produced the preference.
|
|
90
|
+
* @param intentId Intent whose candidate pool was ranked.
|
|
78
91
|
* @param now ISO-8601 timestamp.
|
|
79
92
|
*/
|
|
80
|
-
export function planPoolAdjustments(discriminator, chosenSide, questionId, now) {
|
|
93
|
+
export function planPoolAdjustments(discriminator, chosenSide, questionId, recipientUserId, intentId, now) {
|
|
81
94
|
// Chip labels are word-capped side labels; match on the capped prefix too.
|
|
82
95
|
const matchesSide = (side) => side === chosenSide || side.startsWith(chosenSide) || chosenSide.startsWith(side);
|
|
83
96
|
const chosen = discriminator.sides.find(matchesSide);
|
|
@@ -89,6 +102,8 @@ export function planPoolAdjustments(discriminator, chosenSide, questionId, now)
|
|
|
89
102
|
opportunityId: a.opportunityId,
|
|
90
103
|
...buildPoolAdjustment({
|
|
91
104
|
questionId,
|
|
105
|
+
recipientUserId,
|
|
106
|
+
intentId,
|
|
92
107
|
label: discriminator.label,
|
|
93
108
|
assignedSide: a.side,
|
|
94
109
|
chosenSide: chosen,
|
|
@@ -100,23 +115,27 @@ export function planPoolAdjustments(discriminator, chosenSide, questionId, now)
|
|
|
100
115
|
}
|
|
101
116
|
/**
|
|
102
117
|
* Merges one adjustment into an opportunity's existing metadata, replacing
|
|
103
|
-
*
|
|
104
|
-
* NEW metadata object (caller persists it wholesale).
|
|
118
|
+
* only the entry for the same question + recipient + intent provenance.
|
|
119
|
+
* Returns the NEW metadata object (caller persists it wholesale).
|
|
105
120
|
*/
|
|
106
121
|
export function mergePoolAdjustment(metadata, adjustment) {
|
|
107
|
-
const existing = readPoolAdjustments(metadata).filter((a) => a.questionId
|
|
122
|
+
const existing = readPoolAdjustments(metadata).filter((a) => !(a.questionId === adjustment.questionId &&
|
|
123
|
+
a.recipientUserId === adjustment.recipientUserId &&
|
|
124
|
+
a.intentId === adjustment.intentId));
|
|
108
125
|
return { ...(metadata ?? {}), poolAdjustments: [...existing, adjustment] };
|
|
109
126
|
}
|
|
110
|
-
/** Latest user-explainable demotion detail for card presentation
|
|
111
|
-
export function latestPoolDemotionDetail(metadata) {
|
|
112
|
-
return [...readPoolAdjustments(metadata)]
|
|
127
|
+
/** Latest scoped user-explainable demotion detail for card presentation. */
|
|
128
|
+
export function latestPoolDemotionDetail(metadata, provenance) {
|
|
129
|
+
return [...readPoolAdjustments(metadata, provenance)]
|
|
113
130
|
.reverse()
|
|
114
131
|
.find((adjustment) => adjustment.factor < 1 && typeof adjustment.detail === 'string' && adjustment.detail.length > 0)
|
|
115
132
|
?.detail;
|
|
116
133
|
}
|
|
117
|
-
/** Removes
|
|
118
|
-
export function removePoolAdjustment(metadata, questionId) {
|
|
119
|
-
const remaining = readPoolAdjustments(metadata).filter((a) => a.questionId
|
|
134
|
+
/** Removes one question adjustment for exact recipient + intent provenance. */
|
|
135
|
+
export function removePoolAdjustment(metadata, questionId, provenance) {
|
|
136
|
+
const remaining = readPoolAdjustments(metadata).filter((a) => !(a.questionId === questionId &&
|
|
137
|
+
a.recipientUserId === provenance.recipientUserId &&
|
|
138
|
+
a.intentId === provenance.intentId));
|
|
120
139
|
return { ...(metadata ?? {}), poolAdjustments: remaining };
|
|
121
140
|
}
|
|
122
141
|
//# sourceMappingURL=discriminator.adjustments.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discriminator.adjustments.js","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.adjustments.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pool-adjustment domain logic (IND-419) — pure functions, no I/O.\n *\n * When a user answers a pool_discovery question, every candidate the mining\n * pass assigned to a side gets a multiplicative adjustment written to\n * `opportunities.metadata.poolAdjustments`. The home feed (flag-gated) orders\n * by `confidence × Π factor`, floored so demoted candidates stay visible.\n *\n * Invariants:\n * - Deterministic and O(pool): no LLM at answer time — the applyPlan was\n * computed (and evidence-verified) at mining time.\n * - Reversible: entries are keyed by questionId; re-answering replaces them,\n * dismissal-driven reversal removes them.\n * - `detail` carries the template chip text built from the user's OWN answer\n * (\"Hands-on builders vs advisors: you chose Advisor\") — never LLM\n * reasoning (opportunity-presentation-safety).\n */\nimport { POOL_ADJUSTMENT_FACTOR_OTHER, POOL_ADJUSTMENT_FACTOR_UNKNOWN, POOL_ADJUSTMENT_FLOOR } from \"./discriminator.env.js\";\nimport type { QuestionPoolDiscriminator } from \"../../shared/schemas/question.schema.js\";\n\n/** One applied adjustment on an opportunity (stored in metadata.poolAdjustments). */\nexport interface PoolAdjustment {\n /** The answered question that produced this adjustment (reversal key). */\n questionId: string;\n /** Discriminator label, e.g. \"Hands-on builders vs advisors\". */\n label: string;\n /** Side this candidate was assigned to (or \"unknown\"). */\n side: string;\n /** Multiplicative factor: chosen 1.0, other 0.6, unknown 0.9. */\n factor: number;\n /** Template chip text from the user's own answer. Set on demotions only. */\n detail?: string;\n /** ISO-8601 apply timestamp. */\n appliedAt: string;\n}\n\n/** Reads the adjustments array off an opportunity's metadata (tolerant). */\nexport function readPoolAdjustments(metadata: Record<string, unknown> | null | undefined): PoolAdjustment[] {\n const raw = metadata?.poolAdjustments;\n if (!Array.isArray(raw)) return [];\n return raw.filter((a): a is PoolAdjustment =>\n typeof a === \"object\" && a !== null &&\n typeof (a as PoolAdjustment).questionId === \"string\" &&\n typeof (a as PoolAdjustment).factor === \"number\");\n}\n\n/**\n * Cumulative adjustment multiplier for an opportunity, floored at\n * {@link POOL_ADJUSTMENT_FLOOR}. 1 when no adjustments exist.\n */\nexport function poolAdjustmentMultiplier(metadata: Record<string, unknown> | null | undefined): number {\n const adjustments = readPoolAdjustments(metadata);\n if (adjustments.length === 0) return 1;\n const product = adjustments.reduce((acc, a) => acc * (Number.isFinite(a.factor) && a.factor > 0 ? a.factor : 1), 1);\n return Math.max(POOL_ADJUSTMENT_FLOOR, product);\n}\n\n/** Adjusted confidence: `confidence × Π factor`, floored. */\nexport function adjustedConfidence(confidence: number, metadata: Record<string, unknown> | null | undefined): number {\n return confidence * poolAdjustmentMultiplier(metadata);\n}\n\n/** Deterministic provenance signal stored alongside one adjustment. */\nexport interface PoolAdjustmentSignal {\n type: \"pool_discriminator\";\n weight: 1 | -1 | 0;\n detail: string;\n questionId: string;\n}\n\n/** Pure helper input shared by Tier-0 answers and newborn stamping. */\nexport interface BuildPoolAdjustmentInput {\n questionId: string;\n label: string;\n /** Verified side assignment, or null when the candidate is unassigned. */\n assignedSide: string | null;\n chosenSide: string;\n appliedAt: string;\n}\n\n/**\n * Build one P3-compatible adjustment and signal. This is the only place that\n * defines chosen/other/unknown factors, weights, and safe template details.\n */\nexport function buildPoolAdjustment(input: BuildPoolAdjustmentInput): {\n adjustment: PoolAdjustment;\n signal: PoolAdjustmentSignal;\n} {\n const isUnknown = input.assignedSide === null;\n const isChosen = !isUnknown && input.assignedSide === input.chosenSide;\n const factor = isUnknown ? POOL_ADJUSTMENT_FACTOR_UNKNOWN : isChosen ? 1 : POOL_ADJUSTMENT_FACTOR_OTHER;\n const weight = isUnknown ? 0 : isChosen ? 1 : -1;\n const side = input.assignedSide ?? \"unknown\";\n return {\n adjustment: {\n questionId: input.questionId,\n label: input.label,\n side,\n factor,\n ...(!isChosen && !isUnknown ? { detail: `${input.label}: you chose ${input.chosenSide}` } : {}),\n appliedAt: input.appliedAt,\n },\n signal: {\n type: \"pool_discriminator\",\n weight,\n detail: isUnknown ? `${input.label}: unassigned` : `${input.label}: ${input.chosenSide}`,\n questionId: input.questionId,\n },\n };\n}\n\n/** Plan entry: what to write on one opportunity for one answer. */\nexport interface PoolAdjustmentPlanEntry {\n opportunityId: string;\n adjustment: PoolAdjustment;\n signal: PoolAdjustmentSignal;\n}\n\n/**\n * Computes the write plan for one answered discriminator. Pure: the caller\n * loads/patches rows. \"Both matter\" (or any label not in `sides`) yields an\n * empty plan — no preference, no adjustments.\n *\n * @param discriminator The asked discriminator (from detection.pool).\n * @param chosenSide The selected option label (chip label = side label).\n * @param questionId Reversal key.\n * @param now ISO-8601 timestamp.\n */\nexport function planPoolAdjustments(\n discriminator: QuestionPoolDiscriminator,\n chosenSide: string,\n questionId: string,\n now: string,\n): PoolAdjustmentPlanEntry[] {\n // Chip labels are word-capped side labels; match on the capped prefix too.\n const matchesSide = (side: string): boolean =>\n side === chosenSide || side.startsWith(chosenSide) || chosenSide.startsWith(side);\n const chosen = discriminator.sides.find(matchesSide);\n if (!chosen) return []; // \"Both matter\" or unrecognized option → no adjustments.\n\n const plan: PoolAdjustmentPlanEntry[] = [];\n for (const a of discriminator.assignments) {\n plan.push({\n opportunityId: a.opportunityId,\n ...buildPoolAdjustment({\n questionId,\n label: discriminator.label,\n assignedSide: a.side,\n chosenSide: chosen,\n appliedAt: now,\n }),\n });\n }\n return plan;\n}\n\n/**\n * Merges one adjustment into an opportunity's existing metadata, replacing\n * any prior entry for the same questionId (re-answer semantics). Returns the\n * NEW metadata object (caller persists it wholesale).\n */\nexport function mergePoolAdjustment(\n metadata: Record<string, unknown> | null | undefined,\n adjustment: PoolAdjustment,\n): Record<string, unknown> {\n const existing = readPoolAdjustments(metadata).filter((a) => a.questionId !== adjustment.questionId);\n return { ...(metadata ?? {}), poolAdjustments: [...existing, adjustment] };\n}\n\n/** Latest user-explainable demotion detail for card presentation, if any. */\nexport function latestPoolDemotionDetail(\n metadata: Record<string, unknown> | null | undefined,\n): string | undefined {\n return [...readPoolAdjustments(metadata)]\n .reverse()\n .find((adjustment) => adjustment.factor < 1 && typeof adjustment.detail === 'string' && adjustment.detail.length > 0)\n ?.detail;\n}\n\n/** Removes all adjustments for a questionId (dismissal/withdrawal reversal). */\nexport function removePoolAdjustment(\n metadata: Record<string, unknown> | null | undefined,\n questionId: string,\n): Record<string, unknown> {\n const remaining = readPoolAdjustments(metadata).filter((a) => a.questionId !== questionId);\n return { ...(metadata ?? {}), poolAdjustments: remaining };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"discriminator.adjustments.js","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.adjustments.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pool-adjustment domain logic (IND-419) — pure functions, no I/O.\n *\n * When a user answers a pool_discovery question, every candidate the mining\n * pass assigned to a side gets a multiplicative adjustment written to\n * `opportunities.metadata.poolAdjustments`. The home feed (flag-gated) orders\n * by `confidence × Π factor`, floored so demoted candidates stay visible.\n *\n * Invariants:\n * - Deterministic and O(pool): no LLM at answer time — the applyPlan was\n * computed (and evidence-verified) at mining time.\n * - Reversible: entries are keyed by questionId; re-answering replaces them,\n * dismissal-driven reversal removes them.\n * - `detail` carries the template chip text built from the user's OWN answer\n * (\"Hands-on builders vs advisors: you chose Advisor\") — never LLM\n * reasoning (opportunity-presentation-safety).\n */\nimport { POOL_ADJUSTMENT_FACTOR_OTHER, POOL_ADJUSTMENT_FACTOR_UNKNOWN, POOL_ADJUSTMENT_FLOOR } from \"./discriminator.env.js\";\nimport type { QuestionPoolDiscriminator } from \"../../shared/schemas/question.schema.js\";\n\n/** Recipient and intent provenance that scopes one pool preference. */\nexport interface PoolAdjustmentProvenance {\n recipientUserId: string;\n intentId: string;\n}\n\n/** One applied adjustment on an opportunity (stored in metadata.poolAdjustments). */\nexport interface PoolAdjustment extends PoolAdjustmentProvenance {\n /** The answered question that produced this adjustment (reversal key). */\n questionId: string;\n /** Discriminator label, e.g. \"Hands-on builders vs advisors\". */\n label: string;\n /** Side this candidate was assigned to (or \"unknown\"). */\n side: string;\n /** Multiplicative factor: chosen 1.0, other 0.6, unknown 0.9. */\n factor: number;\n /** Template chip text from the user's own answer. Set on demotions only. */\n detail?: string;\n /** ISO-8601 apply timestamp. */\n appliedAt: string;\n}\n\n/** Reads valid adjustments, optionally narrowed to one recipient + intent. */\nexport function readPoolAdjustments(\n metadata: Record<string, unknown> | null | undefined,\n provenance?: PoolAdjustmentProvenance,\n): PoolAdjustment[] {\n const raw = metadata?.poolAdjustments;\n if (!Array.isArray(raw)) return [];\n return raw.filter((a): a is PoolAdjustment => {\n if (\n typeof a !== \"object\" || a === null ||\n typeof (a as PoolAdjustment).questionId !== \"string\" ||\n typeof (a as PoolAdjustment).recipientUserId !== \"string\" ||\n typeof (a as PoolAdjustment).intentId !== \"string\" ||\n typeof (a as PoolAdjustment).factor !== \"number\"\n ) return false;\n return provenance === undefined || (\n (a as PoolAdjustment).recipientUserId === provenance.recipientUserId &&\n (a as PoolAdjustment).intentId === provenance.intentId\n );\n });\n}\n\n/**\n * Cumulative adjustment multiplier for an opportunity, floored at\n * {@link POOL_ADJUSTMENT_FLOOR}. 1 when no scoped adjustments exist.\n */\nexport function poolAdjustmentMultiplier(\n metadata: Record<string, unknown> | null | undefined,\n provenance: PoolAdjustmentProvenance,\n): number {\n const adjustments = readPoolAdjustments(metadata, provenance);\n if (adjustments.length === 0) return 1;\n const product = adjustments.reduce((acc, a) => acc * (Number.isFinite(a.factor) && a.factor > 0 ? a.factor : 1), 1);\n return Math.max(POOL_ADJUSTMENT_FLOOR, product);\n}\n\n/** Adjusted confidence: `confidence × Π factor`, floored. */\nexport function adjustedConfidence(\n confidence: number,\n metadata: Record<string, unknown> | null | undefined,\n provenance: PoolAdjustmentProvenance,\n): number {\n return confidence * poolAdjustmentMultiplier(metadata, provenance);\n}\n\n/** Deterministic provenance signal stored alongside one adjustment. */\nexport interface PoolAdjustmentSignal extends PoolAdjustmentProvenance {\n type: \"pool_discriminator\";\n weight: 1 | -1 | 0;\n detail: string;\n questionId: string;\n}\n\n/** Pure helper input shared by Tier-0 answers and newborn stamping. */\nexport interface BuildPoolAdjustmentInput extends PoolAdjustmentProvenance {\n questionId: string;\n label: string;\n /** Verified side assignment, or null when the candidate is unassigned. */\n assignedSide: string | null;\n chosenSide: string;\n appliedAt: string;\n}\n\n/**\n * Build one P3-compatible adjustment and signal. This is the only place that\n * defines chosen/other/unknown factors, weights, and safe template details.\n */\nexport function buildPoolAdjustment(input: BuildPoolAdjustmentInput): {\n adjustment: PoolAdjustment;\n signal: PoolAdjustmentSignal;\n} {\n const isUnknown = input.assignedSide === null;\n const isChosen = !isUnknown && input.assignedSide === input.chosenSide;\n const factor = isUnknown ? POOL_ADJUSTMENT_FACTOR_UNKNOWN : isChosen ? 1 : POOL_ADJUSTMENT_FACTOR_OTHER;\n const weight = isUnknown ? 0 : isChosen ? 1 : -1;\n const side = input.assignedSide ?? \"unknown\";\n return {\n adjustment: {\n questionId: input.questionId,\n recipientUserId: input.recipientUserId,\n intentId: input.intentId,\n label: input.label,\n side,\n factor,\n ...(!isChosen && !isUnknown ? { detail: `${input.label}: you chose ${input.chosenSide}` } : {}),\n appliedAt: input.appliedAt,\n },\n signal: {\n type: \"pool_discriminator\",\n weight,\n recipientUserId: input.recipientUserId,\n intentId: input.intentId,\n detail: isUnknown ? `${input.label}: unassigned` : `${input.label}: ${input.chosenSide}`,\n questionId: input.questionId,\n },\n };\n}\n\n/** Plan entry: what to write on one opportunity for one answer. */\nexport interface PoolAdjustmentPlanEntry {\n opportunityId: string;\n adjustment: PoolAdjustment;\n signal: PoolAdjustmentSignal;\n}\n\n/**\n * Computes the write plan for one answered discriminator. Pure: the caller\n * loads/patches rows. \"Both matter\" (or any label not in `sides`) yields an\n * empty plan — no preference, no adjustments.\n *\n * @param discriminator The asked discriminator (from detection.pool).\n * @param chosenSide The selected option label (chip label = side label).\n * @param questionId Reversal key.\n * @param recipientUserId User whose answer produced the preference.\n * @param intentId Intent whose candidate pool was ranked.\n * @param now ISO-8601 timestamp.\n */\nexport function planPoolAdjustments(\n discriminator: QuestionPoolDiscriminator,\n chosenSide: string,\n questionId: string,\n recipientUserId: string,\n intentId: string,\n now: string,\n): PoolAdjustmentPlanEntry[] {\n // Chip labels are word-capped side labels; match on the capped prefix too.\n const matchesSide = (side: string): boolean =>\n side === chosenSide || side.startsWith(chosenSide) || chosenSide.startsWith(side);\n const chosen = discriminator.sides.find(matchesSide);\n if (!chosen) return []; // \"Both matter\" or unrecognized option → no adjustments.\n\n const plan: PoolAdjustmentPlanEntry[] = [];\n for (const a of discriminator.assignments) {\n plan.push({\n opportunityId: a.opportunityId,\n ...buildPoolAdjustment({\n questionId,\n recipientUserId,\n intentId,\n label: discriminator.label,\n assignedSide: a.side,\n chosenSide: chosen,\n appliedAt: now,\n }),\n });\n }\n return plan;\n}\n\n/**\n * Merges one adjustment into an opportunity's existing metadata, replacing\n * only the entry for the same question + recipient + intent provenance.\n * Returns the NEW metadata object (caller persists it wholesale).\n */\nexport function mergePoolAdjustment(\n metadata: Record<string, unknown> | null | undefined,\n adjustment: PoolAdjustment,\n): Record<string, unknown> {\n const existing = readPoolAdjustments(metadata).filter((a) => !(\n a.questionId === adjustment.questionId &&\n a.recipientUserId === adjustment.recipientUserId &&\n a.intentId === adjustment.intentId\n ));\n return { ...(metadata ?? {}), poolAdjustments: [...existing, adjustment] };\n}\n\n/** Latest scoped user-explainable demotion detail for card presentation. */\nexport function latestPoolDemotionDetail(\n metadata: Record<string, unknown> | null | undefined,\n provenance: PoolAdjustmentProvenance,\n): string | undefined {\n return [...readPoolAdjustments(metadata, provenance)]\n .reverse()\n .find((adjustment) => adjustment.factor < 1 && typeof adjustment.detail === 'string' && adjustment.detail.length > 0)\n ?.detail;\n}\n\n/** Removes one question adjustment for exact recipient + intent provenance. */\nexport function removePoolAdjustment(\n metadata: Record<string, unknown> | null | undefined,\n questionId: string,\n provenance: PoolAdjustmentProvenance,\n): Record<string, unknown> {\n const remaining = readPoolAdjustments(metadata).filter((a) => !(\n a.questionId === questionId &&\n a.recipientUserId === provenance.recipientUserId &&\n a.intentId === provenance.intentId\n ));\n return { ...(metadata ?? {}), poolAdjustments: remaining };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feed.categorizer.d.ts","sourceRoot":"/","sources":["opportunity/feed/feed.categorizer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"feed.categorizer.d.ts","sourceRoot":"/","sources":["opportunity/feed/feed.categorizer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AA0B3D,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC,CAAC;AAgFF,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,KAAK,CAA2C;;IAMxD;;OAEG;IAEG,UAAU,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC;CA0D5E"}
|
|
@@ -21,6 +21,7 @@ import { protocolLogger } from '../../shared/observability/protocol.logger.js';
|
|
|
21
21
|
import { Timed } from "../../shared/observability/performance.js";
|
|
22
22
|
import { createStructuredModel } from "../../shared/agent/model.config.js";
|
|
23
23
|
import { invokeWithAbortSignal } from "../../shared/agent/model-signal.js";
|
|
24
|
+
import { hasUnsupportedOpportunityClaim } from '../opportunity.claim-safety.js';
|
|
24
25
|
const logger = protocolLogger('HomeCategorizer');
|
|
25
26
|
const categorizationSchema = z.object({
|
|
26
27
|
sections: z.array(z.object({
|
|
@@ -144,6 +145,11 @@ Rules:
|
|
|
144
145
|
iconName: s.iconName,
|
|
145
146
|
itemIndices: s.itemIndices.filter((i) => i >= 0 && i <= maxIndex),
|
|
146
147
|
}));
|
|
148
|
+
if (sections.some((section) => hasUnsupportedOpportunityClaim(section.title)
|
|
149
|
+
|| hasUnsupportedOpportunityClaim(section.subtitle))) {
|
|
150
|
+
logger.warn('HomeCategorizer emitted unsupported affiliation/presence claim; using fallback');
|
|
151
|
+
return buildFallbackResult(cards);
|
|
152
|
+
}
|
|
147
153
|
const reconciledSections = reconcileSections(sections, maxIndex);
|
|
148
154
|
return { sections: reconciledSections };
|
|
149
155
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feed.categorizer.js","sourceRoot":"/","sources":["opportunity/feed/feed.categorizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Home Categorizer Agent\n *\n * Takes a list of presenter-produced opportunity cards and returns dynamic sections\n * with CTA-style titles and Lucide icon names. Used by the home graph after\n * generateCardText.\n */\n\nimport { HumanMessage, SystemMessage } from '@langchain/core/messages';\nimport { z } from 'zod';\n\nimport type { HomeSectionProposal } from './feed.state.js';\nimport { getIconNamesForPrompt, DEFAULT_HOME_SECTION_ICON } from '../../shared/ui/lucide.icon-catalog.js';\nimport { protocolLogger } from '../../shared/observability/protocol.logger.js';\nimport { Timed } from \"../../shared/observability/performance.js\";\nimport { createStructuredModel } from \"../../shared/agent/model.config.js\";\nimport { invokeWithAbortSignal } from \"../../shared/agent/model-signal.js\";\n\nconst logger = protocolLogger('HomeCategorizer');\n\nconst categorizationSchema = z.object({\n sections: z.array(\n z.object({\n id: z.string().describe('Short kebab-case id for the section'),\n title: z\n .string()\n .describe(\n 'Call-to-action style section heading (e.g. REACH OUT NOW, YOUR MOVE, CONNECTIONS WAITING FOR YOU)'\n ),\n subtitle: z.string().nullable().describe('Optional one-line description; use null if none'),\n iconName: z.string().describe('Lucide icon name in kebab-case from the allowed list'),\n itemIndices: z.array(z.number()).describe('Indices of cards in this section (0-based)'),\n })\n ),\n});\n\nexport type CategorizerInputItem = {\n index: number;\n headline?: string;\n mainText: string;\n name: string;\n viewerRole?: string;\n opportunityStatus?: string;\n};\n\nexport type CategorizerResult = {\n sections: HomeSectionProposal[];\n};\n\nconst UNCATEGORIZED_SECTION_ID = 'uncategorized';\nconst UNCATEGORIZED_SECTION_TITLE = 'UNCATEGORIZED OPPORTUNITIES';\n\n/** Single-section fallback grouping every card under one generic heading. */\nfunction buildFallbackResult(cards: CategorizerInputItem[]): CategorizerResult {\n return {\n sections: [\n {\n id: 'opportunities',\n title: 'OPPORTUNITIES',\n iconName: DEFAULT_HOME_SECTION_ICON,\n itemIndices: cards.map((c) => c.index),\n },\n ],\n };\n}\n\n/**\n * Build card summaries for the categorizer prompt.\n */\nfunction buildCardSummaries(cards: CategorizerInputItem[]): string {\n return cards\n .map((c) => {\n const role = c.viewerRole ?? 'party';\n const status = c.opportunityStatus ?? 'pending';\n return `[${c.index}] role=${role}; status=${status}; ${c.headline ?? c.mainText.slice(0, 60)}... (${c.name})`;\n })\n .join('\\n');\n}\n\nfunction reconcileSections(sections: HomeSectionProposal[], maxIndex: number): HomeSectionProposal[] {\n const assigned = new Set<number>();\n\n const normalizedSections = sections.map((section) => {\n const uniqueIndices = Array.from(\n new Set(section.itemIndices.filter((index) => index >= 0 && index <= maxIndex))\n ).sort((a, b) => a - b);\n\n for (const index of uniqueIndices) {\n assigned.add(index);\n }\n\n return {\n ...section,\n itemIndices: uniqueIndices,\n };\n });\n\n const missingIndices: number[] = [];\n for (let index = 0; index <= maxIndex; index += 1) {\n if (!assigned.has(index)) {\n missingIndices.push(index);\n }\n }\n\n if (missingIndices.length === 0) {\n return normalizedSections;\n }\n\n const fallbackSection = normalizedSections.find((section) => section.id === UNCATEGORIZED_SECTION_ID);\n if (fallbackSection) {\n fallbackSection.itemIndices = Array.from(\n new Set([...fallbackSection.itemIndices, ...missingIndices])\n ).sort((a, b) => a - b);\n return normalizedSections;\n }\n\n return [\n ...normalizedSections,\n {\n id: UNCATEGORIZED_SECTION_ID,\n title: UNCATEGORIZED_SECTION_TITLE,\n iconName: DEFAULT_HOME_SECTION_ICON,\n itemIndices: missingIndices,\n },\n ];\n}\n\nexport class HomeCategorizerAgent {\n private model: ReturnType<typeof createStructuredModel>;\n\n constructor() {\n this.model = createStructuredModel(\"homeCategorizer\", categorizationSchema, { name: 'home_sections' });\n }\n\n /**\n * Categorize presenter-produced cards into 1–5 sections with CTA-style titles and icons.\n */\n @Timed()\n async categorize(cards: CategorizerInputItem[]): Promise<CategorizerResult> {\n if (cards.length === 0) {\n return { sections: [] };\n }\n const iconList = getIconNamesForPrompt();\n const cardSummaries = buildCardSummaries(cards);\n const maxIndex = cards.length - 1;\n\n const systemPrompt = `You are organizing connection opportunities into dynamic sections for a user's home feed.\n\nGiven a list of opportunity cards (each with index, headline/summary, and name), group them into 1–5 sections.\n\nEach section must have:\n- id: short kebab-case identifier (e.g. waiting-for-action, your-perspective, connector)\n- title: CALL-TO-ACTION style section heading in uppercase. Write as an action or invitation, not a neutral label. Examples: REACH OUT NOW, YOUR MOVE, CONNECTIONS WAITING FOR YOU, INTROS READY TO SEND, PEOPLE YOU SHOULD MEET\n- subtitle: optional one-line description\n- iconName: exactly ONE icon from this list (use the name as-is): ${iconList}\n- itemIndices: array of card indices that belong in this section (each index 0 to ${maxIndex} at most once)\n\nRules:\n- Use only icon names from the list above.\n- Every card index must appear in exactly one section.\n- Section titles must read as calls-to-action or invitations, not passive labels.\n- If a card has role=introducer and status=pending, prioritize grouping those cards under a connector curation section with a CTA title like \"YOU'RE THE CONNECTOR THEY NEED\", \"MAKE THE INTRO\", or \"DECIDE IF IT'S A GOOD MATCH\".`;\n\n const userContent = `Cards to categorize:\\n${cardSummaries}\\n\\nOutput sections with id, title (CTA-style), subtitle (optional), iconName, and itemIndices.`;\n\n try {\n const result = await invokeWithAbortSignal(this.model, [\n new SystemMessage(systemPrompt),\n new HumanMessage(userContent),\n ]);\n const parsed = categorizationSchema.safeParse(result);\n if (!parsed.success) {\n logger.warn('HomeCategorizer parse failed', { error: parsed.error });\n return buildFallbackResult(cards);\n }\n const sections: HomeSectionProposal[] = parsed.data.sections.map((s) => ({\n id: s.id,\n title: s.title,\n subtitle: s.subtitle ?? undefined,\n iconName: s.iconName,\n itemIndices: s.itemIndices.filter((i) => i >= 0 && i <= maxIndex),\n }));\n const reconciledSections = reconcileSections(sections, maxIndex);\n return { sections: reconciledSections };\n } catch (e) {\n const err = e instanceof Error ? { message: e.message, name: e.name } : String(e);\n logger.error('HomeCategorizer categorize failed', { error: err });\n return buildFallbackResult(cards);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"feed.categorizer.js","sourceRoot":"/","sources":["opportunity/feed/feed.categorizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Home Categorizer Agent\n *\n * Takes a list of presenter-produced opportunity cards and returns dynamic sections\n * with CTA-style titles and Lucide icon names. Used by the home graph after\n * generateCardText.\n */\n\nimport { HumanMessage, SystemMessage } from '@langchain/core/messages';\nimport { z } from 'zod';\n\nimport type { HomeSectionProposal } from './feed.state.js';\nimport { getIconNamesForPrompt, DEFAULT_HOME_SECTION_ICON } from '../../shared/ui/lucide.icon-catalog.js';\nimport { protocolLogger } from '../../shared/observability/protocol.logger.js';\nimport { Timed } from \"../../shared/observability/performance.js\";\nimport { createStructuredModel } from \"../../shared/agent/model.config.js\";\nimport { invokeWithAbortSignal } from \"../../shared/agent/model-signal.js\";\nimport { hasUnsupportedOpportunityClaim } from '../opportunity.claim-safety.js';\n\nconst logger = protocolLogger('HomeCategorizer');\n\nconst categorizationSchema = z.object({\n sections: z.array(\n z.object({\n id: z.string().describe('Short kebab-case id for the section'),\n title: z\n .string()\n .describe(\n 'Call-to-action style section heading (e.g. REACH OUT NOW, YOUR MOVE, CONNECTIONS WAITING FOR YOU)'\n ),\n subtitle: z.string().nullable().describe('Optional one-line description; use null if none'),\n iconName: z.string().describe('Lucide icon name in kebab-case from the allowed list'),\n itemIndices: z.array(z.number()).describe('Indices of cards in this section (0-based)'),\n })\n ),\n});\n\nexport type CategorizerInputItem = {\n index: number;\n headline?: string;\n mainText: string;\n name: string;\n viewerRole?: string;\n opportunityStatus?: string;\n};\n\nexport type CategorizerResult = {\n sections: HomeSectionProposal[];\n};\n\nconst UNCATEGORIZED_SECTION_ID = 'uncategorized';\nconst UNCATEGORIZED_SECTION_TITLE = 'UNCATEGORIZED OPPORTUNITIES';\n\n/** Single-section fallback grouping every card under one generic heading. */\nfunction buildFallbackResult(cards: CategorizerInputItem[]): CategorizerResult {\n return {\n sections: [\n {\n id: 'opportunities',\n title: 'OPPORTUNITIES',\n iconName: DEFAULT_HOME_SECTION_ICON,\n itemIndices: cards.map((c) => c.index),\n },\n ],\n };\n}\n\n/**\n * Build card summaries for the categorizer prompt.\n */\nfunction buildCardSummaries(cards: CategorizerInputItem[]): string {\n return cards\n .map((c) => {\n const role = c.viewerRole ?? 'party';\n const status = c.opportunityStatus ?? 'pending';\n return `[${c.index}] role=${role}; status=${status}; ${c.headline ?? c.mainText.slice(0, 60)}... (${c.name})`;\n })\n .join('\\n');\n}\n\nfunction reconcileSections(sections: HomeSectionProposal[], maxIndex: number): HomeSectionProposal[] {\n const assigned = new Set<number>();\n\n const normalizedSections = sections.map((section) => {\n const uniqueIndices = Array.from(\n new Set(section.itemIndices.filter((index) => index >= 0 && index <= maxIndex))\n ).sort((a, b) => a - b);\n\n for (const index of uniqueIndices) {\n assigned.add(index);\n }\n\n return {\n ...section,\n itemIndices: uniqueIndices,\n };\n });\n\n const missingIndices: number[] = [];\n for (let index = 0; index <= maxIndex; index += 1) {\n if (!assigned.has(index)) {\n missingIndices.push(index);\n }\n }\n\n if (missingIndices.length === 0) {\n return normalizedSections;\n }\n\n const fallbackSection = normalizedSections.find((section) => section.id === UNCATEGORIZED_SECTION_ID);\n if (fallbackSection) {\n fallbackSection.itemIndices = Array.from(\n new Set([...fallbackSection.itemIndices, ...missingIndices])\n ).sort((a, b) => a - b);\n return normalizedSections;\n }\n\n return [\n ...normalizedSections,\n {\n id: UNCATEGORIZED_SECTION_ID,\n title: UNCATEGORIZED_SECTION_TITLE,\n iconName: DEFAULT_HOME_SECTION_ICON,\n itemIndices: missingIndices,\n },\n ];\n}\n\nexport class HomeCategorizerAgent {\n private model: ReturnType<typeof createStructuredModel>;\n\n constructor() {\n this.model = createStructuredModel(\"homeCategorizer\", categorizationSchema, { name: 'home_sections' });\n }\n\n /**\n * Categorize presenter-produced cards into 1–5 sections with CTA-style titles and icons.\n */\n @Timed()\n async categorize(cards: CategorizerInputItem[]): Promise<CategorizerResult> {\n if (cards.length === 0) {\n return { sections: [] };\n }\n const iconList = getIconNamesForPrompt();\n const cardSummaries = buildCardSummaries(cards);\n const maxIndex = cards.length - 1;\n\n const systemPrompt = `You are organizing connection opportunities into dynamic sections for a user's home feed.\n\nGiven a list of opportunity cards (each with index, headline/summary, and name), group them into 1–5 sections.\n\nEach section must have:\n- id: short kebab-case identifier (e.g. waiting-for-action, your-perspective, connector)\n- title: CALL-TO-ACTION style section heading in uppercase. Write as an action or invitation, not a neutral label. Examples: REACH OUT NOW, YOUR MOVE, CONNECTIONS WAITING FOR YOU, INTROS READY TO SEND, PEOPLE YOU SHOULD MEET\n- subtitle: optional one-line description\n- iconName: exactly ONE icon from this list (use the name as-is): ${iconList}\n- itemIndices: array of card indices that belong in this section (each index 0 to ${maxIndex} at most once)\n\nRules:\n- Use only icon names from the list above.\n- Every card index must appear in exactly one section.\n- Section titles must read as calls-to-action or invitations, not passive labels.\n- If a card has role=introducer and status=pending, prioritize grouping those cards under a connector curation section with a CTA title like \"YOU'RE THE CONNECTOR THEY NEED\", \"MAKE THE INTRO\", or \"DECIDE IF IT'S A GOOD MATCH\".`;\n\n const userContent = `Cards to categorize:\\n${cardSummaries}\\n\\nOutput sections with id, title (CTA-style), subtitle (optional), iconName, and itemIndices.`;\n\n try {\n const result = await invokeWithAbortSignal(this.model, [\n new SystemMessage(systemPrompt),\n new HumanMessage(userContent),\n ]);\n const parsed = categorizationSchema.safeParse(result);\n if (!parsed.success) {\n logger.warn('HomeCategorizer parse failed', { error: parsed.error });\n return buildFallbackResult(cards);\n }\n const sections: HomeSectionProposal[] = parsed.data.sections.map((s) => ({\n id: s.id,\n title: s.title,\n subtitle: s.subtitle ?? undefined,\n iconName: s.iconName,\n itemIndices: s.itemIndices.filter((i) => i >= 0 && i <= maxIndex),\n }));\n if (sections.some((section) =>\n hasUnsupportedOpportunityClaim(section.title)\n || hasUnsupportedOpportunityClaim(section.subtitle))) {\n logger.warn('HomeCategorizer emitted unsupported affiliation/presence claim; using fallback');\n return buildFallbackResult(cards);\n }\n const reconciledSections = reconcileSections(sections, maxIndex);\n return { sections: reconciledSections };\n } catch (e) {\n const err = e instanceof Error ? { message: e.message, name: e.name } : String(e);\n logger.error('HomeCategorizer categorize failed', { error: err });\n return buildFallbackResult(cards);\n }\n }\n}\n"]}
|
|
@@ -48,6 +48,8 @@ export type HomeGraphInvokeResult = {
|
|
|
48
48
|
export declare const DEFAULT_HOME_STATUSES: OpportunityStatus[];
|
|
49
49
|
/** Full status enumeration. Pass this to `HomeGraphInvokeInput.statuses` to restore pre-Issue-3 (unfiltered) behavior. */
|
|
50
50
|
export declare const ALL_OPPORTUNITY_STATUSES: OpportunityStatus[];
|
|
51
|
+
/** Pure cache policy for presenter cards; degraded current-request copy retries later. */
|
|
52
|
+
export declare function isHomePresentationCacheable(card: Pick<HomeCardItem, 'presentationPending' | '_presentationFallback' | 'name'>, status: OpportunityStatus | undefined): boolean;
|
|
51
53
|
/**
|
|
52
54
|
* Strip leading narrator name from remark when the UI already prepends "Name: " to the chip.
|
|
53
55
|
* Avoids duplication like "Yankı Ekin Yüksel: Yankı Ekin Yüksel introduced you two..."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feed.graph.d.ts","sourceRoot":"/","sources":["opportunity/feed/feed.graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,+CAA+C,CAAC;AAC1G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EAAkB,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,mBAAmB,EAAwB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"feed.graph.d.ts","sourceRoot":"/","sources":["opportunity/feed/feed.graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,+CAA+C,CAAC;AAC1G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EAAkB,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,mBAAmB,EAAwB,MAAM,iBAAiB,CAAC;AAStI,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAiBzE,8HAA8H;AAC9H,KAAK,WAAW,GAAG,iBAAiB,CAAC;AAErC,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,QAAQ,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,IAAI,EAAE;QAAE,kBAAkB,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,qBAAqB,EAAE,iBAAiB,EAA0B,CAAC;AAgBhF,0HAA0H;AAC1H,eAAO,MAAM,wBAAwB,EAAE,iBAAiB,EAEhC,CAAC;AAOzB,0FAA0F;AAC1F,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,qBAAqB,GAAG,uBAAuB,GAAG,MAAM,CAAC,EAClF,MAAM,EAAE,iBAAiB,GAAG,SAAS,GACpC,OAAO,CAST;AASD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAarF;AAmHD,qBAAa,gBAAgB;IACf,OAAO,CAAC,QAAQ;IAAe,OAAO,CAAC,KAAK;gBAApC,QAAQ,EAAE,WAAW,EAAU,KAAK,EAAE,gBAAgB;IAE1E,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyrBZ"}
|
|
@@ -18,10 +18,11 @@ import { canUserSeeOpportunity, isActionableForViewer, selectByComposition } fro
|
|
|
18
18
|
import { resolveHomeSectionIcon, DEFAULT_HOME_SECTION_ICON } from '../../shared/ui/lucide.icon-catalog.js';
|
|
19
19
|
import { getPrimaryActionLabel, SECONDARY_ACTION_LABEL } from '../opportunity.labels.js';
|
|
20
20
|
import { safeFallbackSummary } from '../opportunity.safe-presentation.js';
|
|
21
|
+
import { buildHomeCardPresentationCacheKey, buildHomeCategoryPresentationCacheKey } from '../opportunity.presentation-cache.js';
|
|
21
22
|
import { protocolLogger } from '../../shared/observability/protocol.logger.js';
|
|
22
23
|
import { timed } from '../../shared/observability/performance.js';
|
|
23
24
|
import { requestContext } from "../../shared/observability/request-context.js";
|
|
24
|
-
import { adjustedConfidence, latestPoolDemotionDetail } from '../discriminator/discriminator.adjustments.js';
|
|
25
|
+
import { adjustedConfidence, latestPoolDemotionDetail, readPoolAdjustments } from '../discriminator/discriminator.adjustments.js';
|
|
25
26
|
import { poolQuestionsRanking } from '../discriminator/discriminator.env.js';
|
|
26
27
|
const logger = protocolLogger('HomeGraph');
|
|
27
28
|
const checkCategorizerCacheLog = protocolLogger('HomeGraph:checkCategorizerCache');
|
|
@@ -52,11 +53,20 @@ const MAX_ITEMS_PER_SECTION = 20;
|
|
|
52
53
|
const PRESENTATION_CONCURRENCY = 50;
|
|
53
54
|
const MAX_REASONING_SNIPPET_LENGTH = 240;
|
|
54
55
|
const HOME_CACHE_TTL = 24 * 60 * 60; // 24 hours in seconds
|
|
56
|
+
/** Pure cache policy for presenter cards; degraded current-request copy retries later. */
|
|
57
|
+
export function isHomePresentationCacheable(card, status) {
|
|
58
|
+
return Boolean(status &&
|
|
59
|
+
status !== 'negotiating' &&
|
|
60
|
+
!card.presentationPending &&
|
|
61
|
+
!card._presentationFallback &&
|
|
62
|
+
card.name &&
|
|
63
|
+
card.name !== 'Unknown');
|
|
64
|
+
}
|
|
55
65
|
/** Redis key for the categorizer cache, derived from the user and the ordered opportunity-id set. */
|
|
56
66
|
function buildCategorizerCacheKey(userId, cards) {
|
|
57
67
|
const oppIds = cards.map((c) => c.opportunityId).join(',');
|
|
58
68
|
const hash = createHash('sha256').update(oppIds).digest('hex').slice(0, 16);
|
|
59
|
-
return
|
|
69
|
+
return buildHomeCategoryPresentationCacheKey(userId, hash);
|
|
60
70
|
}
|
|
61
71
|
/**
|
|
62
72
|
* Strip leading narrator name from remark when the UI already prepends "Name: " to the chip.
|
|
@@ -122,11 +132,20 @@ const getRawConfidence = (opp) => {
|
|
|
122
132
|
* their stored factors (floor 0.3) so the user's answers re-rank the feed.
|
|
123
133
|
* Flag off → identical to raw confidence (adjustments are write-only).
|
|
124
134
|
*/
|
|
125
|
-
const
|
|
135
|
+
const getPoolRankingProvenance = (state) => {
|
|
136
|
+
if (poolQuestionsRanking() !== 'on' ||
|
|
137
|
+
!state.userId ||
|
|
138
|
+
state.scopeType !== 'intent' ||
|
|
139
|
+
!state.scopeId)
|
|
140
|
+
return null;
|
|
141
|
+
return { recipientUserId: state.userId, intentId: state.scopeId };
|
|
142
|
+
};
|
|
143
|
+
const hasPoolAdjustment = (opp, provenance) => readPoolAdjustments(opp.metadata, provenance).length > 0;
|
|
144
|
+
const getConfidence = (opp, provenance) => {
|
|
126
145
|
const raw = getRawConfidence(opp);
|
|
127
|
-
if (
|
|
146
|
+
if (!provenance)
|
|
128
147
|
return raw;
|
|
129
|
-
return adjustedConfidence(raw, opp.metadata);
|
|
148
|
+
return adjustedConfidence(raw, opp.metadata, provenance);
|
|
130
149
|
};
|
|
131
150
|
/** Unique non-introducer, non-viewer userIds for an opportunity (actors can repeat). */
|
|
132
151
|
const getUniqueCounterpartUserIds = (opp, viewerId) => {
|
|
@@ -178,6 +197,7 @@ export class HomeGraphFactory {
|
|
|
178
197
|
// its soft targets, even after visibility filtering and dedup.
|
|
179
198
|
const fetchLimit = Math.min(150, Math.max(50, state.limit * 3));
|
|
180
199
|
const statuses = state.statuses ?? DEFAULT_HOME_STATUSES;
|
|
200
|
+
const poolRankingProvenance = getPoolRankingProvenance(state);
|
|
181
201
|
const options = {
|
|
182
202
|
limit: fetchLimit,
|
|
183
203
|
statuses,
|
|
@@ -229,11 +249,12 @@ export class HomeGraphFactory {
|
|
|
229
249
|
// off. When on, re-order the already-deduped lifecycle set by
|
|
230
250
|
// adjusted confidence so each client-side radar bucket reflects
|
|
231
251
|
// pool answers immediately.
|
|
232
|
-
if (
|
|
252
|
+
if (!poolRankingProvenance ||
|
|
253
|
+
!dedupedByCounterpart.some((opportunity) => hasPoolAdjustment(opportunity, poolRankingProvenance))) {
|
|
233
254
|
return { opportunities: dedupedByCounterpart.slice(0, state.limit) };
|
|
234
255
|
}
|
|
235
256
|
const adjustedOrder = [...dedupedByCounterpart].sort((a, b) => {
|
|
236
|
-
const confidenceDelta = getConfidence(b) - getConfidence(a);
|
|
257
|
+
const confidenceDelta = getConfidence(b, poolRankingProvenance) - getConfidence(a, poolRankingProvenance);
|
|
237
258
|
if (confidenceDelta !== 0)
|
|
238
259
|
return confidenceDelta;
|
|
239
260
|
return safeParseDate(b.updatedAt) - safeParseDate(a.updatedAt);
|
|
@@ -248,8 +269,8 @@ export class HomeGraphFactory {
|
|
|
248
269
|
const bIsIntroducer = b.actors.some((ac) => ac.userId === state.userId && ac.role === 'introducer');
|
|
249
270
|
if (aIsIntroducer !== bIsIntroducer)
|
|
250
271
|
return aIsIntroducer ? 1 : -1;
|
|
251
|
-
const confA = getConfidence(a);
|
|
252
|
-
const confB = getConfidence(b);
|
|
272
|
+
const confA = getConfidence(a, poolRankingProvenance);
|
|
273
|
+
const confB = getConfidence(b, poolRankingProvenance);
|
|
253
274
|
if (confB !== confA)
|
|
254
275
|
return confB - confA;
|
|
255
276
|
const aTime = safeParseDate(a.updatedAt);
|
|
@@ -278,6 +299,7 @@ export class HomeGraphFactory {
|
|
|
278
299
|
const checkPresenterCacheNode = async (state) => {
|
|
279
300
|
return timed("HomeGraph.checkPresenterCache", async () => {
|
|
280
301
|
const { opportunities, userId } = state;
|
|
302
|
+
const poolRankingProvenance = getPoolRankingProvenance(state);
|
|
281
303
|
if (opportunities.length === 0) {
|
|
282
304
|
return { cachedCards: new Map(), uncachedOpportunities: [] };
|
|
283
305
|
}
|
|
@@ -295,7 +317,7 @@ export class HomeGraphFactory {
|
|
|
295
317
|
// transitions (e.g. negotiating → pending) don't serve stale cards.
|
|
296
318
|
const cacheable = opportunities.filter((opp) => opp.status !== 'negotiating');
|
|
297
319
|
const liveNegotiating = opportunities.filter((opp) => opp.status === 'negotiating');
|
|
298
|
-
const keys = cacheable.map((opp) =>
|
|
320
|
+
const keys = cacheable.map((opp) => buildHomeCardPresentationCacheKey(opp.id, opp.status, userId));
|
|
299
321
|
const results = keys.length > 0 ? await this.cache.mget(keys) : [];
|
|
300
322
|
const cachedCards = new Map();
|
|
301
323
|
const uncachedOpportunities = [...liveNegotiating];
|
|
@@ -305,8 +327,8 @@ export class HomeGraphFactory {
|
|
|
305
327
|
const originalIndex = opportunities.indexOf(cacheable[i]);
|
|
306
328
|
// Stamp the live status: pre-status cache entries lack the field,
|
|
307
329
|
// and the key already guarantees it matches the current status.
|
|
308
|
-
const deprioritizedReason =
|
|
309
|
-
? latestPoolDemotionDetail(cacheable[i].metadata)
|
|
330
|
+
const deprioritizedReason = poolRankingProvenance
|
|
331
|
+
? latestPoolDemotionDetail(cacheable[i].metadata, poolRankingProvenance)
|
|
310
332
|
: undefined;
|
|
311
333
|
cachedCards.set(cacheable[i].id, {
|
|
312
334
|
...cached,
|
|
@@ -494,6 +516,7 @@ export class HomeGraphFactory {
|
|
|
494
516
|
viewerRole,
|
|
495
517
|
isGhost: isCounterpartGhost,
|
|
496
518
|
...(secondPartyData ? { secondParty: secondPartyData } : {}),
|
|
519
|
+
_presentationFallback: true,
|
|
497
520
|
_cardIndex: cardIndex,
|
|
498
521
|
});
|
|
499
522
|
try {
|
|
@@ -514,6 +537,9 @@ export class HomeGraphFactory {
|
|
|
514
537
|
const _presenterDuration = Date.now() - presenterStart;
|
|
515
538
|
agentTimingsAccum.push({ name: 'opportunity.presenter', durationMs: _presenterDuration });
|
|
516
539
|
_traceEmitterPresenter?.({ type: "agent_end", name: "opportunity-presenter", durationMs: _presenterDuration, summary: `Presented: ${userName}` });
|
|
540
|
+
if (presentation.isFallback) {
|
|
541
|
+
return fallbackCard();
|
|
542
|
+
}
|
|
517
543
|
let narratorChip;
|
|
518
544
|
// Only show a person as narrator when they are the introducer and not the display counterpart
|
|
519
545
|
// (bad data can have same user as introducer and party, e.g. "Amina introduced you to Amina")
|
|
@@ -571,11 +597,12 @@ export class HomeGraphFactory {
|
|
|
571
597
|
const cachePresenterResultsNode = async (state) => {
|
|
572
598
|
return timed("HomeGraph.cachePresenterResults", async () => {
|
|
573
599
|
const { cards, cachedCards, userId, opportunities } = state;
|
|
600
|
+
const poolRankingProvenance = getPoolRankingProvenance(state);
|
|
574
601
|
const liveById = new Map(opportunities.map((opportunity) => [opportunity.id, opportunity]));
|
|
575
602
|
const cardsWithAdjustments = cards.map((card) => {
|
|
576
603
|
const opportunity = liveById.get(card.opportunityId);
|
|
577
|
-
const deprioritizedReason =
|
|
578
|
-
? latestPoolDemotionDetail(opportunity?.metadata)
|
|
604
|
+
const deprioritizedReason = poolRankingProvenance
|
|
605
|
+
? latestPoolDemotionDetail(opportunity?.metadata, poolRankingProvenance)
|
|
579
606
|
: undefined;
|
|
580
607
|
return { ...card, deprioritizedReason };
|
|
581
608
|
});
|
|
@@ -585,18 +612,11 @@ export class HomeGraphFactory {
|
|
|
585
612
|
try {
|
|
586
613
|
await Promise.all(newCards.map((card) => {
|
|
587
614
|
const status = statusById.get(card.opportunityId);
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
// Never cache skeleton cards — they carry no presenter text and
|
|
592
|
-
// would otherwise be served as "complete" for the full TTL.
|
|
593
|
-
if (card.presentationPending)
|
|
594
|
-
return Promise.resolve();
|
|
595
|
-
// Skip caching cards with unresolved names — transient DB failures
|
|
596
|
-
// would persist "Unknown" placeholders for the full TTL.
|
|
597
|
-
if (!card.name || card.name === 'Unknown')
|
|
615
|
+
// Negotiating, skeleton, fallback, and unresolved-name cards are
|
|
616
|
+
// safe for the current response but must not become 24h entries.
|
|
617
|
+
if (!status || !isHomePresentationCacheable(card, status))
|
|
598
618
|
return Promise.resolve();
|
|
599
|
-
return this.cache.set(
|
|
619
|
+
return this.cache.set(buildHomeCardPresentationCacheKey(card.opportunityId, status, userId), card, { ttl: HOME_CACHE_TTL });
|
|
600
620
|
}));
|
|
601
621
|
}
|
|
602
622
|
catch (e) {
|
|
@@ -632,7 +652,10 @@ export class HomeGraphFactory {
|
|
|
632
652
|
// status, so dynamic categorization would erase the adjusted order on
|
|
633
653
|
// the full second-phase response. Flag off retains the legacy path.
|
|
634
654
|
// Chunk by MAX_ITEMS_PER_SECTION because normalizeAndSort caps sections.
|
|
635
|
-
const
|
|
655
|
+
const poolRankingProvenance = getPoolRankingProvenance(state);
|
|
656
|
+
const preserveAdjustedLifecycleOrder = (state.statuses?.length ?? 0) > 0 &&
|
|
657
|
+
poolRankingProvenance !== null &&
|
|
658
|
+
state.opportunities.some((opportunity) => hasPoolAdjustment(opportunity, poolRankingProvenance));
|
|
636
659
|
if (state.presentation === 'skeleton' || preserveAdjustedLifecycleOrder) {
|
|
637
660
|
const sectionProposals = [];
|
|
638
661
|
for (let start = 0; start < state.cards.length; start += MAX_ITEMS_PER_SECTION) {
|
|
@@ -704,7 +727,9 @@ export class HomeGraphFactory {
|
|
|
704
727
|
};
|
|
705
728
|
const cacheCategorizerResultsNode = async (state) => {
|
|
706
729
|
return timed("HomeGraph.cacheCategorizerResults", async () => {
|
|
707
|
-
if (state.categoryCacheHit ||
|
|
730
|
+
if (state.categoryCacheHit ||
|
|
731
|
+
state.sectionProposals.length === 0 ||
|
|
732
|
+
state.cards.some((card) => card._presentationFallback)) {
|
|
708
733
|
return {};
|
|
709
734
|
}
|
|
710
735
|
try {
|
|
@@ -738,7 +763,7 @@ export class HomeGraphFactory {
|
|
|
738
763
|
.map((i) => {
|
|
739
764
|
usedIndices.add(i);
|
|
740
765
|
const card = cards[i];
|
|
741
|
-
const { _cardIndex, ...rest } = card;
|
|
766
|
+
const { _cardIndex, _presentationFallback, ...rest } = card;
|
|
742
767
|
return rest;
|
|
743
768
|
});
|
|
744
769
|
return {
|