@indexnetwork/protocol 11.0.1-rc.455.1 → 11.0.2-rc.456.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -51,6 +51,7 @@ pin a supported release, use `latest`.
51
51
  enrichment are unchanged.
52
52
 
53
53
  ### Added
54
+ - Add a live answer-first signal-intake eval with unrelated, relevant, and no-bridge profile cases plus provider-free corpus, runner, and scorer checks.
54
55
  - Add the protocol-only Guided Atlas, deterministic architecture inventory
55
56
  generator, and source-evidenced Configuration Lab. The atlas explains
56
57
  normative concepts, the current `packages/protocol` reference implementation,
@@ -69,6 +70,7 @@ pin a supported release, use `latest`.
69
70
  `SynthesisResult` types. Minor bump: additive surface only.
70
71
 
71
72
  ### Changed
73
+ - Make fast signal-intake follow-ups answer-first with a two-stage model boundary: an answer-only core call chooses the missing axis and supplies two or three concrete domain options, then an isolated bridge call may append at most one premise-derived profile option so an existing profile theme cannot dominate or reorder a newly stated intent.
72
74
  - Remove unsupported deprecated source/deep forwarding shims after migrating repository consumers to canonical modules; stable package-root exports are unchanged.
73
75
  - Add a fail-closed isolated provider-free test gate (10.1.1). Tooling-only
74
76
  safety foundation; no runtime or public API behavior changes.
@@ -56,9 +56,10 @@ export interface SynthesisResult {
56
56
  }
57
57
  declare const followUpPlanSchema: z.ZodObject<{
58
58
  questions: z.ZodArray<z.ZodObject<{
59
+ missingAxis: z.ZodEnum<["purpose", "desired_attributes", "exchange", "constraint"]>;
59
60
  title: z.ZodString;
60
61
  prompt: z.ZodString;
61
- options: z.ZodArray<z.ZodObject<{
62
+ answerGroundedOptions: z.ZodArray<z.ZodObject<{
62
63
  label: z.ZodString;
63
64
  description: z.ZodString;
64
65
  }, "strip", z.ZodTypeAny, {
@@ -71,45 +72,92 @@ declare const followUpPlanSchema: z.ZodObject<{
71
72
  multiSelect: z.ZodBoolean;
72
73
  }, "strip", z.ZodTypeAny, {
73
74
  prompt: string;
74
- options: {
75
+ title: string;
76
+ multiSelect: boolean;
77
+ missingAxis: "purpose" | "desired_attributes" | "exchange" | "constraint";
78
+ answerGroundedOptions: {
75
79
  label: string;
76
80
  description: string;
77
81
  }[];
78
- title: string;
79
- multiSelect: boolean;
80
82
  }, {
81
83
  prompt: string;
82
- options: {
84
+ title: string;
85
+ multiSelect: boolean;
86
+ missingAxis: "purpose" | "desired_attributes" | "exchange" | "constraint";
87
+ answerGroundedOptions: {
83
88
  label: string;
84
89
  description: string;
85
90
  }[];
86
- title: string;
87
- multiSelect: boolean;
88
91
  }>, "many">;
89
92
  plannedFollowUpCount: z.ZodNumber;
90
93
  }, "strip", z.ZodTypeAny, {
91
94
  questions: {
92
95
  prompt: string;
93
- options: {
96
+ title: string;
97
+ multiSelect: boolean;
98
+ missingAxis: "purpose" | "desired_attributes" | "exchange" | "constraint";
99
+ answerGroundedOptions: {
94
100
  label: string;
95
101
  description: string;
96
102
  }[];
97
- title: string;
98
- multiSelect: boolean;
99
103
  }[];
100
104
  plannedFollowUpCount: number;
101
105
  }, {
102
106
  questions: {
103
107
  prompt: string;
104
- options: {
108
+ title: string;
109
+ multiSelect: boolean;
110
+ missingAxis: "purpose" | "desired_attributes" | "exchange" | "constraint";
111
+ answerGroundedOptions: {
105
112
  label: string;
106
113
  description: string;
107
114
  }[];
108
- title: string;
109
- multiSelect: boolean;
110
115
  }[];
111
116
  plannedFollowUpCount: number;
112
117
  }>;
118
+ declare const profileBridgePlanSchema: z.ZodObject<{
119
+ bridges: z.ZodArray<z.ZodObject<{
120
+ questionIndex: z.ZodNumber;
121
+ profileBridgeOption: z.ZodNullable<z.ZodObject<{
122
+ label: z.ZodString;
123
+ description: z.ZodString;
124
+ }, "strip", z.ZodTypeAny, {
125
+ label: string;
126
+ description: string;
127
+ }, {
128
+ label: string;
129
+ description: string;
130
+ }>>;
131
+ }, "strip", z.ZodTypeAny, {
132
+ questionIndex: number;
133
+ profileBridgeOption: {
134
+ label: string;
135
+ description: string;
136
+ } | null;
137
+ }, {
138
+ questionIndex: number;
139
+ profileBridgeOption: {
140
+ label: string;
141
+ description: string;
142
+ } | null;
143
+ }>, "many">;
144
+ }, "strip", z.ZodTypeAny, {
145
+ bridges: {
146
+ questionIndex: number;
147
+ profileBridgeOption: {
148
+ label: string;
149
+ description: string;
150
+ } | null;
151
+ }[];
152
+ }, {
153
+ bridges: {
154
+ questionIndex: number;
155
+ profileBridgeOption: {
156
+ label: string;
157
+ description: string;
158
+ } | null;
159
+ }[];
160
+ }>;
113
161
  /** Static round-1 question used only when pack generation fails outright. */
114
162
  export declare const FALLBACK_WHO_QUESTION: IntakePackQuestion;
115
163
  /** Static round-2 question used when live generation fails. */
@@ -124,12 +172,14 @@ export declare function answerLabel(answer: IntakeAnswer): string;
124
172
  /** Runs the two live stages of the fast intake funnel: follow-up planning and synthesis. */
125
173
  export declare class SignalIntakeOrchestrator {
126
174
  private readonly plannerModel;
175
+ private readonly profileBridgeModel;
127
176
  private readonly synthesisModel;
128
177
  /**
129
178
  * @param models - Optional injected structured models. Tests pass stubs.
130
179
  */
131
180
  constructor(models?: {
132
181
  planner?: Runnable<BaseLanguageModelInput, z.infer<typeof followUpPlanSchema>>;
182
+ profileBridge?: Runnable<BaseLanguageModelInput, z.infer<typeof profileBridgePlanSchema>>;
133
183
  synthesis?: Runnable<BaseLanguageModelInput, SynthesisResult>;
134
184
  });
135
185
  /**
@@ -10,16 +10,35 @@ import { HumanMessage, SystemMessage } from "@langchain/core/messages";
10
10
  import { z } from "zod";
11
11
  import { createStructuredModel } from "../../shared/agent/model.config.js";
12
12
  import { normalizeIntakePack } from "./intake.pack.generator.js";
13
- const questionSchema = z.object({
13
+ const optionSchema = z.object({
14
+ label: z.string().min(1),
15
+ description: z.string(),
16
+ });
17
+ const answerFirstQuestionSchema = z.object({
18
+ missingAxis: z
19
+ .enum(["purpose", "desired_attributes", "exchange", "constraint"])
20
+ .describe("The unanswered decision axis selected only from the answered intake rounds."),
14
21
  title: z.string(),
15
22
  prompt: z.string().min(1),
16
- options: z.array(z.object({ label: z.string().min(1), description: z.string() })),
23
+ answerGroundedOptions: z
24
+ .array(optionSchema)
25
+ .min(2)
26
+ .max(3)
27
+ .describe("Two or three distinct choices derived only from the answered intake rounds."),
17
28
  multiSelect: z.boolean(),
18
29
  });
19
30
  const followUpPlanSchema = z.object({
20
- questions: z.array(questionSchema),
31
+ questions: z.array(answerFirstQuestionSchema),
21
32
  plannedFollowUpCount: z.number().int().min(0),
22
33
  });
34
+ const profileBridgePlanSchema = z.object({
35
+ bridges: z.array(z.object({
36
+ questionIndex: z.number().int().min(0),
37
+ profileBridgeOption: optionSchema
38
+ .nullable()
39
+ .describe("One natural profile intersection, or null when the bridge would be forced."),
40
+ })),
41
+ });
23
42
  const synthesisSchema = z.object({
24
43
  description: z.string().min(1),
25
44
  lookingFor: z.string().min(1),
@@ -49,18 +68,37 @@ export const FALLBACK_BRING_QUESTION = {
49
68
  ],
50
69
  multiSelect: false,
51
70
  };
52
- const PLAN_SYSTEM_PROMPT = `You plan and write follow-up intake questions for a networking product.
71
+ const PLAN_SYSTEM_PROMPT = `You plan and write answer-first follow-up intake questions for a networking product.
72
+
73
+ You receive ONLY the answered intake rounds. Use them to choose the next missing
74
+ axis, write a standalone prompt that names the newly stated person or domain, and
75
+ create 2-3 meaningfully distinct answerGroundedOptions. Choose the most useful
76
+ unanswered axis from: purpose, desired_attributes, exchange, or constraint. Never
77
+ re-ask an axis the rounds already answer. Do not infer a professional background,
78
+ industry, capability, or commercial goal that the rounds do not state.
79
+
80
+ Every answerGroundedOption must be visibly anchored to the stated person or domain
81
+ in its label or description and represent a concrete, distinct path. Generic labels
82
+ such as "Learn", "Share", "Collaborate", or "Networking" are invalid when they do
83
+ not say what the user would learn, share, collaborate on, or network about.
84
+ - Scuba divers: "Learn diving techniques", "Find dive buddies", "Share dive stories".
85
+ - Climate founders: "Compare climate sectors", "Find climate peers", "Discuss adaptation".
53
86
 
54
- Given a brief about the person and the intake rounds they already answered, decide
55
- how many further questions (up to the stated maximum) would make their signal
56
- specific enough to match on, and write them. Each question is one concise prompt
57
- with 3-4 concrete options grounded in the brief and the previous answers; each
58
- option has a short label and a one-line description. Set multiSelect true only
59
- when several options can genuinely apply together. Never re-ask a dimension that
60
- is already answered; skip a dimension the brief already covers. Never expose raw
61
- JSON, IDs, or internal vocabulary. plannedFollowUpCount is the TOTAL number of
62
- follow-up questions the interview should contain, including any returned now;
63
- when the input already fixes it, echo that value unchanged.`;
87
+ Each option needs a short label and a one-line description. Set multiSelect true
88
+ only when several options can genuinely apply together. Never expose raw JSON,
89
+ IDs, or internal vocabulary. plannedFollowUpCount is the TOTAL number of follow-up
90
+ questions the interview should contain, including any returned now; when the input
91
+ already fixes it, echo that value unchanged.`;
92
+ const PROFILE_BRIDGE_SYSTEM_PROMPT = `You may append one optional profile bridge to each already-written signal-intake question.
93
+
94
+ The question, missing axis, and answer-grounded options are immutable. For each
95
+ questionIndex, compare them with the profile brief and return either one genuinely
96
+ useful profileBridgeOption or null. A bridge is useful only when it creates a
97
+ natural additional path that is not already represented. Never rewrite, replace,
98
+ remove, or reclassify a core option. Never return more than one bridge per question.
99
+ When the profile theme is unrelated or the intersection would feel forced, return
100
+ null. Running club + investor may support one sponsorship bridge; scuba divers +
101
+ pianist should return null.`;
64
102
  const SYNTHESIS_SYSTEM_PROMPT = `You write one clear signal for a networking product.
65
103
 
66
104
  Combine the brief and the person's answers into a specific description of who they
@@ -79,6 +117,37 @@ community constraint. Never invent facts beyond the brief and the answers.`;
79
117
  export function answerLabel(answer) {
80
118
  return [...answer.selectedOptions, answer.freeText?.trim() ?? ""].filter(Boolean).join(", ");
81
119
  }
120
+ function normalizeFollowUpQuestion(question, profileBridge, brief) {
121
+ const seen = new Set();
122
+ const normalizeOption = (option) => {
123
+ const label = option.label.trim();
124
+ const key = label.toLocaleLowerCase();
125
+ if (!label || seen.has(key))
126
+ return null;
127
+ seen.add(key);
128
+ return { label, description: option.description.trim() };
129
+ };
130
+ const answerGroundedOptions = question.answerGroundedOptions
131
+ .map(normalizeOption)
132
+ .filter((option) => option !== null)
133
+ .slice(0, 3);
134
+ if (answerGroundedOptions.length < 2) {
135
+ throw new Error("Follow-up question needs at least two answer-grounded options.");
136
+ }
137
+ const profileBridgeOption = profileBridge ? normalizeOption(profileBridge) : null;
138
+ return normalizeIntakePack({
139
+ brief,
140
+ question: {
141
+ title: question.title,
142
+ prompt: question.prompt,
143
+ options: [
144
+ ...answerGroundedOptions,
145
+ ...(profileBridgeOption ? [profileBridgeOption] : []),
146
+ ],
147
+ multiSelect: question.multiSelect,
148
+ },
149
+ }).question;
150
+ }
82
151
  /** Runs the two live stages of the fast intake funnel: follow-up planning and synthesis. */
83
152
  export class SignalIntakeOrchestrator {
84
153
  /**
@@ -87,6 +156,8 @@ export class SignalIntakeOrchestrator {
87
156
  constructor(models) {
88
157
  this.plannerModel = models?.planner
89
158
  ?? createStructuredModel("signalIntakePack", followUpPlanSchema);
159
+ this.profileBridgeModel = models?.profileBridge
160
+ ?? createStructuredModel("signalIntakePack", profileBridgePlanSchema, { name: "signal_intake_profile_bridges" });
90
161
  this.synthesisModel = models?.synthesis
91
162
  ?? createStructuredModel("signalIntakePack", synthesisSchema);
92
163
  }
@@ -107,11 +178,29 @@ export class SignalIntakeOrchestrator {
107
178
  try {
108
179
  const raw = await this.plannerModel.invoke([
109
180
  new SystemMessage(PLAN_SYSTEM_PROMPT),
110
- new HumanMessage(`Brief:\n${input.brief}\n\n${roundsText}\n\nWrite up to ${input.maxFollowUps} follow-up question(s).${lockedLine}`),
181
+ new HumanMessage(`ANSWERED ROUNDS:\n${roundsText}\n\nWrite up to ${input.maxFollowUps} follow-up question(s).${lockedLine}`),
111
182
  ]);
112
- const questions = raw.questions
113
- .slice(0, input.maxFollowUps)
114
- .map((q) => normalizeIntakePack({ brief: input.brief, question: q }).question);
183
+ const coreQuestions = raw.questions.slice(0, input.maxFollowUps);
184
+ const bridgeByQuestion = new Map();
185
+ if (coreQuestions.length > 0 && input.brief.trim()) {
186
+ try {
187
+ const bridgePlan = await this.profileBridgeModel.invoke([
188
+ new SystemMessage(PROFILE_BRIDGE_SYSTEM_PROMPT),
189
+ new HumanMessage(`ANSWERED ROUNDS:\n${roundsText}\n\nPROFILE BRIEF:\n${input.brief}\n\nIMMUTABLE CORE QUESTIONS:\n${JSON.stringify(coreQuestions, null, 2)}\n\nReturn one bridge decision for each questionIndex.`),
190
+ ]);
191
+ for (const bridge of bridgePlan.bridges) {
192
+ if (bridge.questionIndex < coreQuestions.length
193
+ && !bridgeByQuestion.has(bridge.questionIndex)) {
194
+ bridgeByQuestion.set(bridge.questionIndex, bridge.profileBridgeOption);
195
+ }
196
+ }
197
+ }
198
+ catch {
199
+ // Profile personalization is optional. A bridge-model failure must not
200
+ // discard valid answer-grounded questions from the primary model.
201
+ }
202
+ }
203
+ const questions = coreQuestions.map((question, index) => normalizeFollowUpQuestion(question, bridgeByQuestion.get(index) ?? null, input.brief));
115
204
  // Backstop: while budget remains, a successful empty plan must not
116
205
  // silently shrink the interview; serve the static fallback instead.
117
206
  if (questions.length === 0 && input.maxFollowUps > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indexnetwork/protocol",
3
- "version": "11.0.1-rc.455.1",
3
+ "version": "11.0.2-rc.456.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -40,6 +40,7 @@
40
40
  "eval:profile": "bun --env-file=../../.env.test ./eval/profile/profile.eval.ts",
41
41
  "eval:opportunity": "bun --env-file=../../.env.test ./eval/opportunity/opportunity.eval.ts",
42
42
  "eval:clarification": "bun --env-file=../../.env.test ./eval/clarification/clarification.eval.ts",
43
+ "eval:intake": "bun --env-file=../../.env.test ./eval/intake/intake.eval.ts",
43
44
  "eval:discovery-retrieval": "bun --env-file=../../.env.test ./eval/discovery-retrieval/discovery-retrieval.eval.ts",
44
45
  "eval:canary": "bun --env-file=../../.env.test ./eval/canary/canary.eval.ts",
45
46
  "eval:stance": "bun --env-file=../../.env.test ./eval/stance/stance.eval.ts",