@indexnetwork/protocol 4.5.0-rc.322.1 → 4.5.0-rc.324.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 (111) hide show
  1. package/dist/agent/agent.tools.js +1 -1
  2. package/dist/agent/agent.tools.js.map +1 -1
  3. package/dist/chat/chat.interrupt.classifier.js +1 -1
  4. package/dist/chat/chat.interrupt.classifier.js.map +1 -1
  5. package/dist/chat/chat.suggester.js +3 -3
  6. package/dist/chat/chat.suggester.js.map +1 -1
  7. package/dist/chat/chat.title.generator.js +2 -2
  8. package/dist/chat/chat.title.generator.js.map +1 -1
  9. package/dist/contact/contact.tools.js +1 -1
  10. package/dist/contact/contact.tools.js.map +1 -1
  11. package/dist/enrichment/enrichment.graph.d.ts.map +1 -1
  12. package/dist/enrichment/enrichment.graph.js +9 -3
  13. package/dist/enrichment/enrichment.graph.js.map +1 -1
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/integration/integration.tools.js +1 -1
  19. package/dist/integration/integration.tools.js.map +1 -1
  20. package/dist/intent/intent.graph.d.ts.map +1 -1
  21. package/dist/intent/intent.graph.js +12 -10
  22. package/dist/intent/intent.graph.js.map +1 -1
  23. package/dist/intent/intent.indexer.d.ts.map +1 -1
  24. package/dist/intent/intent.indexer.js +4 -3
  25. package/dist/intent/intent.indexer.js.map +1 -1
  26. package/dist/intent/intent.inferrer.d.ts.map +1 -1
  27. package/dist/intent/intent.inferrer.js +2 -1
  28. package/dist/intent/intent.inferrer.js.map +1 -1
  29. package/dist/intent/intent.reconciler.d.ts.map +1 -1
  30. package/dist/intent/intent.reconciler.js +5 -4
  31. package/dist/intent/intent.reconciler.js.map +1 -1
  32. package/dist/intent/intent.verifier.d.ts.map +1 -1
  33. package/dist/intent/intent.verifier.js +4 -3
  34. package/dist/intent/intent.verifier.js.map +1 -1
  35. package/dist/maintenance/maintenance.graph.d.ts.map +1 -1
  36. package/dist/maintenance/maintenance.graph.js +27 -5
  37. package/dist/maintenance/maintenance.graph.js.map +1 -1
  38. package/dist/mcp/mcp.server.d.ts.map +1 -1
  39. package/dist/mcp/mcp.server.js +5 -4
  40. package/dist/mcp/mcp.server.js.map +1 -1
  41. package/dist/negotiation/insight.generator.js +2 -2
  42. package/dist/negotiation/insight.generator.js.map +1 -1
  43. package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
  44. package/dist/negotiation/negotiation.graph.js +15 -11
  45. package/dist/negotiation/negotiation.graph.js.map +1 -1
  46. package/dist/negotiation/negotiation.tools.js +1 -1
  47. package/dist/negotiation/negotiation.tools.js.map +1 -1
  48. package/dist/network/network.recommender.d.ts.map +1 -1
  49. package/dist/network/network.recommender.js +5 -4
  50. package/dist/network/network.recommender.js.map +1 -1
  51. package/dist/network/network.tools.d.ts.map +1 -1
  52. package/dist/network/network.tools.js +4 -2
  53. package/dist/network/network.tools.js.map +1 -1
  54. package/dist/opportunity/feed/feed.graph.d.ts.map +1 -1
  55. package/dist/opportunity/feed/feed.graph.js +30 -23
  56. package/dist/opportunity/feed/feed.graph.js.map +1 -1
  57. package/dist/opportunity/opportunity.discover.d.ts.map +1 -1
  58. package/dist/opportunity/opportunity.discover.js +6 -4
  59. package/dist/opportunity/opportunity.discover.js.map +1 -1
  60. package/dist/opportunity/opportunity.enricher.js +2 -2
  61. package/dist/opportunity/opportunity.enricher.js.map +1 -1
  62. package/dist/opportunity/opportunity.evaluator.d.ts.map +1 -1
  63. package/dist/opportunity/opportunity.evaluator.js +9 -7
  64. package/dist/opportunity/opportunity.evaluator.js.map +1 -1
  65. package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
  66. package/dist/opportunity/opportunity.graph.js +123 -105
  67. package/dist/opportunity/opportunity.graph.js.map +1 -1
  68. package/dist/opportunity/opportunity.introducer.js +5 -5
  69. package/dist/opportunity/opportunity.introducer.js.map +1 -1
  70. package/dist/opportunity/opportunity.persist.js +2 -2
  71. package/dist/opportunity/opportunity.persist.js.map +1 -1
  72. package/dist/opportunity/opportunity.presenter.d.ts.map +1 -1
  73. package/dist/opportunity/opportunity.presenter.js +4 -2
  74. package/dist/opportunity/opportunity.presenter.js.map +1 -1
  75. package/dist/opportunity/opportunity.tools.d.ts.map +1 -1
  76. package/dist/opportunity/opportunity.tools.js +2 -1
  77. package/dist/opportunity/opportunity.tools.js.map +1 -1
  78. package/dist/opportunity/opportunity.utils.d.ts.map +1 -1
  79. package/dist/opportunity/opportunity.utils.js +29 -5
  80. package/dist/opportunity/opportunity.utils.js.map +1 -1
  81. package/dist/premise/premise.analyzer.d.ts.map +1 -1
  82. package/dist/premise/premise.analyzer.js +3 -2
  83. package/dist/premise/premise.analyzer.js.map +1 -1
  84. package/dist/premise/premise.decomposer.d.ts.map +1 -1
  85. package/dist/premise/premise.decomposer.js +12 -3
  86. package/dist/premise/premise.decomposer.js.map +1 -1
  87. package/dist/premise/premise.graph.d.ts.map +1 -1
  88. package/dist/premise/premise.graph.js +19 -9
  89. package/dist/premise/premise.graph.js.map +1 -1
  90. package/dist/premise/premise.indexer.d.ts.map +1 -1
  91. package/dist/premise/premise.indexer.js +2 -1
  92. package/dist/premise/premise.indexer.js.map +1 -1
  93. package/dist/premise/premise.tools.d.ts.map +1 -1
  94. package/dist/premise/premise.tools.js +8 -4
  95. package/dist/premise/premise.tools.js.map +1 -1
  96. package/dist/shared/agent/tool.factory.d.ts.map +1 -1
  97. package/dist/shared/agent/tool.factory.js +4 -2
  98. package/dist/shared/agent/tool.factory.js.map +1 -1
  99. package/dist/shared/agent/tool.registry.d.ts.map +1 -1
  100. package/dist/shared/agent/tool.registry.js +6 -4
  101. package/dist/shared/agent/tool.registry.js.map +1 -1
  102. package/dist/shared/interfaces/database.interface.d.ts +1 -1
  103. package/dist/shared/interfaces/database.interface.js.map +1 -1
  104. package/dist/shared/observability/log.d.ts +3 -2
  105. package/dist/shared/observability/log.d.ts.map +1 -1
  106. package/dist/shared/observability/log.js +72 -9
  107. package/dist/shared/observability/log.js.map +1 -1
  108. package/dist/shared/observability/protocol.logger.d.ts.map +1 -1
  109. package/dist/shared/observability/protocol.logger.js +5 -3
  110. package/dist/shared/observability/protocol.logger.js.map +1 -1
  111. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"opportunity.utils.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAKxE,qEAAqE;AACrE,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhE,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,oBAAoB,CAAC;IACjC,aAAa,EAAE,oBAAoB,CAAC;CACrC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,gBAAgB,GAAG,YAAY,CAe5E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CA+BhG;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAiBT;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,EAC5F,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CA6BT;AAED,0CAA0C;AAC1C,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,gBAAgB,GAAG,SAAS,CAAC;AAEvE,8CAA8C;AAC9C,eAAO,MAAM,iBAAiB;;;;CAIpB,CAAC;AAEX;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE;IAAE,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACxE,QAAQ,EAAE,MAAM,GACf,YAAY,CAKd;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC/G,aAAa,EAAE,CAAC,EAAE,EAClB,QAAQ,EAAE,MAAM,GACf,CAAC,EAAE,CAwDL;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAC5C,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,cAAc,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACjD,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,EAAE,CAwC5C;AAED;;;GAGG;AACH,eAAO,MAAM,+BAA+B,IAAI,CAAC;AAEjD,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD,EACC,UAAU,EAAE,CAAC,EAAE,EACf,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC5C,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACA;IAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAAC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CA4D3C"}
1
+ {"version":3,"file":"opportunity.utils.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.utils.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import { log } from '../shared/observability/log.js';
9
9
  const logger = log.graph.from('SelectByComposition');
10
+ const dedupeByPersonLog = log.graph.from('DeduplicateByPerson');
11
+ const digestCandidatesLog = log.graph.from('SelectDigestCandidates');
10
12
  /**
11
13
  * Derive actor roles from the corpus type of a lens match.
12
14
  *
@@ -233,7 +235,19 @@ export function selectByComposition(opportunities, viewerId) {
233
235
  selected.connection.sort(sortByOriginal);
234
236
  selected['connector-flow'].sort(sortByOriginal);
235
237
  selected.expired.sort(sortByOriginal);
236
- logger.info(`[selectByComposition] input=${opportunities.length} buckets: connection=${buckets.connection.length} connector-flow=${buckets['connector-flow'].length} expired=${buckets.expired.length} → selected: connection=${selected.connection.length} connector-flow=${selected['connector-flow'].length} expired=${selected.expired.length}`);
238
+ logger.info('Selected opportunities by composition', {
239
+ input: opportunities.length,
240
+ buckets: {
241
+ connection: buckets.connection.length,
242
+ connectorFlow: buckets['connector-flow'].length,
243
+ expired: buckets.expired.length,
244
+ },
245
+ selected: {
246
+ connection: selected.connection.length,
247
+ connectorFlow: selected['connector-flow'].length,
248
+ expired: selected.expired.length,
249
+ },
250
+ });
237
251
  return [
238
252
  ...selected.connection,
239
253
  ...selected['connector-flow'],
@@ -278,7 +292,10 @@ export function deduplicateByPerson(opportunities, viewerId) {
278
292
  all.sort((a, b) => a.index - b.index);
279
293
  const result = all.map((entry) => entry.opp);
280
294
  if (result.length < opportunities.length) {
281
- logger.info(`[deduplicateByPerson] deduped ${opportunities.length} ${result.length} opportunities`);
295
+ dedupeByPersonLog.info('Deduped opportunities by person', {
296
+ input: opportunities.length,
297
+ output: result.length,
298
+ });
282
299
  }
283
300
  return result;
284
301
  }
@@ -326,7 +343,10 @@ export function selectDigestCandidates(candidates, opts) {
326
343
  return !counterpart || !acceptedCounterpartIds.has(counterpart.userId);
327
344
  });
328
345
  if (afterAccepted.length < candidates.length) {
329
- logger.info(`[selectDigestCandidates] accepted-counterpart suppression dropped ${candidates.length - afterAccepted.length} of ${candidates.length} candidates`);
346
+ digestCandidatesLog.info('Accepted-counterpart suppression dropped candidates', {
347
+ dropped: candidates.length - afterAccepted.length,
348
+ total: candidates.length,
349
+ });
330
350
  }
331
351
  // Rule 2: delivery-ledger dedup keyed (opportunityId, deliveredAtStatus).
332
352
  // Keep the LATEST committed delivery per key — cooldown measures time since
@@ -343,7 +363,9 @@ export function selectDigestCandidates(candidates, opts) {
343
363
  const fresh = afterAccepted.filter((opp) => !lastDeliveredByKey.has(`${opp.id}:${opp.status}`));
344
364
  if (fresh.length > 0) {
345
365
  if (fresh.length < afterAccepted.length) {
346
- logger.info(`[selectDigestCandidates] ledger dedup dropped ${afterAccepted.length - fresh.length} already-shown candidates`);
366
+ digestCandidatesLog.info('Ledger dedup dropped already-shown candidates', {
367
+ dropped: afterAccepted.length - fresh.length,
368
+ });
347
369
  }
348
370
  return { pool: fresh, redeliveryIds: new Set() };
349
371
  }
@@ -355,7 +377,9 @@ export function selectDigestCandidates(candidates, opts) {
355
377
  .filter((entry) => entry.at instanceof Date && now.getTime() - entry.at.getTime() >= cooldownMs)
356
378
  .sort((a, b) => a.at.getTime() - b.at.getTime());
357
379
  if (cooled.length > 0) {
358
- logger.info(`[selectDigestCandidates] no fresh candidates — re-showing ${cooled.length} past-cooldown candidate(s)`);
380
+ digestCandidatesLog.info('No fresh candidates — re-showing past-cooldown candidates', {
381
+ count: cooled.length,
382
+ });
359
383
  }
360
384
  return {
361
385
  pool: cooled.map((entry) => entry.opp),
@@ -1 +1 @@
1
- {"version":3,"file":"opportunity.utils.js","sourceRoot":"/","sources":["opportunity/opportunity.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opportunity graph utilities: role derivation from corpus type.\n * Used by the opportunity graph to map lens corpus to opportunity actor roles.\n *\n * With lens-based HyDE, strategy selection is handled automatically by the\n * LensInferrer agent. This file provides corpus-to-role mapping for opportunity actors.\n */\n\nimport type { HydeTargetCorpus } from '../shared/hyde/lens.inferrer.js';\nimport { log } from '../shared/observability/log.js';\n\nconst logger = log.graph.from('SelectByComposition');\n\n/** Actor roles in the opportunity model (agent / patient / peer). */\nexport type OpportunityActorRole = 'agent' | 'patient' | 'peer';\n\n/** Result of mapping a corpus to source and candidate roles. */\nexport interface DerivedRoles {\n sourceRole: OpportunityActorRole;\n candidateRole: OpportunityActorRole;\n}\n\n/**\n * Derive actor roles from the corpus type of a lens match.\n *\n * When a candidate is found via:\n * - \"profiles\" corpus → found by who they are → candidate can help → agent\n * - \"intents\" corpus → found by what they need → candidate needs something → patient\n *\n * @param corpus - The target corpus that produced the match ('profiles' | 'intents')\n * @returns Roles for the source (intent owner) and the candidate (matched user/intent)\n */\nexport function deriveRolesFromCorpus(corpus: HydeTargetCorpus): DerivedRoles {\n switch (corpus) {\n case 'profiles':\n // Source seeks someone who can help → source is patient, candidate can help → agent\n return { sourceRole: 'patient', candidateRole: 'agent' };\n case 'intents':\n // Source offers or needs; candidate has complementary goal → source is agent, candidate is patient\n return { sourceRole: 'agent', candidateRole: 'patient' };\n case 'premises':\n // Premise matches are symmetric: two people whose self-descriptions align.\n // Unlike intents (directional roles), premises express stable identity truths.\n return { sourceRole: 'peer', candidateRole: 'peer' };\n default:\n return { sourceRole: 'peer', candidateRole: 'peer' };\n }\n}\n\n/**\n * Validates opportunity actors: if an opportunity has an introducer, it must have\n * one or two non-introducer actors (1 = 1:1 intro e.g. \"I want to connect with X\";\n * 2 = introducer connecting two others).\n *\n * Also rejects self-matches — the same person occupying both sides of a\n * connection. The discovery/persist pipeline trusts the LLM evaluator's actor\n * list, which can collapse onto a single user; downstream readers then garble\n * identity (e.g. a connect link/greeting rendered in one party's voice while the\n * card shows the viewer \"matched with themselves\"). Two degenerate shapes are\n * blocked here, at the single persist chokepoint:\n * - every userId-bearing non-introducer actor collapses to the same user,\n * e.g. `[X(agent), X(patient)]`\n * - an introducer who is also a participant (\"Amina introduced you to Amina\")\n * Only `userId`-bearing actors are checked; role-only actors (legacy/tests) pass.\n * Duplicate rows for one participant are allowed when at least one other distinct\n * participant is present (some callers model multiple intents as multiple actor rows).\n *\n * @param actors - Array of actors with at least a role and optional userId\n * @throws Error when the actor set is invalid\n */\nexport function validateOpportunityActors(actors: Array<{ userId?: string; role: string }>): void {\n const introducerCount = actors.filter((a) => a.role === 'introducer').length;\n const nonIntroducerCount = actors.filter((a) => a.role !== 'introducer').length;\n\n if (introducerCount > 0 && (nonIntroducerCount < 1 || nonIntroducerCount > 2)) {\n throw new Error(\n 'An opportunity with an introducer must have one or two other actors.'\n );\n }\n\n // Self-match guard. Compare only actors that carry a userId so role-only\n // shapes (used by some callers/tests) are unaffected.\n const introducerUserIds = new Set(\n actors.filter((a) => a.role === 'introducer' && a.userId).map((a) => a.userId as string),\n );\n const nonIntroducerUserIds = actors\n .filter((a) => a.role !== 'introducer' && a.userId)\n .map((a) => a.userId as string);\n\n for (const userId of nonIntroducerUserIds) {\n if (introducerUserIds.has(userId)) {\n throw new Error(\n 'An opportunity actor cannot be both the introducer and a participant (self-match).'\n );\n }\n }\n\n const uniqueNonIntroducerUserIds = new Set(nonIntroducerUserIds);\n if (nonIntroducerUserIds.length > 1 && uniqueNonIntroducerUserIds.size === 1) {\n throw new Error('An opportunity cannot match a user with themselves (duplicate participant).');\n }\n}\n\n/**\n * Read-level ACL: whether a user is an actor on the opportunity and may fetch\n * its details. Intentionally broader than `isActionableForViewer` — a user can\n * read an opportunity they are not currently expected to act on (e.g. an agent\n * viewing an accepted opportunity).\n *\n * The feed graph and debug controller chain both predicates: an opportunity only\n * reaches the home feed if it passes `canUserSeeOpportunity` first, then\n * `isActionableForViewer`. For `agent with introducer at pending`,\n * `canUserSeeOpportunity` returns false (read gate blocks it), so the opportunity\n * never surfaces even though `isActionableForViewer` Rule 4 would return true in\n * isolation. This is by design — the agent is not granted read access through the\n * home path until the introducer path completes (negotiation → accepted).\n *\n * Compact Visibility Rule (from lifecycle doc):\n * - Introducer or peer: always see.\n * - Patient or party: see if (status is not latent, or there is no introducer).\n * - Agent: see if (status is accepted/rejected/expired, or (status is not latent and there is no introducer)).\n */\nexport function canUserSeeOpportunity(\n actors: Array<{ userId: string; role: string }>,\n status: string,\n userId: string\n): boolean {\n const hasIntroducer = actors.some((a) => a.role === 'introducer');\n const userRoles = actors.filter((a) => a.userId === userId).map((a) => a.role);\n if (userRoles.length === 0) return false;\n\n return userRoles.some((role) => {\n if (role === 'introducer') return true;\n if (role === 'peer') return true;\n if (role === 'patient' || role === 'party')\n return status !== 'latent' || !hasIntroducer;\n if (role === 'agent')\n return (\n ['accepted', 'rejected', 'expired'].includes(status) ||\n (status !== 'latent' && !hasIntroducer)\n );\n return false;\n });\n}\n\n/**\n * Whether an opportunity should appear on the viewer's home feed (actionable =\n * has a pending action for this user).\n *\n * Rules (see `docs/Latent Opportunity Lifecycle.md` — Role-Visibility Matrix):\n *\n * (1) `latent`, no introducer → all actors actionable\n * (2) `latent`, introducer `approved !== true` → introducer only\n * (3) `latent`, introducer `approved === true` → all non-introducer actors\n * (4) `pending` (any introducer config) → non-introducer actors who have not acted\n * (5) `accepted`/`rejected`/`expired`/`stalled`/`draft`/`negotiating`\n * → never actionable\n *\n * The introducer approval signal is stored on the `introducer`-roled actor's\n * `approved: boolean` field within the opportunity's `actors` JSONB. It flips\n * from `false` to `true` when the introducer approves; status stays `latent`\n * across the flip while a background negotiation runs.\n */\nexport function isActionableForViewer(\n actors: Array<{ userId: string; role: string; approved?: boolean; actedAt?: string | null }>,\n status: string,\n viewerId: string\n): boolean {\n const viewerActors = actors.filter((a) => a.userId === viewerId);\n if (viewerActors.length === 0) return false;\n\n const introducer = actors.find((a) => a.role === 'introducer');\n const hasIntroducer = !!introducer;\n const introducerApproved = introducer?.approved === true;\n\n return viewerActors.some(({ role, actedAt }) => {\n if (role === 'introducer') {\n // Rule 2: introducer sees own latent opp only while not yet approved.\n return status === 'latent' && !introducerApproved;\n }\n\n // Non-introducer actors: patient / party / agent / peer.\n if (status === 'latent') {\n // Rule 1: no introducer → visible.\n // Rule 3: introducer approved → visible.\n return !hasIntroducer || introducerApproved;\n }\n if (status === 'pending') {\n // Rule 4: pending is actionable only for actors who have not already\n // acted. Once an actor has `actedAt`, the opportunity is waiting on the\n // counterparty and should not appear in that actor's home feed.\n return !actedAt;\n }\n // Rule 5: never actionable at terminal or internal statuses.\n return false;\n });\n}\n\n/** Feed category for home composition. */\nexport type FeedCategory = 'connection' | 'connector-flow' | 'expired';\n\n/** Soft targets for home feed composition. */\nexport const FEED_SOFT_TARGETS = {\n connection: 3,\n connectorFlow: 2,\n expired: 2,\n} as const;\n\n/**\n * Classify an actionable opportunity into a feed category.\n * Assumes the opportunity already passed isActionableForViewer or is expired.\n *\n * @param opp - Opportunity with actors and status\n * @param viewerId - The viewing user's ID\n * @returns Feed category\n */\nexport function classifyOpportunity(\n opp: { actors: Array<{ userId: string; role: string }>; status: string },\n viewerId: string\n): FeedCategory {\n if (opp.status === 'expired') return 'expired';\n const viewerIsIntroducer = opp.actors.some((a) => a.userId === viewerId && a.role === 'introducer');\n if (viewerIsIntroducer) return 'connector-flow';\n return 'connection';\n}\n\n/**\n * Select opportunities for the home feed using soft composition targets.\n * Fills each category up to its target, then redistributes unused slots\n * to categories that have more items available. Preserves input order.\n *\n * @param opportunities - Pre-sorted opportunities (by confidence/recency)\n * @param viewerId - The viewing user's ID\n * @returns Composition-balanced subset\n */\nexport function selectByComposition<T extends { actors: Array<{ userId: string; role: string }>; status: string }>(\n opportunities: T[],\n viewerId: string\n): T[] {\n const buckets: Record<FeedCategory, T[]> = {\n connection: [],\n 'connector-flow': [],\n expired: [],\n };\n\n for (const opp of opportunities) {\n const category = classifyOpportunity(opp, viewerId);\n buckets[category].push(opp);\n }\n\n const targets: Record<FeedCategory, number> = {\n connection: FEED_SOFT_TARGETS.connection,\n 'connector-flow': FEED_SOFT_TARGETS.connectorFlow,\n expired: FEED_SOFT_TARGETS.expired,\n };\n\n // First pass: fill each category up to its target\n const selected: Record<FeedCategory, T[]> = {\n connection: buckets.connection.slice(0, targets.connection),\n 'connector-flow': buckets['connector-flow'].slice(0, targets['connector-flow']),\n expired: buckets.expired.slice(0, targets.expired),\n };\n\n // Calculate unused slots and remaining items\n const totalTarget = targets.connection + targets['connector-flow'] + targets.expired;\n const usedSlots = selected.connection.length + selected['connector-flow'].length + selected.expired.length;\n let unusedSlots = totalTarget - usedSlots;\n\n // Second pass: redistribute unused slots to categories with remaining items\n // Priority: connection > connector-flow > expired\n const redistOrder: FeedCategory[] = ['connection', 'connector-flow', 'expired'];\n for (const category of redistOrder) {\n if (unusedSlots <= 0) break;\n const remaining = buckets[category].slice(selected[category].length);\n const take = Math.min(remaining.length, unusedSlots);\n selected[category].push(...remaining.slice(0, take));\n unusedSlots -= take;\n }\n\n // Merge in category priority order: connection > connector-flow > expired\n // Within each category, preserve original input order\n const indexMap = new Map(opportunities.map((opp, i) => [opp, i]));\n const sortByOriginal = (a: T, b: T) => (indexMap.get(a) ?? 0) - (indexMap.get(b) ?? 0);\n selected.connection.sort(sortByOriginal);\n selected['connector-flow'].sort(sortByOriginal);\n selected.expired.sort(sortByOriginal);\n\n logger.info(`[selectByComposition] input=${opportunities.length} buckets: connection=${buckets.connection.length} connector-flow=${buckets['connector-flow'].length} expired=${buckets.expired.length} → selected: connection=${selected.connection.length} connector-flow=${selected['connector-flow'].length} expired=${selected.expired.length}`);\n\n return [\n ...selected.connection,\n ...selected['connector-flow'],\n ...selected.expired,\n ];\n}\n\n/**\n * Deduplicate opportunities so each counterpart appears at most once.\n * Keeps the opportunity with the highest interpretation.confidence per\n * counterpart userId. On ties, the first encountered wins (stable).\n *\n * Counterpart = first actor whose userId !== viewerId and role !== 'introducer'.\n * Opportunities without a derivable counterpart pass through undeduped.\n *\n * @param opportunities - Pre-sorted opportunities (e.g. by confidence/recency)\n * @param viewerId - The viewing user's ID\n * @returns Deduped subset preserving original input order among winners\n */\nexport function deduplicateByPerson<T extends {\n actors: Array<{ userId: string; role: string }>;\n interpretation?: { confidence?: number } | null;\n}>(opportunities: T[], viewerId: string): T[] {\n const bestByCounterpart = new Map<string, { opp: T; index: number }>();\n const noCounterpart: Array<{ opp: T; index: number }> = [];\n\n for (let i = 0; i < opportunities.length; i++) {\n const opp = opportunities[i];\n const counterpart = opp.actors.find(\n (a) => a.userId !== viewerId && a.role !== 'introducer',\n );\n\n if (!counterpart) {\n noCounterpart.push({ opp, index: i });\n continue;\n }\n\n const key = counterpart.userId;\n const existing = bestByCounterpart.get(key);\n\n if (!existing) {\n bestByCounterpart.set(key, { opp, index: i });\n continue;\n }\n\n const newConf = opp.interpretation?.confidence ?? -1;\n const oldConf = existing.opp.interpretation?.confidence ?? -1;\n if (newConf > oldConf) {\n bestByCounterpart.set(key, { opp, index: i });\n }\n }\n\n const all = [...bestByCounterpart.values(), ...noCounterpart];\n all.sort((a, b) => a.index - b.index);\n\n const result = all.map((entry) => entry.opp);\n if (result.length < opportunities.length) {\n logger.info(\n `[deduplicateByPerson] deduped ${opportunities.length} → ${result.length} opportunities`,\n );\n }\n return result;\n}\n\n/**\n * Days a digest-delivered opportunity stays suppressed before it becomes\n * eligible for a \"still open\" reminder re-show (when nothing fresh exists).\n */\nexport const DIGEST_REDELIVERY_COOLDOWN_DAYS = 5;\n\n/** Committed delivery row shape consumed by {@link selectDigestCandidates}. */\nexport interface DigestDeliveredRow {\n opportunityId: string;\n deliveredAtStatus: string;\n deliveredAt: Date;\n}\n\n/**\n * Cross-day digest suppression for scheduled-brief candidates.\n *\n * Three rules, applied in order:\n * 1. **Accepted-counterpart suppression** — a direct-connection candidate whose\n * counterpart the viewer has already connected with (an `accepted`\n * opportunity exists with that person) is dropped permanently. A new\n * discovery run re-minting the same person must not resurface them.\n * Connector-flow candidates (viewer is the introducer) are exempt: being\n * connected with someone doesn't make an intro ask on their behalf stale.\n * 2. **Delivery-ledger dedup** — candidates with a committed delivery row at\n * the same `(opportunityId, status)` key have already been shown. While any\n * fresh (never-shown) candidate exists, shown ones are dropped entirely.\n * 3. **Cooldown re-show** — when *no* fresh candidate survives, already-shown\n * candidates whose latest delivery is at least `cooldownDays` old are\n * returned instead, least-recently-shown first, flagged via\n * `redeliveryIds` so the digest can frame them as reminders.\n *\n * Pure function — callers fetch accepted counterparts and ledger rows.\n *\n * @param candidates - Deduped, confidence-ordered digest candidates.\n * @param opts.viewerId - The digest recipient.\n * @param opts.acceptedCounterpartIds - userIds the viewer already connected with.\n * @param opts.deliveredRows - Committed ledger rows for the candidate ids.\n * @param opts.now - Clock override for tests.\n * @param opts.cooldownDays - Cooldown override (default {@link DIGEST_REDELIVERY_COOLDOWN_DAYS}).\n * @returns Surviving pool plus the set of candidate ids that are cooldown re-shows.\n */\nexport function selectDigestCandidates<T extends {\n id: string;\n status: string;\n actors: Array<{ userId: string; role: string }>;\n}>(\n candidates: T[],\n opts: {\n viewerId: string;\n acceptedCounterpartIds: ReadonlySet<string>;\n deliveredRows: DigestDeliveredRow[];\n now?: Date;\n cooldownDays?: number;\n },\n): { pool: T[]; redeliveryIds: Set<string> } {\n const { viewerId, acceptedCounterpartIds } = opts;\n\n // Rule 1: accepted-counterpart suppression (direct connections only).\n const afterAccepted = candidates.filter((opp) => {\n const viewerIsIntroducer = opp.actors.some(\n (a) => a.role === 'introducer' && a.userId === viewerId,\n );\n if (viewerIsIntroducer) return true;\n const counterpart = opp.actors.find(\n (a) => a.userId !== viewerId && a.role !== 'introducer',\n );\n return !counterpart || !acceptedCounterpartIds.has(counterpart.userId);\n });\n if (afterAccepted.length < candidates.length) {\n logger.info(\n `[selectDigestCandidates] accepted-counterpart suppression dropped ${candidates.length - afterAccepted.length} of ${candidates.length} candidates`,\n );\n }\n\n // Rule 2: delivery-ledger dedup keyed (opportunityId, deliveredAtStatus).\n // Keep the LATEST committed delivery per key — cooldown measures time since\n // the user last saw the card, not since they first saw it.\n const lastDeliveredByKey = new Map<string, Date>();\n for (const row of opts.deliveredRows) {\n if (!(row.deliveredAt instanceof Date) || Number.isNaN(row.deliveredAt.getTime())) continue;\n const key = `${row.opportunityId}:${row.deliveredAtStatus}`;\n const existing = lastDeliveredByKey.get(key);\n if (!existing || row.deliveredAt > existing) lastDeliveredByKey.set(key, row.deliveredAt);\n }\n\n const fresh = afterAccepted.filter((opp) => !lastDeliveredByKey.has(`${opp.id}:${opp.status}`));\n if (fresh.length > 0) {\n if (fresh.length < afterAccepted.length) {\n logger.info(\n `[selectDigestCandidates] ledger dedup dropped ${afterAccepted.length - fresh.length} already-shown candidates`,\n );\n }\n return { pool: fresh, redeliveryIds: new Set<string>() };\n }\n\n // Rule 3: nothing fresh — re-show the least-recently-shown candidates past cooldown.\n const cooldownMs = (opts.cooldownDays ?? DIGEST_REDELIVERY_COOLDOWN_DAYS) * 86_400_000;\n const now = opts.now ?? new Date();\n const cooled = afterAccepted\n .map((opp) => ({ opp, at: lastDeliveredByKey.get(`${opp.id}:${opp.status}`) }))\n .filter((entry): entry is { opp: T; at: Date } =>\n entry.at instanceof Date && now.getTime() - entry.at.getTime() >= cooldownMs,\n )\n .sort((a, b) => a.at.getTime() - b.at.getTime());\n\n if (cooled.length > 0) {\n logger.info(\n `[selectDigestCandidates] no fresh candidates — re-showing ${cooled.length} past-cooldown candidate(s)`,\n );\n }\n return {\n pool: cooled.map((entry) => entry.opp),\n redeliveryIds: new Set(cooled.map((entry) => entry.opp.id)),\n };\n}\n"]}
1
+ {"version":3,"file":"opportunity.utils.js","sourceRoot":"/","sources":["opportunity/opportunity.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opportunity graph utilities: role derivation from corpus type.\n * Used by the opportunity graph to map lens corpus to opportunity actor roles.\n *\n * With lens-based HyDE, strategy selection is handled automatically by the\n * LensInferrer agent. This file provides corpus-to-role mapping for opportunity actors.\n */\n\nimport type { HydeTargetCorpus } from '../shared/hyde/lens.inferrer.js';\nimport { log } from '../shared/observability/log.js';\n\nconst logger = log.graph.from('SelectByComposition');\nconst dedupeByPersonLog = log.graph.from('DeduplicateByPerson');\nconst digestCandidatesLog = log.graph.from('SelectDigestCandidates');\n\n/** Actor roles in the opportunity model (agent / patient / peer). */\nexport type OpportunityActorRole = 'agent' | 'patient' | 'peer';\n\n/** Result of mapping a corpus to source and candidate roles. */\nexport interface DerivedRoles {\n sourceRole: OpportunityActorRole;\n candidateRole: OpportunityActorRole;\n}\n\n/**\n * Derive actor roles from the corpus type of a lens match.\n *\n * When a candidate is found via:\n * - \"profiles\" corpus → found by who they are → candidate can help → agent\n * - \"intents\" corpus → found by what they need → candidate needs something → patient\n *\n * @param corpus - The target corpus that produced the match ('profiles' | 'intents')\n * @returns Roles for the source (intent owner) and the candidate (matched user/intent)\n */\nexport function deriveRolesFromCorpus(corpus: HydeTargetCorpus): DerivedRoles {\n switch (corpus) {\n case 'profiles':\n // Source seeks someone who can help → source is patient, candidate can help → agent\n return { sourceRole: 'patient', candidateRole: 'agent' };\n case 'intents':\n // Source offers or needs; candidate has complementary goal → source is agent, candidate is patient\n return { sourceRole: 'agent', candidateRole: 'patient' };\n case 'premises':\n // Premise matches are symmetric: two people whose self-descriptions align.\n // Unlike intents (directional roles), premises express stable identity truths.\n return { sourceRole: 'peer', candidateRole: 'peer' };\n default:\n return { sourceRole: 'peer', candidateRole: 'peer' };\n }\n}\n\n/**\n * Validates opportunity actors: if an opportunity has an introducer, it must have\n * one or two non-introducer actors (1 = 1:1 intro e.g. \"I want to connect with X\";\n * 2 = introducer connecting two others).\n *\n * Also rejects self-matches — the same person occupying both sides of a\n * connection. The discovery/persist pipeline trusts the LLM evaluator's actor\n * list, which can collapse onto a single user; downstream readers then garble\n * identity (e.g. a connect link/greeting rendered in one party's voice while the\n * card shows the viewer \"matched with themselves\"). Two degenerate shapes are\n * blocked here, at the single persist chokepoint:\n * - every userId-bearing non-introducer actor collapses to the same user,\n * e.g. `[X(agent), X(patient)]`\n * - an introducer who is also a participant (\"Amina introduced you to Amina\")\n * Only `userId`-bearing actors are checked; role-only actors (legacy/tests) pass.\n * Duplicate rows for one participant are allowed when at least one other distinct\n * participant is present (some callers model multiple intents as multiple actor rows).\n *\n * @param actors - Array of actors with at least a role and optional userId\n * @throws Error when the actor set is invalid\n */\nexport function validateOpportunityActors(actors: Array<{ userId?: string; role: string }>): void {\n const introducerCount = actors.filter((a) => a.role === 'introducer').length;\n const nonIntroducerCount = actors.filter((a) => a.role !== 'introducer').length;\n\n if (introducerCount > 0 && (nonIntroducerCount < 1 || nonIntroducerCount > 2)) {\n throw new Error(\n 'An opportunity with an introducer must have one or two other actors.'\n );\n }\n\n // Self-match guard. Compare only actors that carry a userId so role-only\n // shapes (used by some callers/tests) are unaffected.\n const introducerUserIds = new Set(\n actors.filter((a) => a.role === 'introducer' && a.userId).map((a) => a.userId as string),\n );\n const nonIntroducerUserIds = actors\n .filter((a) => a.role !== 'introducer' && a.userId)\n .map((a) => a.userId as string);\n\n for (const userId of nonIntroducerUserIds) {\n if (introducerUserIds.has(userId)) {\n throw new Error(\n 'An opportunity actor cannot be both the introducer and a participant (self-match).'\n );\n }\n }\n\n const uniqueNonIntroducerUserIds = new Set(nonIntroducerUserIds);\n if (nonIntroducerUserIds.length > 1 && uniqueNonIntroducerUserIds.size === 1) {\n throw new Error('An opportunity cannot match a user with themselves (duplicate participant).');\n }\n}\n\n/**\n * Read-level ACL: whether a user is an actor on the opportunity and may fetch\n * its details. Intentionally broader than `isActionableForViewer` — a user can\n * read an opportunity they are not currently expected to act on (e.g. an agent\n * viewing an accepted opportunity).\n *\n * The feed graph and debug controller chain both predicates: an opportunity only\n * reaches the home feed if it passes `canUserSeeOpportunity` first, then\n * `isActionableForViewer`. For `agent with introducer at pending`,\n * `canUserSeeOpportunity` returns false (read gate blocks it), so the opportunity\n * never surfaces even though `isActionableForViewer` Rule 4 would return true in\n * isolation. This is by design — the agent is not granted read access through the\n * home path until the introducer path completes (negotiation → accepted).\n *\n * Compact Visibility Rule (from lifecycle doc):\n * - Introducer or peer: always see.\n * - Patient or party: see if (status is not latent, or there is no introducer).\n * - Agent: see if (status is accepted/rejected/expired, or (status is not latent and there is no introducer)).\n */\nexport function canUserSeeOpportunity(\n actors: Array<{ userId: string; role: string }>,\n status: string,\n userId: string\n): boolean {\n const hasIntroducer = actors.some((a) => a.role === 'introducer');\n const userRoles = actors.filter((a) => a.userId === userId).map((a) => a.role);\n if (userRoles.length === 0) return false;\n\n return userRoles.some((role) => {\n if (role === 'introducer') return true;\n if (role === 'peer') return true;\n if (role === 'patient' || role === 'party')\n return status !== 'latent' || !hasIntroducer;\n if (role === 'agent')\n return (\n ['accepted', 'rejected', 'expired'].includes(status) ||\n (status !== 'latent' && !hasIntroducer)\n );\n return false;\n });\n}\n\n/**\n * Whether an opportunity should appear on the viewer's home feed (actionable =\n * has a pending action for this user).\n *\n * Rules (see `docs/Latent Opportunity Lifecycle.md` — Role-Visibility Matrix):\n *\n * (1) `latent`, no introducer → all actors actionable\n * (2) `latent`, introducer `approved !== true` → introducer only\n * (3) `latent`, introducer `approved === true` → all non-introducer actors\n * (4) `pending` (any introducer config) → non-introducer actors who have not acted\n * (5) `accepted`/`rejected`/`expired`/`stalled`/`draft`/`negotiating`\n * → never actionable\n *\n * The introducer approval signal is stored on the `introducer`-roled actor's\n * `approved: boolean` field within the opportunity's `actors` JSONB. It flips\n * from `false` to `true` when the introducer approves; status stays `latent`\n * across the flip while a background negotiation runs.\n */\nexport function isActionableForViewer(\n actors: Array<{ userId: string; role: string; approved?: boolean; actedAt?: string | null }>,\n status: string,\n viewerId: string\n): boolean {\n const viewerActors = actors.filter((a) => a.userId === viewerId);\n if (viewerActors.length === 0) return false;\n\n const introducer = actors.find((a) => a.role === 'introducer');\n const hasIntroducer = !!introducer;\n const introducerApproved = introducer?.approved === true;\n\n return viewerActors.some(({ role, actedAt }) => {\n if (role === 'introducer') {\n // Rule 2: introducer sees own latent opp only while not yet approved.\n return status === 'latent' && !introducerApproved;\n }\n\n // Non-introducer actors: patient / party / agent / peer.\n if (status === 'latent') {\n // Rule 1: no introducer → visible.\n // Rule 3: introducer approved → visible.\n return !hasIntroducer || introducerApproved;\n }\n if (status === 'pending') {\n // Rule 4: pending is actionable only for actors who have not already\n // acted. Once an actor has `actedAt`, the opportunity is waiting on the\n // counterparty and should not appear in that actor's home feed.\n return !actedAt;\n }\n // Rule 5: never actionable at terminal or internal statuses.\n return false;\n });\n}\n\n/** Feed category for home composition. */\nexport type FeedCategory = 'connection' | 'connector-flow' | 'expired';\n\n/** Soft targets for home feed composition. */\nexport const FEED_SOFT_TARGETS = {\n connection: 3,\n connectorFlow: 2,\n expired: 2,\n} as const;\n\n/**\n * Classify an actionable opportunity into a feed category.\n * Assumes the opportunity already passed isActionableForViewer or is expired.\n *\n * @param opp - Opportunity with actors and status\n * @param viewerId - The viewing user's ID\n * @returns Feed category\n */\nexport function classifyOpportunity(\n opp: { actors: Array<{ userId: string; role: string }>; status: string },\n viewerId: string\n): FeedCategory {\n if (opp.status === 'expired') return 'expired';\n const viewerIsIntroducer = opp.actors.some((a) => a.userId === viewerId && a.role === 'introducer');\n if (viewerIsIntroducer) return 'connector-flow';\n return 'connection';\n}\n\n/**\n * Select opportunities for the home feed using soft composition targets.\n * Fills each category up to its target, then redistributes unused slots\n * to categories that have more items available. Preserves input order.\n *\n * @param opportunities - Pre-sorted opportunities (by confidence/recency)\n * @param viewerId - The viewing user's ID\n * @returns Composition-balanced subset\n */\nexport function selectByComposition<T extends { actors: Array<{ userId: string; role: string }>; status: string }>(\n opportunities: T[],\n viewerId: string\n): T[] {\n const buckets: Record<FeedCategory, T[]> = {\n connection: [],\n 'connector-flow': [],\n expired: [],\n };\n\n for (const opp of opportunities) {\n const category = classifyOpportunity(opp, viewerId);\n buckets[category].push(opp);\n }\n\n const targets: Record<FeedCategory, number> = {\n connection: FEED_SOFT_TARGETS.connection,\n 'connector-flow': FEED_SOFT_TARGETS.connectorFlow,\n expired: FEED_SOFT_TARGETS.expired,\n };\n\n // First pass: fill each category up to its target\n const selected: Record<FeedCategory, T[]> = {\n connection: buckets.connection.slice(0, targets.connection),\n 'connector-flow': buckets['connector-flow'].slice(0, targets['connector-flow']),\n expired: buckets.expired.slice(0, targets.expired),\n };\n\n // Calculate unused slots and remaining items\n const totalTarget = targets.connection + targets['connector-flow'] + targets.expired;\n const usedSlots = selected.connection.length + selected['connector-flow'].length + selected.expired.length;\n let unusedSlots = totalTarget - usedSlots;\n\n // Second pass: redistribute unused slots to categories with remaining items\n // Priority: connection > connector-flow > expired\n const redistOrder: FeedCategory[] = ['connection', 'connector-flow', 'expired'];\n for (const category of redistOrder) {\n if (unusedSlots <= 0) break;\n const remaining = buckets[category].slice(selected[category].length);\n const take = Math.min(remaining.length, unusedSlots);\n selected[category].push(...remaining.slice(0, take));\n unusedSlots -= take;\n }\n\n // Merge in category priority order: connection > connector-flow > expired\n // Within each category, preserve original input order\n const indexMap = new Map(opportunities.map((opp, i) => [opp, i]));\n const sortByOriginal = (a: T, b: T) => (indexMap.get(a) ?? 0) - (indexMap.get(b) ?? 0);\n selected.connection.sort(sortByOriginal);\n selected['connector-flow'].sort(sortByOriginal);\n selected.expired.sort(sortByOriginal);\n\n logger.info('Selected opportunities by composition', {\n input: opportunities.length,\n buckets: {\n connection: buckets.connection.length,\n connectorFlow: buckets['connector-flow'].length,\n expired: buckets.expired.length,\n },\n selected: {\n connection: selected.connection.length,\n connectorFlow: selected['connector-flow'].length,\n expired: selected.expired.length,\n },\n });\n\n return [\n ...selected.connection,\n ...selected['connector-flow'],\n ...selected.expired,\n ];\n}\n\n/**\n * Deduplicate opportunities so each counterpart appears at most once.\n * Keeps the opportunity with the highest interpretation.confidence per\n * counterpart userId. On ties, the first encountered wins (stable).\n *\n * Counterpart = first actor whose userId !== viewerId and role !== 'introducer'.\n * Opportunities without a derivable counterpart pass through undeduped.\n *\n * @param opportunities - Pre-sorted opportunities (e.g. by confidence/recency)\n * @param viewerId - The viewing user's ID\n * @returns Deduped subset preserving original input order among winners\n */\nexport function deduplicateByPerson<T extends {\n actors: Array<{ userId: string; role: string }>;\n interpretation?: { confidence?: number } | null;\n}>(opportunities: T[], viewerId: string): T[] {\n const bestByCounterpart = new Map<string, { opp: T; index: number }>();\n const noCounterpart: Array<{ opp: T; index: number }> = [];\n\n for (let i = 0; i < opportunities.length; i++) {\n const opp = opportunities[i];\n const counterpart = opp.actors.find(\n (a) => a.userId !== viewerId && a.role !== 'introducer',\n );\n\n if (!counterpart) {\n noCounterpart.push({ opp, index: i });\n continue;\n }\n\n const key = counterpart.userId;\n const existing = bestByCounterpart.get(key);\n\n if (!existing) {\n bestByCounterpart.set(key, { opp, index: i });\n continue;\n }\n\n const newConf = opp.interpretation?.confidence ?? -1;\n const oldConf = existing.opp.interpretation?.confidence ?? -1;\n if (newConf > oldConf) {\n bestByCounterpart.set(key, { opp, index: i });\n }\n }\n\n const all = [...bestByCounterpart.values(), ...noCounterpart];\n all.sort((a, b) => a.index - b.index);\n\n const result = all.map((entry) => entry.opp);\n if (result.length < opportunities.length) {\n dedupeByPersonLog.info('Deduped opportunities by person', {\n input: opportunities.length,\n output: result.length,\n });\n }\n return result;\n}\n\n/**\n * Days a digest-delivered opportunity stays suppressed before it becomes\n * eligible for a \"still open\" reminder re-show (when nothing fresh exists).\n */\nexport const DIGEST_REDELIVERY_COOLDOWN_DAYS = 5;\n\n/** Committed delivery row shape consumed by {@link selectDigestCandidates}. */\nexport interface DigestDeliveredRow {\n opportunityId: string;\n deliveredAtStatus: string;\n deliveredAt: Date;\n}\n\n/**\n * Cross-day digest suppression for scheduled-brief candidates.\n *\n * Three rules, applied in order:\n * 1. **Accepted-counterpart suppression** — a direct-connection candidate whose\n * counterpart the viewer has already connected with (an `accepted`\n * opportunity exists with that person) is dropped permanently. A new\n * discovery run re-minting the same person must not resurface them.\n * Connector-flow candidates (viewer is the introducer) are exempt: being\n * connected with someone doesn't make an intro ask on their behalf stale.\n * 2. **Delivery-ledger dedup** — candidates with a committed delivery row at\n * the same `(opportunityId, status)` key have already been shown. While any\n * fresh (never-shown) candidate exists, shown ones are dropped entirely.\n * 3. **Cooldown re-show** — when *no* fresh candidate survives, already-shown\n * candidates whose latest delivery is at least `cooldownDays` old are\n * returned instead, least-recently-shown first, flagged via\n * `redeliveryIds` so the digest can frame them as reminders.\n *\n * Pure function — callers fetch accepted counterparts and ledger rows.\n *\n * @param candidates - Deduped, confidence-ordered digest candidates.\n * @param opts.viewerId - The digest recipient.\n * @param opts.acceptedCounterpartIds - userIds the viewer already connected with.\n * @param opts.deliveredRows - Committed ledger rows for the candidate ids.\n * @param opts.now - Clock override for tests.\n * @param opts.cooldownDays - Cooldown override (default {@link DIGEST_REDELIVERY_COOLDOWN_DAYS}).\n * @returns Surviving pool plus the set of candidate ids that are cooldown re-shows.\n */\nexport function selectDigestCandidates<T extends {\n id: string;\n status: string;\n actors: Array<{ userId: string; role: string }>;\n}>(\n candidates: T[],\n opts: {\n viewerId: string;\n acceptedCounterpartIds: ReadonlySet<string>;\n deliveredRows: DigestDeliveredRow[];\n now?: Date;\n cooldownDays?: number;\n },\n): { pool: T[]; redeliveryIds: Set<string> } {\n const { viewerId, acceptedCounterpartIds } = opts;\n\n // Rule 1: accepted-counterpart suppression (direct connections only).\n const afterAccepted = candidates.filter((opp) => {\n const viewerIsIntroducer = opp.actors.some(\n (a) => a.role === 'introducer' && a.userId === viewerId,\n );\n if (viewerIsIntroducer) return true;\n const counterpart = opp.actors.find(\n (a) => a.userId !== viewerId && a.role !== 'introducer',\n );\n return !counterpart || !acceptedCounterpartIds.has(counterpart.userId);\n });\n if (afterAccepted.length < candidates.length) {\n digestCandidatesLog.info('Accepted-counterpart suppression dropped candidates', {\n dropped: candidates.length - afterAccepted.length,\n total: candidates.length,\n });\n }\n\n // Rule 2: delivery-ledger dedup keyed (opportunityId, deliveredAtStatus).\n // Keep the LATEST committed delivery per key — cooldown measures time since\n // the user last saw the card, not since they first saw it.\n const lastDeliveredByKey = new Map<string, Date>();\n for (const row of opts.deliveredRows) {\n if (!(row.deliveredAt instanceof Date) || Number.isNaN(row.deliveredAt.getTime())) continue;\n const key = `${row.opportunityId}:${row.deliveredAtStatus}`;\n const existing = lastDeliveredByKey.get(key);\n if (!existing || row.deliveredAt > existing) lastDeliveredByKey.set(key, row.deliveredAt);\n }\n\n const fresh = afterAccepted.filter((opp) => !lastDeliveredByKey.has(`${opp.id}:${opp.status}`));\n if (fresh.length > 0) {\n if (fresh.length < afterAccepted.length) {\n digestCandidatesLog.info('Ledger dedup dropped already-shown candidates', {\n dropped: afterAccepted.length - fresh.length,\n });\n }\n return { pool: fresh, redeliveryIds: new Set<string>() };\n }\n\n // Rule 3: nothing fresh — re-show the least-recently-shown candidates past cooldown.\n const cooldownMs = (opts.cooldownDays ?? DIGEST_REDELIVERY_COOLDOWN_DAYS) * 86_400_000;\n const now = opts.now ?? new Date();\n const cooled = afterAccepted\n .map((opp) => ({ opp, at: lastDeliveredByKey.get(`${opp.id}:${opp.status}`) }))\n .filter((entry): entry is { opp: T; at: Date } =>\n entry.at instanceof Date && now.getTime() - entry.at.getTime() >= cooldownMs,\n )\n .sort((a, b) => a.at.getTime() - b.at.getTime());\n\n if (cooled.length > 0) {\n digestCandidatesLog.info('No fresh candidates — re-showing past-cooldown candidates', {\n count: cooled.length,\n });\n }\n return {\n pool: cooled.map((entry) => entry.opp),\n redeliveryIds: new Set(cooled.map((entry) => entry.opp.id)),\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"premise.analyzer.d.ts","sourceRoot":"/","sources":["premise/premise.analyzer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgExB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;EAoBlB,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEnE;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAA2C;;IAS3C,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAwBlG"}
1
+ {"version":3,"file":"premise.analyzer.d.ts","sourceRoot":"/","sources":["premise/premise.analyzer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiExB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;EAoBlB,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEnE;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAA2C;;IAS3C,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAwBlG"}
@@ -14,6 +14,7 @@ import { Timed } from "../shared/observability/performance.js";
14
14
  import { createStructuredModel } from "../shared/agent/model.config.js";
15
15
  import { invokeWithAbortSignal } from "../shared/agent/model-signal.js";
16
16
  const logger = protocolLogger("PremiseAnalyzer");
17
+ const invokeLog = protocolLogger("PremiseAnalyzer:invoke");
17
18
  const systemPrompt = `
18
19
  You are the Premise Analyzer for the Index Network — an intent-driven discovery protocol.
19
20
 
@@ -88,7 +89,7 @@ export class PremiseAnalyzer {
88
89
  });
89
90
  }
90
91
  async invoke(premiseText, profileContext) {
91
- logger.verbose(`[PremiseAnalyzer.invoke] Analyzing: "${premiseText.substring(0, 50)}..."`);
92
+ invokeLog.verbose('Analyzing premise text', { preview: premiseText.substring(0, 50) });
92
93
  const contextBlock = profileContext
93
94
  ? `\n# Speaker Profile (Context)\n${profileContext}\n`
94
95
  : "";
@@ -103,7 +104,7 @@ Classify this premise and score its felicity conditions.`;
103
104
  ];
104
105
  const result = await invokeWithAbortSignal(this.model, messages);
105
106
  const output = responseFormat.parse(result);
106
- logger.verbose(`[PremiseAnalyzer.invoke] Result: ${output.speechActType} entropy=${output.semanticEntropy}`);
107
+ invokeLog.verbose('Analysis result', { speechActType: output.speechActType, semanticEntropy: output.semanticEntropy });
107
108
  return output;
108
109
  }
109
110
  }
@@ -1 +1 @@
1
- {"version":3,"file":"premise.analyzer.js","sourceRoot":"/","sources":["premise/premise.analyzer.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,4CAA4C,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAExE,MAAM,MAAM,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAC;AAEjD,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDpB,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAC5B,+EAA+E;QAC/E,oCAAoC,CACrC;IACD,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC,QAAQ,CAC1D,6FAA6F,CAC9F;IACD,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CACpD,8EAA8E,CAC/E;IACD,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CACpD,8DAA8D,CAC/D;IACD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAClD,+DAA+D,CAChE;IACD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAChD,8EAA8E,CAC/E;CACF,CAAC,CAAC;AAIH;;GAEG;AACH,MAAM,OAAO,eAAe;IAG1B;QACE,IAAI,CAAC,KAAK,GAAG,qBAAqB,CAAC,iBAAiB,EAAE,cAAc,EAAE;YACpE,IAAI,EAAE,kBAAkB;SACzB,CAAC,CAAC;IACL,CAAC;IAGY,AAAN,KAAK,CAAC,MAAM,CAAC,WAAmB,EAAE,cAAuB;QAC9D,MAAM,CAAC,OAAO,CAAC,wCAAwC,WAAW,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;QAE3F,MAAM,YAAY,GAAG,cAAc;YACjC,CAAC,CAAC,kCAAkC,cAAc,IAAI;YACtD,CAAC,CAAC,EAAE,CAAC;QAEP,MAAM,MAAM,GAAG,GAAG,YAAY;;GAE/B,WAAW;;yDAE2C,CAAC;QAEtD,MAAM,QAAQ,GAAG;YACf,IAAI,aAAa,CAAC,YAAY,CAAC;YAC/B,IAAI,YAAY,CAAC,MAAM,CAAC;SACzB,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,CAAC,OAAO,CAAC,oCAAoC,MAAM,CAAC,aAAa,YAAY,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC;QAC7G,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAxBc;IADZ,KAAK,EAAE;;;;6CAwBP","sourcesContent":["import { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\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(\"PremiseAnalyzer\");\n\nconst systemPrompt = `\nYou are the Premise Analyzer for the Index Network — an intent-driven discovery protocol.\n\nYour job: classify a self-descriptive premise using adapted Speech Act Theory, then score its felicity conditions.\n\nA premise is a proposition a person asserts about themselves. It is NOT a desire or request (those are intents). Premises are conditions of possibility — facts about who someone is that ground opportunity discovery.\n\nAlways reason before classifying. Output reasoning first.\n\n═══════════════════════════════════════════════════\nSTEP 1 — CLASSIFY THE SPEECH ACT\n═══════════════════════════════════════════════════\n\nDECLARATIVE: The premise constitutes a fact about the speaker's identity, role, or status.\n Examples:\n · \"I am a climate-tech founder\" → DECLARATIVE\n · \"I hold a PhD in computational biology\" → DECLARATIVE\n · \"I am based in Berlin\" → DECLARATIVE\n · \"I am raising Series A\" → DECLARATIVE (constitutes current status)\n\nASSERTIVE: The premise describes a capability, experience, or characteristic.\n Examples:\n · \"I have 10 years of experience in distributed systems\" → ASSERTIVE\n · \"I built a collaboration platform used by 50k users\" → ASSERTIVE\n · \"I speak fluent Mandarin and German\" → ASSERTIVE\n · \"I specialize in zero-knowledge proofs\" → ASSERTIVE\n\n═══════════════════════════════════════════════════\nSTEP 2 — SCORE THE FELICITY CONDITIONS (0–100)\n═══════════════════════════════════════════════════\n\nAUTHORITY (Preparatory Condition)\n Does the speaker plausibly have standing to assert this?\n 100 → Highly specific, verifiable claim (\"I founded X in 2019\")\n 60 → Plausible but unverifiable (\"I have deep expertise in AI\")\n 20 → Implausible or grandiose (\"I am the world's leading expert\")\n\nSINCERITY (Sincerity Condition)\n Does the linguistic form suggest genuine self-description vs. aspiration?\n 100 → Present tense, first person, specific (\"I am a YC-backed founder\")\n 60 → Hedged or aspirational (\"I'm sort of getting into crypto\")\n 20 → Clearly aspirational masquerading as fact (\"I'm basically a VC\")\n\nCLARITY (Essential Condition)\n How specific and matchable is this premise?\n 100 → \"I build distributed database systems in Rust at a Series B startup\"\n 60 → \"I work in tech\" (clear direction, vague spec)\n 20 → \"I do things\" (barely informative)\n\nSEMANTIC ENTROPY → 0.0 to 1.0\n 0.0 = maximally constrained (role + domain + location + stage all specified)\n 1.0 = no constraints at all\n 0.0 example: \"I am a senior ML engineer at Google Brain in Mountain View\"\n 1.0 example: \"I'm a person\"\n`;\n\nconst responseFormat = z.object({\n reasoning: z.string().describe(\n \"Step-by-step analysis: (1) whether this is DECLARATIVE or ASSERTIVE and why, \" +\n \"(2) felicity condition assessment.\"\n ),\n speechActType: z.enum([\"DECLARATIVE\", \"ASSERTIVE\"]).describe(\n \"DECLARATIVE = constitutes identity/role/status; ASSERTIVE = describes capability/experience\"\n ),\n felicityAuthority: z.number().min(0).max(100).describe(\n \"Preparatory: does the speaker plausibly have standing to assert this (0-100)\"\n ),\n felicitySincerity: z.number().min(0).max(100).describe(\n \"Sincerity: genuine self-description vs. aspirational (0-100)\"\n ),\n felicityClarity: z.number().min(0).max(100).describe(\n \"Essential: how specific and matchable is this premise (0-100)\"\n ),\n semanticEntropy: z.number().min(0).max(1).describe(\n \"Constraint density: 0.0 = maximally specific, 1.0 = completely unconstrained\"\n ),\n});\n\nexport type PremiseAnalyzerOutput = z.infer<typeof responseFormat>;\n\n/**\n * Classifies a premise using adapted Speech Act Theory and scores felicity conditions.\n */\nexport class PremiseAnalyzer {\n private model: ReturnType<typeof createStructuredModel>;\n\n constructor() {\n this.model = createStructuredModel(\"premiseAnalyzer\", responseFormat, {\n name: \"premise_analyzer\"\n });\n }\n\n @Timed()\n public async invoke(premiseText: string, profileContext?: string): Promise<PremiseAnalyzerOutput> {\n logger.verbose(`[PremiseAnalyzer.invoke] Analyzing: \"${premiseText.substring(0, 50)}...\"`);\n\n const contextBlock = profileContext\n ? `\\n# Speaker Profile (Context)\\n${profileContext}\\n`\n : \"\";\n\n const prompt = `${contextBlock}\n# Premise to Analyze\n\"${premiseText}\"\n\nClassify this premise and score its felicity conditions.`;\n\n const messages = [\n new SystemMessage(systemPrompt),\n new HumanMessage(prompt),\n ];\n\n const result = await invokeWithAbortSignal(this.model, messages);\n const output = responseFormat.parse(result);\n\n logger.verbose(`[PremiseAnalyzer.invoke] Result: ${output.speechActType} entropy=${output.semanticEntropy}`);\n return output;\n }\n}\n"]}
1
+ {"version":3,"file":"premise.analyzer.js","sourceRoot":"/","sources":["premise/premise.analyzer.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,4CAA4C,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAExE,MAAM,MAAM,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAC;AACjD,MAAM,SAAS,GAAG,cAAc,CAAC,wBAAwB,CAAC,CAAC;AAE3D,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDpB,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAC5B,+EAA+E;QAC/E,oCAAoC,CACrC;IACD,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC,QAAQ,CAC1D,6FAA6F,CAC9F;IACD,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CACpD,8EAA8E,CAC/E;IACD,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CACpD,8DAA8D,CAC/D;IACD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAClD,+DAA+D,CAChE;IACD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAChD,8EAA8E,CAC/E;CACF,CAAC,CAAC;AAIH;;GAEG;AACH,MAAM,OAAO,eAAe;IAG1B;QACE,IAAI,CAAC,KAAK,GAAG,qBAAqB,CAAC,iBAAiB,EAAE,cAAc,EAAE;YACpE,IAAI,EAAE,kBAAkB;SACzB,CAAC,CAAC;IACL,CAAC;IAGY,AAAN,KAAK,CAAC,MAAM,CAAC,WAAmB,EAAE,cAAuB;QAC9D,SAAS,CAAC,OAAO,CAAC,wBAAwB,EAAE,EAAE,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAEvF,MAAM,YAAY,GAAG,cAAc;YACjC,CAAC,CAAC,kCAAkC,cAAc,IAAI;YACtD,CAAC,CAAC,EAAE,CAAC;QAEP,MAAM,MAAM,GAAG,GAAG,YAAY;;GAE/B,WAAW;;yDAE2C,CAAC;QAEtD,MAAM,QAAQ,GAAG;YACf,IAAI,aAAa,CAAC,YAAY,CAAC;YAC/B,IAAI,YAAY,CAAC,MAAM,CAAC;SACzB,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAE5C,SAAS,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,aAAa,EAAE,MAAM,CAAC,aAAa,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC;QACvH,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAxBc;IADZ,KAAK,EAAE;;;;6CAwBP","sourcesContent":["import { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\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(\"PremiseAnalyzer\");\nconst invokeLog = protocolLogger(\"PremiseAnalyzer:invoke\");\n\nconst systemPrompt = `\nYou are the Premise Analyzer for the Index Network — an intent-driven discovery protocol.\n\nYour job: classify a self-descriptive premise using adapted Speech Act Theory, then score its felicity conditions.\n\nA premise is a proposition a person asserts about themselves. It is NOT a desire or request (those are intents). Premises are conditions of possibility — facts about who someone is that ground opportunity discovery.\n\nAlways reason before classifying. Output reasoning first.\n\n═══════════════════════════════════════════════════\nSTEP 1 — CLASSIFY THE SPEECH ACT\n═══════════════════════════════════════════════════\n\nDECLARATIVE: The premise constitutes a fact about the speaker's identity, role, or status.\n Examples:\n · \"I am a climate-tech founder\" → DECLARATIVE\n · \"I hold a PhD in computational biology\" → DECLARATIVE\n · \"I am based in Berlin\" → DECLARATIVE\n · \"I am raising Series A\" → DECLARATIVE (constitutes current status)\n\nASSERTIVE: The premise describes a capability, experience, or characteristic.\n Examples:\n · \"I have 10 years of experience in distributed systems\" → ASSERTIVE\n · \"I built a collaboration platform used by 50k users\" → ASSERTIVE\n · \"I speak fluent Mandarin and German\" → ASSERTIVE\n · \"I specialize in zero-knowledge proofs\" → ASSERTIVE\n\n═══════════════════════════════════════════════════\nSTEP 2 — SCORE THE FELICITY CONDITIONS (0–100)\n═══════════════════════════════════════════════════\n\nAUTHORITY (Preparatory Condition)\n Does the speaker plausibly have standing to assert this?\n 100 → Highly specific, verifiable claim (\"I founded X in 2019\")\n 60 → Plausible but unverifiable (\"I have deep expertise in AI\")\n 20 → Implausible or grandiose (\"I am the world's leading expert\")\n\nSINCERITY (Sincerity Condition)\n Does the linguistic form suggest genuine self-description vs. aspiration?\n 100 → Present tense, first person, specific (\"I am a YC-backed founder\")\n 60 → Hedged or aspirational (\"I'm sort of getting into crypto\")\n 20 → Clearly aspirational masquerading as fact (\"I'm basically a VC\")\n\nCLARITY (Essential Condition)\n How specific and matchable is this premise?\n 100 → \"I build distributed database systems in Rust at a Series B startup\"\n 60 → \"I work in tech\" (clear direction, vague spec)\n 20 → \"I do things\" (barely informative)\n\nSEMANTIC ENTROPY → 0.0 to 1.0\n 0.0 = maximally constrained (role + domain + location + stage all specified)\n 1.0 = no constraints at all\n 0.0 example: \"I am a senior ML engineer at Google Brain in Mountain View\"\n 1.0 example: \"I'm a person\"\n`;\n\nconst responseFormat = z.object({\n reasoning: z.string().describe(\n \"Step-by-step analysis: (1) whether this is DECLARATIVE or ASSERTIVE and why, \" +\n \"(2) felicity condition assessment.\"\n ),\n speechActType: z.enum([\"DECLARATIVE\", \"ASSERTIVE\"]).describe(\n \"DECLARATIVE = constitutes identity/role/status; ASSERTIVE = describes capability/experience\"\n ),\n felicityAuthority: z.number().min(0).max(100).describe(\n \"Preparatory: does the speaker plausibly have standing to assert this (0-100)\"\n ),\n felicitySincerity: z.number().min(0).max(100).describe(\n \"Sincerity: genuine self-description vs. aspirational (0-100)\"\n ),\n felicityClarity: z.number().min(0).max(100).describe(\n \"Essential: how specific and matchable is this premise (0-100)\"\n ),\n semanticEntropy: z.number().min(0).max(1).describe(\n \"Constraint density: 0.0 = maximally specific, 1.0 = completely unconstrained\"\n ),\n});\n\nexport type PremiseAnalyzerOutput = z.infer<typeof responseFormat>;\n\n/**\n * Classifies a premise using adapted Speech Act Theory and scores felicity conditions.\n */\nexport class PremiseAnalyzer {\n private model: ReturnType<typeof createStructuredModel>;\n\n constructor() {\n this.model = createStructuredModel(\"premiseAnalyzer\", responseFormat, {\n name: \"premise_analyzer\"\n });\n }\n\n @Timed()\n public async invoke(premiseText: string, profileContext?: string): Promise<PremiseAnalyzerOutput> {\n invokeLog.verbose('Analyzing premise text', { preview: premiseText.substring(0, 50) });\n\n const contextBlock = profileContext\n ? `\\n# Speaker Profile (Context)\\n${profileContext}\\n`\n : \"\";\n\n const prompt = `${contextBlock}\n# Premise to Analyze\n\"${premiseText}\"\n\nClassify this premise and score its felicity conditions.`;\n\n const messages = [\n new SystemMessage(systemPrompt),\n new HumanMessage(prompt),\n ];\n\n const result = await invokeWithAbortSignal(this.model, messages);\n const output = responseFormat.parse(result);\n\n invokeLog.verbose('Analysis result', { speechActType: output.speechActType, semanticEntropy: output.semanticEntropy });\n return output;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"premise.decomposer.d.ts","sourceRoot":"/","sources":["premise/premise.decomposer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkIxB,QAAA,MAAM,iBAAiB;;;;;;;;;;;;EAQrB,CAAC;AAEH,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAalB,CAAC;AAEH,oFAAoF;AACpF,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACrE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,KAAK,CAA2C;;IAS3C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;CAiDnI"}
1
+ {"version":3,"file":"premise.decomposer.d.ts","sourceRoot":"/","sources":["premise/premise.decomposer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAmIxB,QAAA,MAAM,iBAAiB;;;;;;;;;;;;EAQrB,CAAC;AAEH,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAalB,CAAC;AAEH,oFAAoF;AACpF,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACrE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,KAAK,CAA2C;;IAS3C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;CAyDnI"}
@@ -14,6 +14,7 @@ import { Timed } from "../shared/observability/performance.js";
14
14
  import { createStructuredModel } from "../shared/agent/model.config.js";
15
15
  import { invokeWithAbortSignal } from "../shared/agent/model-signal.js";
16
16
  const logger = protocolLogger("PremiseDecomposer");
17
+ const invokeLog = protocolLogger("PremiseDecomposer:invoke");
17
18
  const systemPrompt = `
18
19
  You are the Premise Decomposer for the Index Network — an intent-driven discovery protocol.
19
20
 
@@ -157,7 +158,11 @@ export class PremiseDecomposer {
157
158
  });
158
159
  }
159
160
  async invoke(input, existingPremises, currentBio) {
160
- logger.verbose(`[PremiseDecomposer.invoke] Decomposing input (${input.length} chars, ${existingPremises?.length ?? 0} existing premise(s), bio: ${currentBio ? 'yes' : 'no'})`);
161
+ invokeLog.verbose('Decomposing input', {
162
+ inputChars: input.length,
163
+ existingPremiseCount: existingPremises?.length ?? 0,
164
+ hasBio: Boolean(currentBio),
165
+ });
161
166
  const existingBlock = existingPremises?.length
162
167
  ? `\n\nEXISTING PREMISES (retract by id when the input disavows them):\n${existingPremises
163
168
  .map((p) => `- id: ${p.id} · "${p.text}"`)
@@ -177,7 +182,7 @@ export class PremiseDecomposer {
177
182
  const knownIds = new Set((existingPremises ?? []).map((p) => p.id));
178
183
  const droppedIds = output.retractedPremiseIds.filter((id) => !knownIds.has(id));
179
184
  if (droppedIds.length > 0) {
180
- logger.warn(`[PremiseDecomposer.invoke] Dropped ${droppedIds.length} unknown retraction id(s)`, { droppedIds });
185
+ invokeLog.warn('Dropped unknown retraction ids', { count: droppedIds.length, droppedIds });
181
186
  }
182
187
  output.retractedPremiseIds = output.retractedPremiseIds.filter((id) => knownIds.has(id));
183
188
  // A revised bio is only meaningful when we offered one to revise. Also
@@ -193,7 +198,11 @@ export class PremiseDecomposer {
193
198
  else {
194
199
  output.revisedBio = normalizedBio;
195
200
  }
196
- logger.verbose(`[PremiseDecomposer.invoke] Extracted ${output.premises.length} premise(s), ${output.retractedPremiseIds.length} retraction(s), revisedBio: ${output.revisedBio ? 'yes' : 'no'}`);
201
+ invokeLog.verbose('Extracted premises and retractions', {
202
+ premiseCount: output.premises.length,
203
+ retractionCount: output.retractedPremiseIds.length,
204
+ hasRevisedBio: Boolean(output.revisedBio),
205
+ });
197
206
  return output;
198
207
  }
199
208
  }
@@ -1 +1 @@
1
- {"version":3,"file":"premise.decomposer.js","sourceRoot":"/","sources":["premise/premise.decomposer.ts"],"names":[],"mappings":"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","sourcesContent":["import { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\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(\"PremiseDecomposer\");\n\nconst systemPrompt = `\nYou are the Premise Decomposer for the Index Network — an intent-driven discovery protocol.\n\nYour job: decompose free-text input about a person into individual, atomic premises.\n\nA premise is a single self-descriptive proposition — a fact someone asserts about themselves.\nEach premise should be:\n1. **Atomic** — one fact per premise, not compound statements\n2. **First-person** — phrased as \"I am...\", \"I work at...\", \"I specialize in...\"\n3. **Self-descriptive** — about the person's identity, role, skills, interests, location, or context\n4. **Non-redundant** — do not repeat the same information in multiple premises\n\n═══════════════════════════════════════════════════\nINPUT TYPES\n═══════════════════════════════════════════════════\n\nYou may receive:\n- **Chat messages**: \"I'm a software engineer at Google, based in SF. I'm interested in AI safety.\"\n- **Bio text**: A paragraph describing someone's background\n- **Scraped content**: LinkedIn/GitHub/Twitter data about someone\n- **Mixed input**: Name, location, and free-text combined\n\n═══════════════════════════════════════════════════\nDECOMPOSITION RULES\n═══════════════════════════════════════════════════\n\n1. Extract every distinct factual claim about the person\n2. Convert third-person (\"She works at...\") to first-person (\"I work at...\")\n3. Separate compound statements: \"I know Python and Rust\" → two premises\n4. Capture the FULL breadth of self-description, not just job/title. In particular,\n do not drop:\n - **Skills & expertise** (\"I specialize in distributed systems\", \"I know Rust\")\n - **Interests & focus areas** (\"I am interested in AI safety\", \"I care about climate\")\n - **Background/narrative context** (education, prior roles, domains, affiliations)\n These map to what a profile would store as interests/skills/context — every such\n fact must surface as its own premise so nothing is lost.\n5. Classify each premise:\n - \\`assertive\\` — stable identity facts (role, skills, interests, education, location)\n - \\`contextual\\` — temporal/situational (current projects, events, fundraising status)\n6. For \\`contextual\\` premises, estimate \\`validityDays\\` — how many days the fact is\n likely to remain true from now (e.g. \"speaking next week\" → ~7, \"raising a Series A\"\n → ~120, \"currently building X\" → ~180). Use null for \\`assertive\\` premises, which\n have no natural expiry.\n7. Skip vague or uninformative statements (\"I like stuff\", \"I'm a person\")\n8. Skip desires, requests, or intents (\"I'm looking for...\", \"I want to...\") — those are intents, not premises\n9. If the input contains NO extractable premises (e.g. just \"Yes\" or \"Hello\"), return an empty array\n10. NEVER extract denials or removal instructions as premises. \"I have nothing to do with X\",\n \"I am not a Y\", or \"remove X from my profile\" are retractions, not facts — handle them\n via the retraction rules below and do NOT emit a premise for them.\n\n═════════════════════════════════════════════════\nRETRACTION RULES\n═════════════════════════════════════════════════\n\nWhen the message includes the user's EXISTING PREMISES (listed with ids), you must also\ndetect which of them the input disavows and return their ids in \\`retractedPremiseIds\\`.\n\nA premise must be retracted when the input:\n- Explicitly asks to remove it (\"remove all mentions of X\", \"delete the part about Y\")\n- Denies it (\"I have nothing to do with X\", \"I never worked at Y\", \"that's not true\")\n- States it no longer holds (\"I no longer live in Berlin\", \"I left Google\")\n- Directly contradicts it (\"I'm based in Istanbul\" contradicts \"I am based in Ankara\")\n\nRules:\n- Only return ids that appear in the provided EXISTING PREMISES list — never invent ids.\n- Retract EVERY existing premise that matches the disavowed topic, not just the first one.\n- Do not retract premises the input merely omits — silence is not disavowal.\n- If no existing premises are provided, or nothing is disavowed, return an empty array.\n- Retraction and extraction are independent: an input can retract old premises AND\n contribute new ones in the same pass.\n\n═══════════════════════════════════════════════════\nBIO REVISION RULES\n═══════════════════════════════════════════════════\n\nWhen the message includes the user's CURRENT BIO, check whether the input disavows,\nremoves, or corrects anything that appears in it.\n\n- If it does, return \\`revisedBio\\`: the bio rewritten with those facts removed or\n corrected. Preserve everything the input does not dispute — same tone, similar\n length, no inventions.\n- If the input explicitly rewrites the bio (\"update my bio to X\"), return that\n rewritten bio.\n- NEVER add new facts to the bio — new information becomes premises, not bio edits.\n Purely additive input (\"I also enjoy X\") must return revisedBio: null.\n- If the bio is unaffected, or no CURRENT BIO was provided, return null.\n- Never return a revised bio that still mentions a disavowed fact.\n\n═══════════════════════════════════════════════════\nEXAMPLES\n═══════════════════════════════════════════════════\n\nInput: \"I'm a climate tech founder based in Berlin. I have a PhD in renewable energy systems and I'm currently raising Series A.\"\nOutput:\n 1. \"I am a climate tech founder\" (assertive, validityDays: null)\n 2. \"I am based in Berlin\" (assertive, validityDays: null)\n 3. \"I hold a PhD in renewable energy systems\" (assertive, validityDays: null)\n 4. \"I am currently raising Series A\" (contextual, validityDays: 120)\n\nInput: \"John Doe is a senior ML engineer at Meta in Menlo Park. He has 8 years of experience in NLP and computer vision.\"\nOutput:\n 1. \"I am a senior ML engineer\" (assertive, validityDays: null)\n 2. \"I work at Meta\" (assertive, validityDays: null)\n 3. \"I am based in Menlo Park\" (assertive, validityDays: null)\n 4. \"I have 8 years of experience in NLP\" (assertive, validityDays: null)\n 5. \"I have experience in computer vision\" (assertive, validityDays: null)\n\nInput: \"Yes, create my profile\"\nOutput: [] (empty — no premises)\n\nInput: \"Remove all mentions of the HOPE programming language. I have nothing to do with it. I specialize in compiler design.\"\nExisting premises:\n - id: aaaa-1 · \"I am the creator of the HOPE programming language\"\n - id: aaaa-2 · \"I specialize in compiler design\"\n - id: aaaa-3 · \"I am based in Istanbul\"\nCurrent bio: \"Software engineer in Istanbul. Creator of the HOPE programming language. Specializes in compiler design.\"\nOutput:\n premises: [] (compiler design already exists as aaaa-2 — nothing new to add)\n retractedPremiseIds: [\"aaaa-1\"]\n revisedBio: \"Software engineer in Istanbul. Specializes in compiler design.\"\n`;\n\nconst premiseItemSchema = z.object({\n text: z.string().describe(\"The premise text in first person (e.g. 'I am a software engineer at Google')\"),\n tier: z.enum([\"assertive\", \"contextual\"]).describe(\n \"assertive = stable identity fact; contextual = temporal/situational\"\n ),\n validityDays: z.number().int().positive().nullable().describe(\n \"For contextual premises: estimated days the fact remains true from now (its validity window). null for assertive premises, which do not expire.\"\n ),\n});\n\nconst responseFormat = z.object({\n reasoning: z.string().describe(\n \"Brief analysis of what factual claims can be extracted from the input\"\n ),\n premises: z.array(premiseItemSchema).describe(\n \"Array of extracted premises. Empty if input contains no self-descriptive facts.\"\n ),\n retractedPremiseIds: z.array(z.string()).default([]).describe(\n \"Ids of EXISTING premises (from the provided list) that the input disavows, denies, or asks to remove. Empty when nothing is disavowed or no existing premises were provided.\"\n ),\n revisedBio: z.string().nullable().default(null).describe(\n \"When the input disavows or corrects facts that appear in the provided CURRENT BIO, the bio rewritten without those facts (preserving everything else). null when the bio is unaffected or no bio was provided.\"\n ),\n});\n\n/** An existing ACTIVE premise offered to the decomposer for retraction matching. */\nexport interface ExistingPremiseRef {\n id: string;\n text: string;\n}\n\nexport type PremiseDecomposerOutput = z.infer<typeof responseFormat>;\nexport type DecomposedPremise = z.infer<typeof premiseItemSchema>;\n\n/**\n * Decomposes free-text input (chat messages, bios, scraped content) into\n * individual atomic premises suitable for the premise graph.\n */\nexport class PremiseDecomposer {\n private model: ReturnType<typeof createStructuredModel>;\n\n constructor() {\n this.model = createStructuredModel(\"premiseDecomposer\", responseFormat, {\n name: \"premise_decomposer\",\n });\n }\n\n @Timed()\n public async invoke(input: string, existingPremises?: ExistingPremiseRef[], currentBio?: string): Promise<PremiseDecomposerOutput> {\n logger.verbose(`[PremiseDecomposer.invoke] Decomposing input (${input.length} chars, ${existingPremises?.length ?? 0} existing premise(s), bio: ${currentBio ? 'yes' : 'no'})`);\n\n const existingBlock = existingPremises?.length\n ? `\\n\\nEXISTING PREMISES (retract by id when the input disavows them):\\n${existingPremises\n .map((p) => `- id: ${p.id} · \"${p.text}\"`)\n .join(\"\\n\")}`\n : \"\";\n\n const bioBlock = currentBio?.trim()\n ? `\\n\\nCURRENT BIO (return revisedBio when the input disavows or corrects anything in it):\\n${currentBio.trim()}`\n : \"\";\n\n const prompt = `Decompose the following text into individual premises:\\n\\n${input}${existingBlock}${bioBlock}`;\n\n const messages = [\n new SystemMessage(systemPrompt),\n new HumanMessage(prompt),\n ];\n\n const result = await invokeWithAbortSignal(this.model, messages);\n const output = responseFormat.parse(result);\n\n // Guard against hallucinated ids: only keep retractions that reference premises we offered.\n const knownIds = new Set((existingPremises ?? []).map((p) => p.id));\n const droppedIds = output.retractedPremiseIds.filter((id) => !knownIds.has(id));\n if (droppedIds.length > 0) {\n logger.warn(`[PremiseDecomposer.invoke] Dropped ${droppedIds.length} unknown retraction id(s)`, { droppedIds });\n }\n output.retractedPremiseIds = output.retractedPremiseIds.filter((id) => knownIds.has(id));\n\n // A revised bio is only meaningful when we offered one to revise. Also\n // normalize structured-output quirks: literal \"null\", empty strings, and\n // no-op rewrites all mean \"bio unaffected\".\n const normalizedBio = output.revisedBio?.trim();\n if (\n !currentBio?.trim() ||\n !normalizedBio ||\n normalizedBio.toLowerCase() === 'null' ||\n normalizedBio === currentBio.trim()\n ) {\n output.revisedBio = null;\n } else {\n output.revisedBio = normalizedBio;\n }\n\n logger.verbose(`[PremiseDecomposer.invoke] Extracted ${output.premises.length} premise(s), ${output.retractedPremiseIds.length} retraction(s), revisedBio: ${output.revisedBio ? 'yes' : 'no'}`);\n return output;\n }\n}\n"]}
1
+ {"version":3,"file":"premise.decomposer.js","sourceRoot":"/","sources":["premise/premise.decomposer.ts"],"names":[],"mappings":"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","sourcesContent":["import { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\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(\"PremiseDecomposer\");\nconst invokeLog = protocolLogger(\"PremiseDecomposer:invoke\");\n\nconst systemPrompt = `\nYou are the Premise Decomposer for the Index Network — an intent-driven discovery protocol.\n\nYour job: decompose free-text input about a person into individual, atomic premises.\n\nA premise is a single self-descriptive proposition — a fact someone asserts about themselves.\nEach premise should be:\n1. **Atomic** — one fact per premise, not compound statements\n2. **First-person** — phrased as \"I am...\", \"I work at...\", \"I specialize in...\"\n3. **Self-descriptive** — about the person's identity, role, skills, interests, location, or context\n4. **Non-redundant** — do not repeat the same information in multiple premises\n\n═══════════════════════════════════════════════════\nINPUT TYPES\n═══════════════════════════════════════════════════\n\nYou may receive:\n- **Chat messages**: \"I'm a software engineer at Google, based in SF. I'm interested in AI safety.\"\n- **Bio text**: A paragraph describing someone's background\n- **Scraped content**: LinkedIn/GitHub/Twitter data about someone\n- **Mixed input**: Name, location, and free-text combined\n\n═══════════════════════════════════════════════════\nDECOMPOSITION RULES\n═══════════════════════════════════════════════════\n\n1. Extract every distinct factual claim about the person\n2. Convert third-person (\"She works at...\") to first-person (\"I work at...\")\n3. Separate compound statements: \"I know Python and Rust\" → two premises\n4. Capture the FULL breadth of self-description, not just job/title. In particular,\n do not drop:\n - **Skills & expertise** (\"I specialize in distributed systems\", \"I know Rust\")\n - **Interests & focus areas** (\"I am interested in AI safety\", \"I care about climate\")\n - **Background/narrative context** (education, prior roles, domains, affiliations)\n These map to what a profile would store as interests/skills/context — every such\n fact must surface as its own premise so nothing is lost.\n5. Classify each premise:\n - \\`assertive\\` — stable identity facts (role, skills, interests, education, location)\n - \\`contextual\\` — temporal/situational (current projects, events, fundraising status)\n6. For \\`contextual\\` premises, estimate \\`validityDays\\` — how many days the fact is\n likely to remain true from now (e.g. \"speaking next week\" → ~7, \"raising a Series A\"\n → ~120, \"currently building X\" → ~180). Use null for \\`assertive\\` premises, which\n have no natural expiry.\n7. Skip vague or uninformative statements (\"I like stuff\", \"I'm a person\")\n8. Skip desires, requests, or intents (\"I'm looking for...\", \"I want to...\") — those are intents, not premises\n9. If the input contains NO extractable premises (e.g. just \"Yes\" or \"Hello\"), return an empty array\n10. NEVER extract denials or removal instructions as premises. \"I have nothing to do with X\",\n \"I am not a Y\", or \"remove X from my profile\" are retractions, not facts — handle them\n via the retraction rules below and do NOT emit a premise for them.\n\n═════════════════════════════════════════════════\nRETRACTION RULES\n═════════════════════════════════════════════════\n\nWhen the message includes the user's EXISTING PREMISES (listed with ids), you must also\ndetect which of them the input disavows and return their ids in \\`retractedPremiseIds\\`.\n\nA premise must be retracted when the input:\n- Explicitly asks to remove it (\"remove all mentions of X\", \"delete the part about Y\")\n- Denies it (\"I have nothing to do with X\", \"I never worked at Y\", \"that's not true\")\n- States it no longer holds (\"I no longer live in Berlin\", \"I left Google\")\n- Directly contradicts it (\"I'm based in Istanbul\" contradicts \"I am based in Ankara\")\n\nRules:\n- Only return ids that appear in the provided EXISTING PREMISES list — never invent ids.\n- Retract EVERY existing premise that matches the disavowed topic, not just the first one.\n- Do not retract premises the input merely omits — silence is not disavowal.\n- If no existing premises are provided, or nothing is disavowed, return an empty array.\n- Retraction and extraction are independent: an input can retract old premises AND\n contribute new ones in the same pass.\n\n═══════════════════════════════════════════════════\nBIO REVISION RULES\n═══════════════════════════════════════════════════\n\nWhen the message includes the user's CURRENT BIO, check whether the input disavows,\nremoves, or corrects anything that appears in it.\n\n- If it does, return \\`revisedBio\\`: the bio rewritten with those facts removed or\n corrected. Preserve everything the input does not dispute — same tone, similar\n length, no inventions.\n- If the input explicitly rewrites the bio (\"update my bio to X\"), return that\n rewritten bio.\n- NEVER add new facts to the bio — new information becomes premises, not bio edits.\n Purely additive input (\"I also enjoy X\") must return revisedBio: null.\n- If the bio is unaffected, or no CURRENT BIO was provided, return null.\n- Never return a revised bio that still mentions a disavowed fact.\n\n═══════════════════════════════════════════════════\nEXAMPLES\n═══════════════════════════════════════════════════\n\nInput: \"I'm a climate tech founder based in Berlin. I have a PhD in renewable energy systems and I'm currently raising Series A.\"\nOutput:\n 1. \"I am a climate tech founder\" (assertive, validityDays: null)\n 2. \"I am based in Berlin\" (assertive, validityDays: null)\n 3. \"I hold a PhD in renewable energy systems\" (assertive, validityDays: null)\n 4. \"I am currently raising Series A\" (contextual, validityDays: 120)\n\nInput: \"John Doe is a senior ML engineer at Meta in Menlo Park. He has 8 years of experience in NLP and computer vision.\"\nOutput:\n 1. \"I am a senior ML engineer\" (assertive, validityDays: null)\n 2. \"I work at Meta\" (assertive, validityDays: null)\n 3. \"I am based in Menlo Park\" (assertive, validityDays: null)\n 4. \"I have 8 years of experience in NLP\" (assertive, validityDays: null)\n 5. \"I have experience in computer vision\" (assertive, validityDays: null)\n\nInput: \"Yes, create my profile\"\nOutput: [] (empty — no premises)\n\nInput: \"Remove all mentions of the HOPE programming language. I have nothing to do with it. I specialize in compiler design.\"\nExisting premises:\n - id: aaaa-1 · \"I am the creator of the HOPE programming language\"\n - id: aaaa-2 · \"I specialize in compiler design\"\n - id: aaaa-3 · \"I am based in Istanbul\"\nCurrent bio: \"Software engineer in Istanbul. Creator of the HOPE programming language. Specializes in compiler design.\"\nOutput:\n premises: [] (compiler design already exists as aaaa-2 — nothing new to add)\n retractedPremiseIds: [\"aaaa-1\"]\n revisedBio: \"Software engineer in Istanbul. Specializes in compiler design.\"\n`;\n\nconst premiseItemSchema = z.object({\n text: z.string().describe(\"The premise text in first person (e.g. 'I am a software engineer at Google')\"),\n tier: z.enum([\"assertive\", \"contextual\"]).describe(\n \"assertive = stable identity fact; contextual = temporal/situational\"\n ),\n validityDays: z.number().int().positive().nullable().describe(\n \"For contextual premises: estimated days the fact remains true from now (its validity window). null for assertive premises, which do not expire.\"\n ),\n});\n\nconst responseFormat = z.object({\n reasoning: z.string().describe(\n \"Brief analysis of what factual claims can be extracted from the input\"\n ),\n premises: z.array(premiseItemSchema).describe(\n \"Array of extracted premises. Empty if input contains no self-descriptive facts.\"\n ),\n retractedPremiseIds: z.array(z.string()).default([]).describe(\n \"Ids of EXISTING premises (from the provided list) that the input disavows, denies, or asks to remove. Empty when nothing is disavowed or no existing premises were provided.\"\n ),\n revisedBio: z.string().nullable().default(null).describe(\n \"When the input disavows or corrects facts that appear in the provided CURRENT BIO, the bio rewritten without those facts (preserving everything else). null when the bio is unaffected or no bio was provided.\"\n ),\n});\n\n/** An existing ACTIVE premise offered to the decomposer for retraction matching. */\nexport interface ExistingPremiseRef {\n id: string;\n text: string;\n}\n\nexport type PremiseDecomposerOutput = z.infer<typeof responseFormat>;\nexport type DecomposedPremise = z.infer<typeof premiseItemSchema>;\n\n/**\n * Decomposes free-text input (chat messages, bios, scraped content) into\n * individual atomic premises suitable for the premise graph.\n */\nexport class PremiseDecomposer {\n private model: ReturnType<typeof createStructuredModel>;\n\n constructor() {\n this.model = createStructuredModel(\"premiseDecomposer\", responseFormat, {\n name: \"premise_decomposer\",\n });\n }\n\n @Timed()\n public async invoke(input: string, existingPremises?: ExistingPremiseRef[], currentBio?: string): Promise<PremiseDecomposerOutput> {\n invokeLog.verbose('Decomposing input', {\n inputChars: input.length,\n existingPremiseCount: existingPremises?.length ?? 0,\n hasBio: Boolean(currentBio),\n });\n\n const existingBlock = existingPremises?.length\n ? `\\n\\nEXISTING PREMISES (retract by id when the input disavows them):\\n${existingPremises\n .map((p) => `- id: ${p.id} · \"${p.text}\"`)\n .join(\"\\n\")}`\n : \"\";\n\n const bioBlock = currentBio?.trim()\n ? `\\n\\nCURRENT BIO (return revisedBio when the input disavows or corrects anything in it):\\n${currentBio.trim()}`\n : \"\";\n\n const prompt = `Decompose the following text into individual premises:\\n\\n${input}${existingBlock}${bioBlock}`;\n\n const messages = [\n new SystemMessage(systemPrompt),\n new HumanMessage(prompt),\n ];\n\n const result = await invokeWithAbortSignal(this.model, messages);\n const output = responseFormat.parse(result);\n\n // Guard against hallucinated ids: only keep retractions that reference premises we offered.\n const knownIds = new Set((existingPremises ?? []).map((p) => p.id));\n const droppedIds = output.retractedPremiseIds.filter((id) => !knownIds.has(id));\n if (droppedIds.length > 0) {\n invokeLog.warn('Dropped unknown retraction ids', { count: droppedIds.length, droppedIds });\n }\n output.retractedPremiseIds = output.retractedPremiseIds.filter((id) => knownIds.has(id));\n\n // A revised bio is only meaningful when we offered one to revise. Also\n // normalize structured-output quirks: literal \"null\", empty strings, and\n // no-op rewrites all mean \"bio unaffected\".\n const normalizedBio = output.revisedBio?.trim();\n if (\n !currentBio?.trim() ||\n !normalizedBio ||\n normalizedBio.toLowerCase() === 'null' ||\n normalizedBio === currentBio.trim()\n ) {\n output.revisedBio = null;\n } else {\n output.revisedBio = normalizedBio;\n }\n\n invokeLog.verbose('Extracted premises and retractions', {\n premiseCount: output.premises.length,\n retractionCount: output.retractedPremiseIds.length,\n hasRevisedBio: Boolean(output.revisedBio),\n });\n return output;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"premise.graph.d.ts","sourceRoot":"/","sources":["premise/premise.graph.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAKtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAC;AACxG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4CAA4C,CAAC;AAG3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AA6BtE;;GAEG;AACH,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,eAAe;gBAHf,QAAQ,EAAE,oBAAoB,EAC9B,QAAQ,EAAE,QAAQ,EAClB,cAAc,GAAE,cAAqC,EACrD,eAAe,GAAE;QAAE,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;KAA0B;IAG3I;;;;OAIG;IACI,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAmK0B,MAAM;gCAAkB,MAAM;;;;;CA6F3E"}
1
+ {"version":3,"file":"premise.graph.d.ts","sourceRoot":"/","sources":["premise/premise.graph.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAKtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAC;AACxG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4CAA4C,CAAC;AAG3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAmCtE;;GAEG;AACH,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,eAAe;gBAHf,QAAQ,EAAE,oBAAoB,EAC9B,QAAQ,EAAE,QAAQ,EAClB,cAAc,GAAE,cAAqC,EACrD,eAAe,GAAE;QAAE,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;KAA0B;IAG3I;;;;OAIG;IACI,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAqK0B,MAAM;gCAAkB,MAAM;;;;;CA6F3E"}
@@ -8,6 +8,12 @@ import { scopeFromNetworkId } from "../shared/agent/tool.scope.js";
8
8
  import { protocolLogger } from "../shared/observability/protocol.logger.js";
9
9
  import { timed } from "../shared/observability/performance.js";
10
10
  const logger = protocolLogger("PremiseGraphFactory");
11
+ const queryLog = protocolLogger("PremiseGraph:query");
12
+ const analyzeLog = protocolLogger("PremiseGraph:analyze");
13
+ const embedLog = protocolLogger("PremiseGraph:embed");
14
+ const persistLog = protocolLogger("PremiseGraph:persist");
15
+ const indexLog = protocolLogger("PremiseGraph:index");
16
+ const dedupeLog = protocolLogger("PremiseGraph:dedupe");
11
17
  /**
12
18
  * Minimum cosine similarity (0-1) at which a freshly-decomposed premise is treated
13
19
  * as a near-duplicate of an existing ACTIVE premise for the same user and skipped
@@ -53,7 +59,7 @@ export class PremiseGraphFactory {
53
59
  const indexer = this.premiseIndexer;
54
60
  const queryNode = async (state) => {
55
61
  return timed("PremiseGraph.query", async () => {
56
- logger.verbose(`[PremiseGraph.query] Fetching premises for user ${state.userId}`);
62
+ queryLog.verbose('Fetching premises for user', { userId: state.userId });
57
63
  const premises = await this.database.getPremisesForUser(state.userId, 'ACTIVE');
58
64
  return {
59
65
  readResult: {
@@ -68,7 +74,7 @@ export class PremiseGraphFactory {
68
74
  if (!state.assertionText) {
69
75
  return { error: "assertionText is required for create/update mode" };
70
76
  }
71
- logger.verbose(`[PremiseGraph.analyze] Analyzing: "${state.assertionText.substring(0, 50)}..."`);
77
+ analyzeLog.verbose('Analyzing assertion text', { preview: state.assertionText.substring(0, 50) });
72
78
  const start = Date.now();
73
79
  const result = await analyzer.invoke(state.assertionText);
74
80
  const timing = {
@@ -92,7 +98,7 @@ export class PremiseGraphFactory {
92
98
  if (!state.assertionText) {
93
99
  return { error: "assertionText is required for embedding" };
94
100
  }
95
- logger.verbose(`[PremiseGraph.embed] Generating embedding for premise`);
101
+ embedLog.verbose(`Generating embedding for premise`);
96
102
  // Embedder.generate returns number[] | number[][], cast for single string input
97
103
  const embedding = await this.embedder.generate(state.assertionText, undefined, getAbortSignalConfig());
98
104
  return { embedding };
@@ -121,7 +127,11 @@ export class PremiseGraphFactory {
121
127
  threshold: DEDUP_SIMILARITY_THRESHOLD,
122
128
  });
123
129
  if (match) {
124
- logger.verbose(`[PremiseGraph.dedupe] Skipping near-duplicate (similarity=${match.similarity.toFixed(3)} >= ${DEDUP_SIMILARITY_THRESHOLD}) of premise ${match.premiseId}`);
130
+ dedupeLog.verbose('Skipping near-duplicate premise', {
131
+ similarity: Number(match.similarity.toFixed(3)),
132
+ threshold: DEDUP_SIMILARITY_THRESHOLD,
133
+ premiseId: match.premiseId,
134
+ });
125
135
  return { duplicateOf: match };
126
136
  }
127
137
  return {};
@@ -135,7 +145,7 @@ export class PremiseGraphFactory {
135
145
  return { error: "targetPremiseId is required for update mode" };
136
146
  }
137
147
  if (state.operationMode === 'update' && state.targetPremiseId) {
138
- logger.verbose(`[PremiseGraph.persist] Updating premise ${state.targetPremiseId}`);
148
+ persistLog.verbose('Updating premise', { premiseId: state.targetPremiseId });
139
149
  const updated = await this.database.updatePremise(state.targetPremiseId, {
140
150
  assertion: {
141
151
  text: state.assertionText,
@@ -151,7 +161,7 @@ export class PremiseGraphFactory {
151
161
  });
152
162
  return { premise: updated };
153
163
  }
154
- logger.verbose(`[PremiseGraph.persist] Creating new premise for user ${state.userId}`);
164
+ persistLog.verbose('Creating new premise for user', { userId: state.userId });
155
165
  // Provenance confidence: prefer an explicit caller-supplied value; otherwise
156
166
  // derive it from the analyzer's felicity scores (how authoritative, sincere,
157
167
  // and clear the assertion is) rather than a blanket 1.0, so the stored
@@ -184,7 +194,7 @@ export class PremiseGraphFactory {
184
194
  return timed("PremiseGraph.index", async () => {
185
195
  if (!state.premise)
186
196
  return {};
187
- logger.verbose(`[PremiseGraph.index] Scoring premise against user networks`);
197
+ indexLog.verbose(`Scoring premise against user networks`);
188
198
  const assignmentMemberships = await this.database.getAssignmentNetworkMembershipsForUser(state.userId);
189
199
  const requestScope = state.scopeType && state.scopeId
190
200
  ? { scopeType: state.scopeType, scopeId: state.scopeId }
@@ -239,10 +249,10 @@ export class PremiseGraphFactory {
239
249
  }
240
250
  }
241
251
  catch (err) {
242
- logger.verbose(`[PremiseGraph.index] Failed to score network ${networkId}, skipping: ${err}`);
252
+ indexLog.verbose('Failed to score network, skipping', { networkId, error: err });
243
253
  }
244
254
  }
245
- logger.verbose(`[PremiseGraph.index] Assigned to ${assignments.length} networks`);
255
+ indexLog.verbose('Assigned to networks', { count: assignments.length });
246
256
  return { networkAssignments: assignments, agentTimings };
247
257
  });
248
258
  };