@indexnetwork/protocol 66.0.0-rc.608.1 → 66.0.0-rc.610.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/IMPLEMENTATION.md +11 -12
  3. package/STABILITY.md +0 -1
  4. package/dist/capabilities/intents.d.ts +7 -3
  5. package/dist/capabilities/intents.js +10 -0
  6. package/dist/index.d.ts +6 -5
  7. package/dist/index.js +3 -3
  8. package/dist/internal/intents/graph/intent.graph.reconcile.js +1 -1
  9. package/dist/internal/intents/intent.admission.d.ts +22 -1
  10. package/dist/internal/intents/intent.admission.js +29 -8
  11. package/dist/internal/intents/intent.preparer.d.ts +29 -4
  12. package/dist/internal/intents/intent.preparer.js +125 -18
  13. package/dist/internal/opportunities/opportunity.cards.d.ts +78 -0
  14. package/dist/internal/opportunities/opportunity.cards.js +165 -0
  15. package/dist/internal/opportunities/opportunity.presentation.d.ts +2 -2
  16. package/dist/internal/opportunities/opportunity.presentation.js +3 -3
  17. package/dist/internal/opportunities/opportunity.utils.d.ts +0 -42
  18. package/dist/internal/opportunities/opportunity.utils.js +0 -102
  19. package/dist/internal/shared/agent/decision.client.d.ts +42 -0
  20. package/dist/internal/shared/agent/decision.client.js +61 -0
  21. package/dist/platform/database/capabilities.d.ts +2 -2
  22. package/dist/platform/database/entities.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/dist/internal/opportunities/opportunity.feed-selection.d.ts +0 -24
  25. package/dist/internal/opportunities/opportunity.feed-selection.js +0 -40
  26. package/dist/internal/opportunities/radar/radar.graph.d.ts +0 -230
  27. package/dist/internal/opportunities/radar/radar.graph.js +0 -506
  28. package/dist/internal/opportunities/radar/radar.state.d.ts +0 -86
  29. package/dist/internal/opportunities/radar/radar.state.js +0 -87
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Opportunity cards: presenter-texted cards for a viewer, as a list or one at
3
+ * a time. The list loads, filters, and dedupes the viewer's opportunities,
4
+ * serves cached cards, and presents the misses through OpportunityPresenter.
5
+ */
6
+ import { protocolLogger } from '../shared/observability/protocol.logger.js';
7
+ import { buildOpportunityCardCacheKey, gatherPresenterContext } from './opportunity.presentation.js';
8
+ import { loadNegotiationContext } from './negotiation-context.loader.js';
9
+ import { canUserSeeOpportunity, isActionableForViewer } from './opportunity.utils.js';
10
+ import { getPrimaryActionLabel, SECONDARY_ACTION_LABEL } from './opportunity.labels.js';
11
+ const logger = protocolLogger('OpportunityCards');
12
+ const PRESENTATION_CONCURRENCY = 50;
13
+ const CARD_CACHE_TTL = 24 * 60 * 60;
14
+ const ROLE_PRIORITY = new Map([
15
+ ['patient', 0],
16
+ ['party', 1],
17
+ ['agent', 2],
18
+ ['peer', 3],
19
+ ]);
20
+ /** Prefer direct counterpart roles, then a stable sort by user id. */
21
+ function pickDisplayCounterpartActor(opportunity, viewerId) {
22
+ const candidates = opportunity.actors.filter((actor) => actor.userId !== viewerId);
23
+ return [...candidates].sort((a, b) => {
24
+ const aPriority = ROLE_PRIORITY.get(a.role) ?? 99;
25
+ const bPriority = ROLE_PRIORITY.get(b.role) ?? 99;
26
+ if (aPriority !== bPriority)
27
+ return aPriority - bPriority;
28
+ return a.userId.localeCompare(b.userId);
29
+ })[0] ?? null;
30
+ }
31
+ function counterpartUserIds(opportunity, viewerId) {
32
+ return new Set(opportunity.actors.map((a) => a.userId).filter((id) => id && id !== viewerId));
33
+ }
34
+ /**
35
+ * Present one opportunity as a card for the viewer.
36
+ *
37
+ * @param deps - Database, cache, and presenter.
38
+ * @param opportunity - The opportunity to present.
39
+ * @param viewerId - The viewing user.
40
+ * @param options - `intentId` focuses viewer context on one intent; `skeleton` skips the presenter.
41
+ * @returns The card, or null when the counterpart's name cannot be resolved.
42
+ * @throws When the presenter fails.
43
+ */
44
+ export async function presentOpportunityCard(deps, opportunity, viewerId, options = {}) {
45
+ const { database } = deps;
46
+ const viewerRole = opportunity.actors.find((a) => a.userId === viewerId)?.role ?? 'party';
47
+ const counterpart = pickDisplayCounterpartActor(opportunity, viewerId);
48
+ if (!counterpart)
49
+ return null;
50
+ const user = await database.getUser(counterpart.userId).catch(() => null);
51
+ let name = user?.name?.trim();
52
+ if (!name) {
53
+ const profile = await database.getProfile(counterpart.userId).catch(() => null);
54
+ name = profile?.identity?.name?.trim();
55
+ }
56
+ if (!name)
57
+ return null;
58
+ const identity = {
59
+ opportunityId: opportunity.id,
60
+ createdAt: opportunity.createdAt.toISOString(),
61
+ status: opportunity.status,
62
+ userId: counterpart.userId,
63
+ name,
64
+ avatar: user?.avatar ?? null,
65
+ primaryActionLabel: getPrimaryActionLabel(viewerRole),
66
+ secondaryActionLabel: SECONDARY_ACTION_LABEL,
67
+ viewerRole,
68
+ };
69
+ if (options.skeleton) {
70
+ return { ...identity, mainText: '', cta: '', mutualIntentsLabel: 'Shared interests', presentationPending: true };
71
+ }
72
+ const [context, negotiationContext] = await Promise.all([
73
+ gatherPresenterContext(database, opportunity, viewerId, counterpart.userId, options.intentId),
74
+ loadNegotiationContext(database, opportunity.id, opportunity.status, viewerId),
75
+ ]);
76
+ const presentation = await deps.presenter.presentCard({
77
+ ...context,
78
+ opportunityStatus: opportunity.status,
79
+ ...(negotiationContext ? { negotiationContext } : {}),
80
+ });
81
+ return {
82
+ ...identity,
83
+ mainText: presentation.personalizedSummary,
84
+ cta: presentation.suggestedAction,
85
+ headline: presentation.headline,
86
+ mutualIntentsLabel: presentation.mutualIntentsLabel,
87
+ narratorChip: { name: 'Index', text: presentation.narratorRemark },
88
+ };
89
+ }
90
+ /**
91
+ * List the viewer's opportunities in the given statuses as presented cards,
92
+ * newest first, one card per counterpart. Cards whose presentation fails are
93
+ * dropped.
94
+ *
95
+ * @param deps - Database, cache, and presenter.
96
+ * @param input - Viewer, statuses, optional network/intent scope, limit, cache bypass, skeleton mode.
97
+ * @returns The cards and the number of opportunities selected.
98
+ */
99
+ export async function listOpportunityCards(deps, input) {
100
+ const { viewerId, intentId } = input;
101
+ const raw = await deps.database.getOpportunitiesForUser(viewerId, {
102
+ limit: Math.min(150, Math.max(50, input.limit * 3)),
103
+ statuses: input.statuses,
104
+ ...(input.networkId ? { networkId: input.networkId } : {}),
105
+ ...(intentId ? { scopeType: 'intent', scopeId: intentId } : {}),
106
+ });
107
+ const requested = new Set(input.statuses);
108
+ const visible = raw
109
+ .filter((opp) => requested.has(opp.status) && canUserSeeOpportunity(opp.actors, opp.status, viewerId))
110
+ .filter((opp) => opp.status !== 'pending'
111
+ || isActionableForViewer(opp.actors, opp.status, viewerId, opp.committedActorIds ?? []))
112
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
113
+ const seen = new Set();
114
+ const opportunities = visible.filter((opp) => {
115
+ const ids = counterpartUserIds(opp, viewerId);
116
+ if ([...ids].some((id) => seen.has(id)))
117
+ return false;
118
+ for (const id of ids)
119
+ seen.add(id);
120
+ return true;
121
+ }).slice(0, input.limit);
122
+ const cacheable = (opp) => opp.status !== 'negotiating';
123
+ const keyFor = (opp) => buildOpportunityCardCacheKey(opp.id, opp.status, viewerId, intentId);
124
+ const cacheableOpps = input.noCache ? [] : opportunities.filter(cacheable);
125
+ const cached = new Map();
126
+ if (cacheableOpps.length > 0) {
127
+ try {
128
+ const hits = await deps.cache.mget(cacheableOpps.map(keyFor));
129
+ cacheableOpps.forEach((opp, i) => {
130
+ const hit = hits[i];
131
+ if (hit)
132
+ cached.set(opp.id, { ...hit, status: opp.status });
133
+ });
134
+ }
135
+ catch (error) {
136
+ logger.warn('card cache read failed, presenting all', { error });
137
+ }
138
+ }
139
+ const cards = [];
140
+ for (let i = 0; i < opportunities.length; i += PRESENTATION_CONCURRENCY) {
141
+ const chunk = opportunities.slice(i, i + PRESENTATION_CONCURRENCY);
142
+ cards.push(...await Promise.all(chunk.map(async (opp) => {
143
+ const hit = cached.get(opp.id);
144
+ if (hit)
145
+ return hit;
146
+ try {
147
+ const card = await presentOpportunityCard(deps, opp, viewerId, { intentId, skeleton: input.skeleton });
148
+ if (card && !card.presentationPending && cacheable(opp)) {
149
+ await deps.cache.set(keyFor(opp), card, { ttl: CARD_CACHE_TTL }).catch((error) => {
150
+ logger.warn('card cache write failed', { opportunityId: opp.id, error });
151
+ });
152
+ }
153
+ return card;
154
+ }
155
+ catch (error) {
156
+ logger.warn('presenter failed, dropping card', { opportunityId: opp.id, error });
157
+ return null;
158
+ }
159
+ })));
160
+ }
161
+ return {
162
+ cards: cards.filter((card) => card !== null),
163
+ totalOpportunities: opportunities.length,
164
+ };
165
+ }
@@ -20,7 +20,7 @@ export interface UserInfo {
20
20
  }
21
21
  /** Cache namespace for opportunity presentation copy. Bump to invalidate copy safety changes. */
22
22
  export declare const OPPORTUNITY_PRESENTATION_CACHE_VERSION = "v2";
23
- export declare function buildRadarCardPresentationCacheKey(opportunityId: string, status: string, viewerId: string, focusedViewerIntentId?: string): string;
23
+ export declare function buildOpportunityCardCacheKey(opportunityId: string, status: string, viewerId: string, focusedViewerIntentId?: string): string;
24
24
  export declare function buildApiChatCardPresentationCacheKey(opportunityId: string, viewerId: string): string;
25
25
  /**
26
26
  * Opportunity Presenter Agent
@@ -159,7 +159,7 @@ export declare function summarizeSignalsForPresenter(signals: Opportunity['inter
159
159
  * Gather all context needed for the presenter from the database.
160
160
  * Fetches viewer profile, viewer intents, other party profile(s), and network in parallel.
161
161
  *
162
- * @param displayCounterpartUserId - When set (e.g. for a radar card), only this counterpart is included in otherPartyContext so the presenter writes about the person on the card.
162
+ * @param displayCounterpartUserId - When set (e.g. for an opportunity card), only this counterpart is included in otherPartyContext so the presenter writes about the person on the card.
163
163
  * @param focusedViewerIntentId - When set, include only that active intent in viewer context.
164
164
  */
165
165
  export declare function gatherPresenterContext(database: PresenterDatabase, opportunity: Opportunity, viewerId: string, displayCounterpartUserId?: string, focusedViewerIntentId?: string): Promise<PresenterInput>;
@@ -27,9 +27,9 @@ import { createStructuredModel } from "../shared/agent/model.config.js";
27
27
  // ──────────────────────────────────────────────────────────────────────
28
28
  /** Cache namespace for opportunity presentation copy. Bump to invalidate copy safety changes. */
29
29
  export const OPPORTUNITY_PRESENTATION_CACHE_VERSION = "v2";
30
- export function buildRadarCardPresentationCacheKey(opportunityId, status, viewerId, focusedViewerIntentId) {
30
+ export function buildOpportunityCardCacheKey(opportunityId, status, viewerId, focusedViewerIntentId) {
31
31
  const scope = focusedViewerIntentId ? `:intent:${focusedViewerIntentId}` : "";
32
- return `radar:${OPPORTUNITY_PRESENTATION_CACHE_VERSION}:card:${opportunityId}:${status}:${viewerId}${scope}`;
32
+ return `card:${OPPORTUNITY_PRESENTATION_CACHE_VERSION}:${opportunityId}:${status}:${viewerId}${scope}`;
33
33
  }
34
34
  export function buildApiChatCardPresentationCacheKey(opportunityId, viewerId) {
35
35
  return `chat:${OPPORTUNITY_PRESENTATION_CACHE_VERSION}:card:${opportunityId}:${viewerId}`;
@@ -387,7 +387,7 @@ export function summarizeSignalsForPresenter(signals) {
387
387
  * Gather all context needed for the presenter from the database.
388
388
  * Fetches viewer profile, viewer intents, other party profile(s), and network in parallel.
389
389
  *
390
- * @param displayCounterpartUserId - When set (e.g. for a radar card), only this counterpart is included in otherPartyContext so the presenter writes about the person on the card.
390
+ * @param displayCounterpartUserId - When set (e.g. for an opportunity card), only this counterpart is included in otherPartyContext so the presenter writes about the person on the card.
391
391
  * @param focusedViewerIntentId - When set, include only that active intent in viewer context.
392
392
  */
393
393
  export async function gatherPresenterContext(database, opportunity, viewerId, displayCounterpartUserId, focusedViewerIntentId) {
@@ -46,11 +46,6 @@ export declare function isActionableForViewer(actors: Array<{
46
46
  }>, status: string, viewerId: string, committedActorIds?: readonly string[]): boolean;
47
47
  /** Feed category for home composition. */
48
48
  export type FeedCategory = 'connection' | 'expired';
49
- /** Soft targets for radar composition. */
50
- export declare const RADAR_SOFT_TARGETS: {
51
- readonly connection: 5;
52
- readonly expired: 2;
53
- };
54
49
  /**
55
50
  * Classify an actionable opportunity into a feed category.
56
51
  * Assumes the opportunity already passed isActionableForViewer or is expired.
@@ -66,40 +61,3 @@ export declare function classifyOpportunity(opp: {
66
61
  }>;
67
62
  status: string;
68
63
  }, _viewerId: string): FeedCategory;
69
- /**
70
- * Select opportunities for the radar using soft composition targets.
71
- * Fills each category up to its target, then redistributes unused slots
72
- * to categories that have more items available. Preserves input order.
73
- *
74
- * @param opportunities - Pre-sorted opportunities (by confidence/recency)
75
- * @param viewerId - The viewing user's ID
76
- * @returns Composition-balanced subset
77
- */
78
- export declare function selectByComposition<T extends {
79
- actors: Array<{
80
- userId: string;
81
- role: string;
82
- }>;
83
- status: string;
84
- }>(opportunities: T[], viewerId: string): T[];
85
- /**
86
- * Deduplicate opportunities so each counterpart appears at most once.
87
- * Keeps the opportunity with the highest interpretation.confidence per
88
- * counterpart userId. On ties, the first encountered wins (stable).
89
- *
90
- * Counterpart = first actor whose userId !== viewerId.
91
- * Opportunities without a derivable counterpart pass through undeduped.
92
- *
93
- * @param opportunities - Pre-sorted opportunities (e.g. by confidence/recency)
94
- * @param viewerId - The viewing user's ID
95
- * @returns Deduped subset preserving original input order among winners
96
- */
97
- export declare function deduplicateByPerson<T extends {
98
- actors: Array<{
99
- userId: string;
100
- role: string;
101
- }>;
102
- interpretation?: {
103
- confidence?: number;
104
- } | null;
105
- }>(opportunities: T[], viewerId: string): T[];
@@ -1,6 +1,3 @@
1
- import { log } from '../shared/observability/log.js';
2
- const logger = log.graph.from('SelectByComposition');
3
- const dedupeByPersonLog = log.graph.from('DeduplicateByPerson');
4
1
  /**
5
2
  * Validates opportunity actors.
6
3
  *
@@ -51,14 +48,6 @@ export function isActionableForViewer(actors, status, viewerId, committedActorId
51
48
  return false;
52
49
  return !committedActorIds.includes(viewerId);
53
50
  }
54
- /** Soft targets for radar composition. */
55
- export const RADAR_SOFT_TARGETS = {
56
- // 3 + 2 in a second category before that category was removed. The slots move to
57
- // connections rather than shrinking the feed: the total a radar can hold is
58
- // unchanged, and connections are the only kind of card left.
59
- connection: 5,
60
- expired: 2,
61
- };
62
51
  /**
63
52
  * Classify an actionable opportunity into a feed category.
64
53
  * Assumes the opportunity already passed isActionableForViewer or is expired.
@@ -72,94 +61,3 @@ export function classifyOpportunity(opp, _viewerId) {
72
61
  return 'expired';
73
62
  return 'connection';
74
63
  }
75
- /**
76
- * Select opportunities for the radar using soft composition targets.
77
- * Fills each category up to its target, then redistributes unused slots
78
- * to categories that have more items available. Preserves input order.
79
- *
80
- * @param opportunities - Pre-sorted opportunities (by confidence/recency)
81
- * @param viewerId - The viewing user's ID
82
- * @returns Composition-balanced subset
83
- */
84
- export function selectByComposition(opportunities, viewerId) {
85
- const buckets = { connection: [], expired: [] };
86
- for (const opp of opportunities) {
87
- buckets[classifyOpportunity(opp, viewerId)].push(opp);
88
- }
89
- const targets = {
90
- connection: RADAR_SOFT_TARGETS.connection,
91
- expired: RADAR_SOFT_TARGETS.expired,
92
- };
93
- // First pass: fill each category up to its target.
94
- const selected = {
95
- connection: buckets.connection.slice(0, targets.connection),
96
- expired: buckets.expired.slice(0, targets.expired),
97
- };
98
- // Second pass: redistribute unused slots, connection before expired.
99
- let unusedSlots = (targets.connection + targets.expired)
100
- - (selected.connection.length + selected.expired.length);
101
- for (const category of ['connection', 'expired']) {
102
- if (unusedSlots <= 0)
103
- break;
104
- const remaining = buckets[category].slice(selected[category].length);
105
- const take = Math.min(remaining.length, unusedSlots);
106
- selected[category].push(...remaining.slice(0, take));
107
- unusedSlots -= take;
108
- }
109
- // Within each category, preserve original input order.
110
- const indexMap = new Map(opportunities.map((opp, i) => [opp, i]));
111
- const sortByOriginal = (a, b) => (indexMap.get(a) ?? 0) - (indexMap.get(b) ?? 0);
112
- selected.connection.sort(sortByOriginal);
113
- selected.expired.sort(sortByOriginal);
114
- logger.info('Selected opportunities by composition', {
115
- input: opportunities.length,
116
- buckets: { connection: buckets.connection.length, expired: buckets.expired.length },
117
- selected: { connection: selected.connection.length, expired: selected.expired.length },
118
- });
119
- return [...selected.connection, ...selected.expired];
120
- }
121
- /**
122
- * Deduplicate opportunities so each counterpart appears at most once.
123
- * Keeps the opportunity with the highest interpretation.confidence per
124
- * counterpart userId. On ties, the first encountered wins (stable).
125
- *
126
- * Counterpart = first actor whose userId !== viewerId.
127
- * Opportunities without a derivable counterpart pass through undeduped.
128
- *
129
- * @param opportunities - Pre-sorted opportunities (e.g. by confidence/recency)
130
- * @param viewerId - The viewing user's ID
131
- * @returns Deduped subset preserving original input order among winners
132
- */
133
- export function deduplicateByPerson(opportunities, viewerId) {
134
- const bestByCounterpart = new Map();
135
- const noCounterpart = [];
136
- for (let i = 0; i < opportunities.length; i++) {
137
- const opp = opportunities[i];
138
- const counterpart = opp.actors.find((a) => a.userId !== viewerId);
139
- if (!counterpart) {
140
- noCounterpart.push({ opp, index: i });
141
- continue;
142
- }
143
- const key = counterpart.userId;
144
- const existing = bestByCounterpart.get(key);
145
- if (!existing) {
146
- bestByCounterpart.set(key, { opp, index: i });
147
- continue;
148
- }
149
- const newConf = opp.interpretation?.confidence ?? -1;
150
- const oldConf = existing.opp.interpretation?.confidence ?? -1;
151
- if (newConf > oldConf) {
152
- bestByCounterpart.set(key, { opp, index: i });
153
- }
154
- }
155
- const all = [...bestByCounterpart.values(), ...noCounterpart];
156
- all.sort((a, b) => a.index - b.index);
157
- const result = all.map((entry) => entry.opp);
158
- if (result.length < opportunities.length) {
159
- dedupeByPersonLog.info('Deduped opportunities by person', {
160
- input: opportunities.length,
161
- output: result.length,
162
- });
163
- }
164
- return result;
165
- }
@@ -0,0 +1,42 @@
1
+ export type DecisionQuestion = {
2
+ type: "noul";
3
+ instructions: string;
4
+ criteria?: {
5
+ true: string;
6
+ false: string;
7
+ };
8
+ } | {
9
+ type: "choice";
10
+ instructions: string;
11
+ criteria: Record<string, string>;
12
+ } | {
13
+ type: "score";
14
+ instructions: string;
15
+ criteria: string[];
16
+ };
17
+ export type DecisionAnswer = {
18
+ type: "noul";
19
+ noul: number;
20
+ } | {
21
+ type: "choice";
22
+ choice: string;
23
+ } | {
24
+ type: "score";
25
+ score: number;
26
+ };
27
+ /**
28
+ * Jev, through OpenRouter's Decisions API. It returns typed answers, not text.
29
+ * A missing key, a bad response, or a timeout throws. Callers keep their own fallback.
30
+ */
31
+ export declare class DecisionClient {
32
+ private readonly apiKey?;
33
+ private readonly timeout;
34
+ constructor(apiKey?: string | undefined, timeout?: number);
35
+ /**
36
+ * @param state - The draft and whatever else the questions refer to.
37
+ * @param questions - Named noul, choice, or score questions.
38
+ * @param signal - Cancels this call. A request-scoped signal is used when this is omitted.
39
+ * @returns One answer per question key.
40
+ */
41
+ decide(state: unknown, questions: Record<string, DecisionQuestion>, signal?: AbortSignal): Promise<Record<string, DecisionAnswer>>;
42
+ }
@@ -0,0 +1,61 @@
1
+ import { getAbortSignalConfig } from "./model-signal.js";
2
+ const DECISIONS_URL = "https://openrouter.ai/api/alpha/decisions";
3
+ const JEV_MODEL = "typesafe/jev-1.13";
4
+ const TIMEOUT_MS = 60000;
5
+ /**
6
+ * Jev, through OpenRouter's Decisions API. It returns typed answers, not text.
7
+ * A missing key, a bad response, or a timeout throws. Callers keep their own fallback.
8
+ */
9
+ export class DecisionClient {
10
+ constructor(apiKey, timeout = TIMEOUT_MS) {
11
+ this.apiKey = apiKey;
12
+ this.timeout = timeout;
13
+ }
14
+ /**
15
+ * @param state - The draft and whatever else the questions refer to.
16
+ * @param questions - Named noul, choice, or score questions.
17
+ * @param signal - Cancels this call. A request-scoped signal is used when this is omitted.
18
+ * @returns One answer per question key.
19
+ */
20
+ async decide(state, questions, signal) {
21
+ const apiKey = this.apiKey ?? process.env.OPENROUTER_API_KEY;
22
+ if (!apiKey?.trim())
23
+ throw new Error("DecisionClient: OPENROUTER_API_KEY is required.");
24
+ const caller = signal ?? getAbortSignalConfig()?.signal;
25
+ const deadline = AbortSignal.timeout(this.timeout);
26
+ const stop = caller ? AbortSignal.any([caller, deadline]) : deadline;
27
+ const response = await fetch(DECISIONS_URL, {
28
+ method: "POST",
29
+ signal: stop,
30
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
31
+ body: JSON.stringify({ model: JEV_MODEL, state, questions }),
32
+ });
33
+ const body = await response.text();
34
+ let data = {};
35
+ try {
36
+ data = JSON.parse(body);
37
+ }
38
+ catch {
39
+ if (response.ok)
40
+ throw new Error(`Jev returned a non-JSON response: ${body.slice(0, 500)}`);
41
+ }
42
+ if (!response.ok || data.error || !data.answers) {
43
+ throw new Error(`Jev request failed (${response.status}): ${(data.error?.message ?? body).slice(0, 500)}`);
44
+ }
45
+ return readAnswers(data.answers);
46
+ }
47
+ }
48
+ function readAnswers(raw) {
49
+ const answers = {};
50
+ for (const [key, answer] of Object.entries(raw)) {
51
+ if (answer?.type === "noul" && typeof answer.noul === "number")
52
+ answers[key] = { type: "noul", noul: answer.noul };
53
+ else if (answer?.type === "choice" && typeof answer.choice === "string")
54
+ answers[key] = { type: "choice", choice: answer.choice };
55
+ else if (answer?.type === "score" && typeof answer.score === "number")
56
+ answers[key] = { type: "score", score: answer.score };
57
+ else
58
+ throw new Error(`Jev answer "${key}" was unusable`);
59
+ }
60
+ return answers;
61
+ }
@@ -51,9 +51,9 @@ export type IntentNetworkGraphDatabase = Pick<Database, 'getIntentForIndexing' |
51
51
  */
52
52
  export type NetworkMembershipGraphDatabase = Pick<Database, 'isNetworkMember' | 'isNetworkOwner' | 'getNetworkWithPermissions' | 'addMemberToNetwork' | 'removeMemberFromNetwork' | 'getNetworkMembersForMember'>;
53
53
  /**
54
- * Database interface for Radar Graph (opportunity radar view).
54
+ * Database interface for opportunity cards.
55
55
  * Load opportunities, enrich with profile/network, and support presenter context.
56
56
  *
57
57
  * Access layer: UserDatabase (own opportunities and profile)
58
58
  */
59
- export type RadarGraphDatabase = Pick<Database, 'getOpportunitiesForUser' | 'getOpportunity' | 'getProfile' | 'getActiveIntents' | 'getNetwork' | 'getUser'> & NegotiationContextDatabase;
59
+ export type OpportunityCardsDatabase = Pick<Database, 'getOpportunitiesForUser' | 'getOpportunity' | 'getProfile' | 'getActiveIntents' | 'getNetwork' | 'getUser'> & NegotiationContextDatabase;
@@ -485,6 +485,6 @@ export interface OpportunityQueryOptions {
485
485
  actorUserId?: string;
486
486
  limit?: number;
487
487
  offset?: number;
488
- /** When set, include draft opportunities for this chat session. When unset, exclude all draft opportunities (e.g. radar view, API). */
488
+ /** When set, include draft opportunities for this chat session. When unset, exclude all draft opportunities (e.g. opportunity cards, API). */
489
489
  conversationId?: string;
490
490
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indexnetwork/protocol",
3
- "version": "66.0.0-rc.608.1",
3
+ "version": "66.0.0-rc.610.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,24 +0,0 @@
1
- import type { CompositeDatabase, Opportunity } from '../../platform/database.js';
2
- export interface OpportunityFeedSelectionInput {
3
- reader: Pick<CompositeDatabase, 'getOpportunitiesForUser'>;
4
- viewerId: string;
5
- networkId?: string;
6
- intentScope: {
7
- scopeType?: 'intent';
8
- scopeId?: string;
9
- };
10
- displayLimit: number;
11
- warn: (message: string, data: Record<string, unknown>) => void;
12
- }
13
- export interface OpportunityFeedSelection {
14
- opportunities: Opportunity[];
15
- dedupedCount: number;
16
- skippedIds: string[];
17
- fetchedCount: number;
18
- }
19
- /**
20
- * Selects actionable opportunities for the chat/feed surface. The caller actor
21
- * check is deliberately the first post-read filter so non-actor rows cannot
22
- * affect selection or downstream profile/presentation reads.
23
- */
24
- export declare function selectOpportunityFeed(args: OpportunityFeedSelectionInput): Promise<OpportunityFeedSelection>;
@@ -1,40 +0,0 @@
1
- import { deduplicateByPerson, selectByComposition } from './opportunity.utils.js';
2
- const ACTIONABLE_FEED_STATUSES = ['pending'];
3
- const FEED_FETCH_LIMIT = 30;
4
- /**
5
- * Selects actionable opportunities for the chat/feed surface. The caller actor
6
- * check is deliberately the first post-read filter so non-actor rows cannot
7
- * affect selection or downstream profile/presentation reads.
8
- */
9
- export async function selectOpportunityFeed(args) {
10
- const fetched = await args.reader.getOpportunitiesForUser(args.viewerId, {
11
- networkId: args.networkId,
12
- ...args.intentScope,
13
- statuses: ACTIONABLE_FEED_STATUSES,
14
- limit: FEED_FETCH_LIMIT,
15
- });
16
- const skippedIds = [];
17
- const callerScoped = fetched.filter((opportunity) => {
18
- if (opportunity.actors.some((actor) => actor.userId === args.viewerId))
19
- return true;
20
- args.warn('Opportunity feed: skipping opportunity where caller is not an actor', {
21
- opportunityId: opportunity.id,
22
- viewerId: args.viewerId,
23
- actorUserIds: opportunity.actors
24
- .map((actor) => actor.userId)
25
- .filter((userId) => typeof userId === 'string'),
26
- });
27
- skippedIds.push(opportunity.id);
28
- return false;
29
- });
30
- const deduped = deduplicateByPerson(callerScoped, args.viewerId);
31
- const selected = deduped.length > 0
32
- ? selectByComposition(deduped, args.viewerId)
33
- : deduped;
34
- return {
35
- opportunities: selected.slice(0, args.displayLimit),
36
- dedupedCount: deduped.length,
37
- skippedIds,
38
- fetchedCount: fetched.length,
39
- };
40
- }