@indexnetwork/protocol 4.12.0-rc.352.1 → 4.14.0-rc.354.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 +3 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/opportunity/discriminator/discriminator.adjustments.d.ts +25 -0
- package/dist/opportunity/discriminator/discriminator.adjustments.d.ts.map +1 -1
- package/dist/opportunity/discriminator/discriminator.adjustments.js +32 -7
- package/dist/opportunity/discriminator/discriminator.adjustments.js.map +1 -1
- package/dist/opportunity/discriminator/discriminator.assigner.d.ts +79 -0
- package/dist/opportunity/discriminator/discriminator.assigner.d.ts.map +1 -0
- package/dist/opportunity/discriminator/discriminator.assigner.js +154 -0
- package/dist/opportunity/discriminator/discriminator.assigner.js.map +1 -0
- package/dist/opportunity/discriminator/discriminator.env.d.ts +7 -0
- package/dist/opportunity/discriminator/discriminator.env.d.ts.map +1 -1
- package/dist/opportunity/discriminator/discriminator.env.js +7 -0
- package/dist/opportunity/discriminator/discriminator.env.js.map +1 -1
- package/dist/opportunity/discriminator/discriminator.miner.d.ts +14 -0
- package/dist/opportunity/discriminator/discriminator.miner.d.ts.map +1 -1
- package/dist/opportunity/discriminator/discriminator.miner.js +14 -5
- package/dist/opportunity/discriminator/discriminator.miner.js.map +1 -1
- package/dist/opportunity/opportunity.graph.d.ts +17 -2
- package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
- package/dist/opportunity/opportunity.graph.js +85 -2
- package/dist/opportunity/opportunity.graph.js.map +1 -1
- package/dist/opportunity/opportunity.pending-questions.d.ts.map +1 -1
- package/dist/opportunity/opportunity.pending-questions.js +2 -1
- package/dist/opportunity/opportunity.pending-questions.js.map +1 -1
- package/dist/opportunity/opportunity.tools.d.ts.map +1 -1
- package/dist/opportunity/opportunity.tools.js +85 -3
- package/dist/opportunity/opportunity.tools.js.map +1 -1
- package/dist/questioner/questioner.env.d.ts +9 -0
- package/dist/questioner/questioner.env.d.ts.map +1 -1
- package/dist/questioner/questioner.env.js +19 -0
- package/dist/questioner/questioner.env.js.map +1 -1
- package/dist/questioner/questioner.presets.d.ts.map +1 -1
- package/dist/questioner/questioner.presets.js +44 -4
- package/dist/questioner/questioner.presets.js.map +1 -1
- package/dist/questioner/questioner.tools.d.ts.map +1 -1
- package/dist/questioner/questioner.tools.js +3 -3
- package/dist/questioner/questioner.tools.js.map +1 -1
- package/dist/questioner/questioner.types.d.ts +34 -7
- package/dist/questioner/questioner.types.d.ts.map +1 -1
- package/dist/questioner/questioner.types.js.map +1 -1
- package/dist/shared/agent/model.config.d.ts +5 -0
- package/dist/shared/agent/model.config.d.ts.map +1 -1
- package/dist/shared/agent/model.config.js +1 -0
- package/dist/shared/agent/model.config.js.map +1 -1
- package/dist/shared/agent/tool.factory.d.ts.map +1 -1
- package/dist/shared/agent/tool.factory.js +2 -1
- package/dist/shared/agent/tool.factory.js.map +1 -1
- package/dist/shared/agent/tool.helpers.d.ts +9 -1
- package/dist/shared/agent/tool.helpers.d.ts.map +1 -1
- package/dist/shared/agent/tool.helpers.js.map +1 -1
- package/dist/shared/interfaces/database.interface.d.ts +2 -0
- package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/database.interface.js.map +1 -1
- package/dist/shared/interfaces/questioner.interface.d.ts +2 -1
- package/dist/shared/interfaces/questioner.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/questioner.interface.js.map +1 -1
- package/dist/shared/schemas/pending-question.schema.d.ts +3 -1
- package/dist/shared/schemas/pending-question.schema.d.ts.map +1 -1
- package/dist/shared/schemas/pending-question.schema.js.map +1 -1
- package/dist/shared/schemas/question.schema.d.ts +7 -0
- package/dist/shared/schemas/question.schema.d.ts.map +1 -1
- package/dist/shared/schemas/question.schema.js +4 -0
- package/dist/shared/schemas/question.schema.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Evidence-verified assignment of call-local newborn candidates to already
|
|
12
|
+
* answered pool-discriminator axes (IND-420 P4b).
|
|
13
|
+
*
|
|
14
|
+
* One structured model call batches every fixed axis and candidate. The model
|
|
15
|
+
* never receives the owner's chosen side; it only classifies public candidate
|
|
16
|
+
* context. Code owns all identifiers, allowed sides, and evidence verification.
|
|
17
|
+
*/
|
|
18
|
+
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { createStructuredModel } from "../../shared/agent/model.config.js";
|
|
21
|
+
import { invokeWithAbortSignal } from "../../shared/agent/model-signal.js";
|
|
22
|
+
import { protocolLogger } from "../../shared/observability/protocol.logger.js";
|
|
23
|
+
import { Timed } from "../../shared/observability/performance.js";
|
|
24
|
+
import { POOL_DISCRIMINATOR_MAX_CANDIDATES } from "./discriminator.env.js";
|
|
25
|
+
import { poolEvidenceMatches } from "./discriminator.miner.js";
|
|
26
|
+
/** QuestionerAdapter caps fresh answered preferences at this count. */
|
|
27
|
+
const MAX_ASSIGNMENT_AXES = 10;
|
|
28
|
+
const logger = protocolLogger("PoolDiscriminatorAssigner");
|
|
29
|
+
const AssignmentSchema = z.object({
|
|
30
|
+
candidateId: z.string(),
|
|
31
|
+
side: z.string().nullable(),
|
|
32
|
+
evidence: z.string().max(200).nullable(),
|
|
33
|
+
});
|
|
34
|
+
const AxisSchema = z.object({
|
|
35
|
+
questionId: z.string(),
|
|
36
|
+
assignments: z.array(AssignmentSchema).max(POOL_DISCRIMINATOR_MAX_CANDIDATES),
|
|
37
|
+
});
|
|
38
|
+
// Keep fields optional at the outer response layer so one malformed/missing
|
|
39
|
+
// axis can be skipped independently, while still bounding and describing the
|
|
40
|
+
// nested assignment payload sent through structured output.
|
|
41
|
+
const ResponseAxisSchema = z.object({
|
|
42
|
+
questionId: z.string().optional(),
|
|
43
|
+
assignments: z.array(AssignmentSchema).max(POOL_DISCRIMINATOR_MAX_CANDIDATES).optional(),
|
|
44
|
+
}).passthrough();
|
|
45
|
+
const ResponseSchema = z.object({ axes: z.array(ResponseAxisSchema).max(MAX_ASSIGNMENT_AXES) });
|
|
46
|
+
const SYSTEM_PROMPT = `Classify candidate matches against FIXED preference axes.
|
|
47
|
+
|
|
48
|
+
Rules:
|
|
49
|
+
- Use only the supplied questionId, candidateId, and side labels. Never invent or rename them.
|
|
50
|
+
- Return one axis object per supplied question and one assignment per candidate.
|
|
51
|
+
- Pick a side only when the candidate context clearly supports it; otherwise use side null and evidence null.
|
|
52
|
+
- Every non-null side requires a short VERBATIM substring copied from that candidate's context. It is checked mechanically.
|
|
53
|
+
- Do not infer the user's preference. You are classifying candidates, not choosing which side is better.
|
|
54
|
+
- Return only questionId, candidateId, side, and evidence fields.`;
|
|
55
|
+
/** Build the classifier prompt without any chosen-side preference. */
|
|
56
|
+
export function buildAssignmentPrompt(input) {
|
|
57
|
+
const axes = input.axes.map((axis) => ({
|
|
58
|
+
questionId: axis.questionId,
|
|
59
|
+
label: axis.label,
|
|
60
|
+
sides: axis.sides,
|
|
61
|
+
}));
|
|
62
|
+
const candidates = input.candidates.map((candidate) => ({
|
|
63
|
+
candidateId: candidate.id,
|
|
64
|
+
publicContext: candidate.publicContext,
|
|
65
|
+
}));
|
|
66
|
+
return JSON.stringify({ axes, candidates }, null, 2);
|
|
67
|
+
}
|
|
68
|
+
/** Verify one model-returned axis against fixed IDs, sides, and contexts. */
|
|
69
|
+
export function verifyAssignedAxis(raw, input) {
|
|
70
|
+
const fixedAxis = input.axes.find((axis) => axis.questionId === raw.questionId);
|
|
71
|
+
if (!fixedAxis)
|
|
72
|
+
return null;
|
|
73
|
+
const contextById = new Map(input.candidates.map((candidate) => [candidate.id, candidate.publicContext]));
|
|
74
|
+
const firstById = new Map();
|
|
75
|
+
for (const assignment of raw.assignments) {
|
|
76
|
+
const context = contextById.get(assignment.candidateId);
|
|
77
|
+
if (context === undefined || firstById.has(assignment.candidateId))
|
|
78
|
+
continue;
|
|
79
|
+
if (assignment.side === null) {
|
|
80
|
+
firstById.set(assignment.candidateId, {
|
|
81
|
+
candidateId: assignment.candidateId,
|
|
82
|
+
side: null,
|
|
83
|
+
evidence: null,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!fixedAxis.sides.includes(assignment.side) || !poolEvidenceMatches(context, assignment.evidence)) {
|
|
88
|
+
firstById.set(assignment.candidateId, {
|
|
89
|
+
candidateId: assignment.candidateId,
|
|
90
|
+
side: null,
|
|
91
|
+
evidence: null,
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
firstById.set(assignment.candidateId, {
|
|
96
|
+
candidateId: assignment.candidateId,
|
|
97
|
+
side: assignment.side,
|
|
98
|
+
evidence: assignment.evidence,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
questionId: fixedAxis.questionId,
|
|
103
|
+
assignments: input.candidates.map((candidate) => firstById.get(candidate.id) ?? {
|
|
104
|
+
candidateId: candidate.id,
|
|
105
|
+
side: null,
|
|
106
|
+
evidence: null,
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Stateless one-call newborn candidate classifier. */
|
|
111
|
+
export class PoolDiscriminatorAssigner {
|
|
112
|
+
constructor(config) {
|
|
113
|
+
this.model = createStructuredModel("poolDiscriminatorAssigner", ResponseSchema, { name: "pool_newborn_assignments" }, config?.modelConfig);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Classify every candidate against every fixed axis in one provider call.
|
|
117
|
+
* Provider failures propagate so the host graph's fail-open seam can retain
|
|
118
|
+
* the original un-stamped create items.
|
|
119
|
+
*/
|
|
120
|
+
async assign(input, options) {
|
|
121
|
+
if (input.axes.length === 0 || input.candidates.length === 0)
|
|
122
|
+
return [];
|
|
123
|
+
const boundedInput = {
|
|
124
|
+
axes: input.axes.slice(0, MAX_ASSIGNMENT_AXES),
|
|
125
|
+
candidates: input.candidates.slice(0, POOL_DISCRIMINATOR_MAX_CANDIDATES),
|
|
126
|
+
};
|
|
127
|
+
const raw = await invokeWithAbortSignal(this.model, [new SystemMessage(SYSTEM_PROMPT), new HumanMessage(buildAssignmentPrompt(boundedInput))], options?.signal);
|
|
128
|
+
const parsed = ResponseSchema.safeParse(raw);
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
logger.warn("Assignment response failed schema validation", { issues: parsed.error.issues.length });
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const verified = [];
|
|
135
|
+
for (const rawAxis of parsed.data.axes) {
|
|
136
|
+
const axis = AxisSchema.safeParse(rawAxis);
|
|
137
|
+
if (!axis.success || seen.has(axis.data.questionId))
|
|
138
|
+
continue;
|
|
139
|
+
const result = verifyAssignedAxis(axis.data, boundedInput);
|
|
140
|
+
if (!result)
|
|
141
|
+
continue;
|
|
142
|
+
seen.add(result.questionId);
|
|
143
|
+
verified.push(result);
|
|
144
|
+
}
|
|
145
|
+
return verified;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
__decorate([
|
|
149
|
+
Timed(),
|
|
150
|
+
__metadata("design:type", Function),
|
|
151
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
152
|
+
__metadata("design:returntype", Promise)
|
|
153
|
+
], PoolDiscriminatorAssigner.prototype, "assign", null);
|
|
154
|
+
//# sourceMappingURL=discriminator.assigner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discriminator.assigner.js","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.assigner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Evidence-verified assignment of call-local newborn candidates to already\n * answered pool-discriminator axes (IND-420 P4b).\n *\n * One structured model call batches every fixed axis and candidate. The model\n * never receives the owner's chosen side; it only classifies public candidate\n * context. Code owns all identifiers, allowed sides, and evidence verification.\n */\nimport { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\n\nimport { createStructuredModel } from \"../../shared/agent/model.config.js\";\nimport { invokeWithAbortSignal } from \"../../shared/agent/model-signal.js\";\nimport { protocolLogger } from \"../../shared/observability/protocol.logger.js\";\nimport { Timed } from \"../../shared/observability/performance.js\";\nimport { POOL_DISCRIMINATOR_MAX_CANDIDATES } from \"./discriminator.env.js\";\nimport { poolEvidenceMatches } from \"./discriminator.miner.js\";\nimport type { PoolCandidate } from \"./discriminator.types.js\";\n\n/** QuestionerAdapter caps fresh answered preferences at this count. */\nconst MAX_ASSIGNMENT_AXES = 10;\nconst logger = protocolLogger(\"PoolDiscriminatorAssigner\");\n\nconst AssignmentSchema = z.object({\n candidateId: z.string(),\n side: z.string().nullable(),\n evidence: z.string().max(200).nullable(),\n});\n\nconst AxisSchema = z.object({\n questionId: z.string(),\n assignments: z.array(AssignmentSchema).max(POOL_DISCRIMINATOR_MAX_CANDIDATES),\n});\n\n// Keep fields optional at the outer response layer so one malformed/missing\n// axis can be skipped independently, while still bounding and describing the\n// nested assignment payload sent through structured output.\nconst ResponseAxisSchema = z.object({\n questionId: z.string().optional(),\n assignments: z.array(AssignmentSchema).max(POOL_DISCRIMINATOR_MAX_CANDIDATES).optional(),\n}).passthrough();\nconst ResponseSchema = z.object({ axes: z.array(ResponseAxisSchema).max(MAX_ASSIGNMENT_AXES) });\ntype AssignmentResponse = z.infer<typeof ResponseSchema>;\ntype RawAxis = z.infer<typeof AxisSchema>;\n\nconst SYSTEM_PROMPT = `Classify candidate matches against FIXED preference axes.\n\nRules:\n- Use only the supplied questionId, candidateId, and side labels. Never invent or rename them.\n- Return one axis object per supplied question and one assignment per candidate.\n- Pick a side only when the candidate context clearly supports it; otherwise use side null and evidence null.\n- Every non-null side requires a short VERBATIM substring copied from that candidate's context. It is checked mechanically.\n- Do not infer the user's preference. You are classifying candidates, not choosing which side is better.\n- Return only questionId, candidateId, side, and evidence fields.`;\n\n/** Fixed answered axis; deliberately excludes the owner's chosen side. */\nexport interface PoolDiscriminatorAssignmentAxis {\n questionId: string;\n label: string;\n sides: string[];\n}\n\n/** One batched assignment call. */\nexport interface PoolDiscriminatorAssignmentInput {\n axes: PoolDiscriminatorAssignmentAxis[];\n candidates: PoolCandidate[];\n}\n\n/** Verified assignment. Invalid or explicit unknown classifications use null. */\nexport interface PoolDiscriminatorCandidateAssignment {\n candidateId: string;\n side: string | null;\n evidence: string | null;\n}\n\n/** One valid returned axis. Missing or malformed axes are omitted entirely. */\nexport interface PoolDiscriminatorAssignedAxis {\n questionId: string;\n assignments: PoolDiscriminatorCandidateAssignment[];\n}\n\n/** Construction options for {@link PoolDiscriminatorAssigner}. */\nexport interface PoolDiscriminatorAssignerConfig {\n modelConfig?: Parameters<typeof createStructuredModel>[3];\n}\n\n/** Build the classifier prompt without any chosen-side preference. */\nexport function buildAssignmentPrompt(input: PoolDiscriminatorAssignmentInput): string {\n const axes = input.axes.map((axis) => ({\n questionId: axis.questionId,\n label: axis.label,\n sides: axis.sides,\n }));\n const candidates = input.candidates.map((candidate) => ({\n candidateId: candidate.id,\n publicContext: candidate.publicContext,\n }));\n return JSON.stringify({ axes, candidates }, null, 2);\n}\n\n/** Verify one model-returned axis against fixed IDs, sides, and contexts. */\nexport function verifyAssignedAxis(\n raw: RawAxis,\n input: PoolDiscriminatorAssignmentInput,\n): PoolDiscriminatorAssignedAxis | null {\n const fixedAxis = input.axes.find((axis) => axis.questionId === raw.questionId);\n if (!fixedAxis) return null;\n\n const contextById = new Map(input.candidates.map((candidate) => [candidate.id, candidate.publicContext]));\n const firstById = new Map<string, PoolDiscriminatorCandidateAssignment>();\n for (const assignment of raw.assignments) {\n const context = contextById.get(assignment.candidateId);\n if (context === undefined || firstById.has(assignment.candidateId)) continue;\n if (assignment.side === null) {\n firstById.set(assignment.candidateId, {\n candidateId: assignment.candidateId,\n side: null,\n evidence: null,\n });\n continue;\n }\n if (!fixedAxis.sides.includes(assignment.side) || !poolEvidenceMatches(context, assignment.evidence)) {\n firstById.set(assignment.candidateId, {\n candidateId: assignment.candidateId,\n side: null,\n evidence: null,\n });\n continue;\n }\n firstById.set(assignment.candidateId, {\n candidateId: assignment.candidateId,\n side: assignment.side,\n evidence: assignment.evidence,\n });\n }\n\n return {\n questionId: fixedAxis.questionId,\n assignments: input.candidates.map((candidate) => firstById.get(candidate.id) ?? {\n candidateId: candidate.id,\n side: null,\n evidence: null,\n }),\n };\n}\n\n/** Stateless one-call newborn candidate classifier. */\nexport class PoolDiscriminatorAssigner {\n private model: ReturnType<typeof createStructuredModel<AssignmentResponse>>;\n\n constructor(config?: PoolDiscriminatorAssignerConfig) {\n this.model = createStructuredModel<AssignmentResponse>(\n \"poolDiscriminatorAssigner\",\n ResponseSchema,\n { name: \"pool_newborn_assignments\" },\n config?.modelConfig,\n );\n }\n\n /**\n * Classify every candidate against every fixed axis in one provider call.\n * Provider failures propagate so the host graph's fail-open seam can retain\n * the original un-stamped create items.\n */\n @Timed()\n async assign(\n input: PoolDiscriminatorAssignmentInput,\n options?: { signal?: AbortSignal },\n ): Promise<PoolDiscriminatorAssignedAxis[]> {\n if (input.axes.length === 0 || input.candidates.length === 0) return [];\n const boundedInput: PoolDiscriminatorAssignmentInput = {\n axes: input.axes.slice(0, MAX_ASSIGNMENT_AXES),\n candidates: input.candidates.slice(0, POOL_DISCRIMINATOR_MAX_CANDIDATES),\n };\n const raw = await invokeWithAbortSignal(\n this.model,\n [new SystemMessage(SYSTEM_PROMPT), new HumanMessage(buildAssignmentPrompt(boundedInput))],\n options?.signal,\n );\n const parsed = ResponseSchema.safeParse(raw);\n if (!parsed.success) {\n logger.warn(\"Assignment response failed schema validation\", { issues: parsed.error.issues.length });\n return [];\n }\n\n const seen = new Set<string>();\n const verified: PoolDiscriminatorAssignedAxis[] = [];\n for (const rawAxis of parsed.data.axes) {\n const axis = AxisSchema.safeParse(rawAxis);\n if (!axis.success || seen.has(axis.data.questionId)) continue;\n const result = verifyAssignedAxis(axis.data, boundedInput);\n if (!result) continue;\n seen.add(result.questionId);\n verified.push(result);\n }\n return verified;\n }\n}\n"]}
|
|
@@ -25,6 +25,13 @@ export type PoolQuestionsMode = "off" | "on";
|
|
|
25
25
|
* "on" implies mining runs even when POOL_QUESTIONS_MINING is off.
|
|
26
26
|
*/
|
|
27
27
|
export declare function poolQuestionsMode(): PoolQuestionsMode;
|
|
28
|
+
/** Newborn-opportunity stamping mode (IND-420 P4b). */
|
|
29
|
+
export type PoolQuestionsStampNewbornMode = "off" | "on";
|
|
30
|
+
/**
|
|
31
|
+
* Current POOL_QUESTIONS_STAMP_NEWBORN mode (default off). Effective callers
|
|
32
|
+
* additionally require {@link poolQuestionsMode} to be on.
|
|
33
|
+
*/
|
|
34
|
+
export declare function poolQuestionsStampNewborn(): PoolQuestionsStampNewbornMode;
|
|
28
35
|
/** Minimum VoI for a discriminator to become (or chain) a question. */
|
|
29
36
|
export declare const POOL_QUESTION_MIN_VOI = 0.2;
|
|
30
37
|
/** Minimum evidence-verification rate for a discriminator to become a question. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discriminator.env.d.ts","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,+CAA+C;AAC/C,MAAM,MAAM,uBAAuB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEvD,wDAAwD;AACxD,wBAAgB,uBAAuB,IAAI,uBAAuB,CAEjE;AAED,uDAAuD;AACvD,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,IAAI,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,iBAAiB,CAErD;AAED,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,mFAAmF;AACnF,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,uEAAuE;AACvE,eAAO,MAAM,oCAAoC,IAAI,CAAC;AAEtD,mDAAmD;AACnD,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,IAAI,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,wBAAwB,CAE/D;AAED,4EAA4E;AAC5E,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD,6EAA6E;AAC7E,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,6DAA6D;AAC7D,eAAO,MAAM,iCAAiC,KAAK,CAAC;AAEpD,qEAAqE;AACrE,eAAO,MAAM,2CAA2C,MAAM,CAAC"}
|
|
1
|
+
{"version":3,"file":"discriminator.env.d.ts","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,+CAA+C;AAC/C,MAAM,MAAM,uBAAuB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEvD,wDAAwD;AACxD,wBAAgB,uBAAuB,IAAI,uBAAuB,CAEjE;AAED,uDAAuD;AACvD,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,IAAI,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,iBAAiB,CAErD;AAED,uDAAuD;AACvD,MAAM,MAAM,6BAA6B,GAAG,KAAK,GAAG,IAAI,CAAC;AAEzD;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,6BAA6B,CAEzE;AAED,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,mFAAmF;AACnF,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,uEAAuE;AACvE,eAAO,MAAM,oCAAoC,IAAI,CAAC;AAEtD,mDAAmD;AACnD,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,IAAI,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,wBAAwB,CAE/D;AAED,4EAA4E;AAC5E,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD,6EAA6E;AAC7E,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,6DAA6D;AAC7D,eAAO,MAAM,iCAAiC,KAAK,CAAC;AAEpD,qEAAqE;AACrE,eAAO,MAAM,2CAA2C,MAAM,CAAC"}
|
|
@@ -25,6 +25,13 @@ export function poolQuestionsMiningMode() {
|
|
|
25
25
|
export function poolQuestionsMode() {
|
|
26
26
|
return process.env.POOL_QUESTIONS_MODE?.trim() === "on" ? "on" : "off";
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Current POOL_QUESTIONS_STAMP_NEWBORN mode (default off). Effective callers
|
|
30
|
+
* additionally require {@link poolQuestionsMode} to be on.
|
|
31
|
+
*/
|
|
32
|
+
export function poolQuestionsStampNewborn() {
|
|
33
|
+
return process.env.POOL_QUESTIONS_STAMP_NEWBORN?.trim() === "on" ? "on" : "off";
|
|
34
|
+
}
|
|
28
35
|
/** Minimum VoI for a discriminator to become (or chain) a question. */
|
|
29
36
|
export const POOL_QUESTION_MIN_VOI = 0.2;
|
|
30
37
|
/** Minimum evidence-verification rate for a discriminator to become a question. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discriminator.env.js","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,wDAAwD;AACxD,MAAM,UAAU,uBAAuB;IACrC,OAAO,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;AACnF,CAAC;AAKD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AACzE,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC,mFAAmF;AACnF,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,CAAC;AAEnD,gFAAgF;AAChF,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAAC,CAAC;AAElD,uEAAuE;AACvE,MAAM,CAAC,MAAM,oCAAoC,GAAG,CAAC,CAAC;AAKtD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AAC5E,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,CAAC;AAEhD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,CAAC;AAElD,8EAA8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAE5C,8EAA8E;AAC9E,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAM,CAAC;AAE7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAAC,CAAC;AAElD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,CAAC;AAEpD,qEAAqE;AACrE,MAAM,CAAC,MAAM,2CAA2C,GAAG,GAAG,CAAC","sourcesContent":["/**\n * Centralized accessors for pool-question environment variables (IND-416).\n *\n * POOL_QUESTIONS_MINING off | shadow — P1 shadow axis mining on\n * discovery-run completion. `shadow` mines and\n * scores axes and logs them; no questions are\n * generated and no user-facing behavior changes.\n * Any value other than \"shadow\" (including unset)\n * means off.\n *\n * All reads go through this module — do not read these variables via\n * `process.env` elsewhere. Values are read on every call (no caching) so\n * tests and long-lived processes observe changes.\n */\n\n/** Mining mode for pool discriminator axes. */\nexport type PoolQuestionsMiningMode = \"off\" | \"shadow\";\n\n/** Current POOL_QUESTIONS_MINING mode (default off). */\nexport function poolQuestionsMiningMode(): PoolQuestionsMiningMode {\n return process.env.POOL_QUESTIONS_MINING?.trim() === \"shadow\" ? \"shadow\" : \"off\";\n}\n\n/** Question mode for pool discriminators (IND-418). */\nexport type PoolQuestionsMode = \"off\" | \"on\";\n\n/**\n * Current POOL_QUESTIONS_MODE (default off). When \"on\", the mining hook also\n * enqueues a pool_discovery question for the top eligible discriminator\n * (still subject to the QUESTIONER_ENABLED master gate and per-intent budget).\n * \"on\" implies mining runs even when POOL_QUESTIONS_MINING is off.\n */\nexport function poolQuestionsMode(): PoolQuestionsMode {\n return process.env.POOL_QUESTIONS_MODE?.trim() === \"on\" ? \"on\" : \"off\";\n}\n\n/** Minimum VoI for a discriminator to become (or chain) a question. */\nexport const POOL_QUESTION_MIN_VOI = 0.2;\n\n/** Minimum evidence-verification rate for a discriminator to become a question. */\nexport const POOL_QUESTION_MIN_EVIDENCE_RATE = 0.6;\n\n/** Max eligible discriminators carried per mining pass (asked + alternates). */\nexport const POOL_QUESTION_MAX_DISCRIMINATORS = 3;\n\n/** Unattended budget: max pending questions of ANY mode per intent. */\nexport const POOL_QUESTION_MAX_PENDING_PER_INTENT = 3;\n\n/** Ranking mode for pool adjustments (IND-419). */\nexport type PoolQuestionsRankingMode = \"off\" | \"on\";\n\n/**\n * Current POOL_QUESTIONS_RANKING (default off). When \"on\", the home feed\n * orders by adjusted confidence (confidence × Π poolAdjustments.factor,\n * floor {@link POOL_ADJUSTMENT_FLOOR}). Adjustments are WRITTEN regardless of\n * this flag — it only gates whether ordering reads them, so the apply path\n * can ship first.\n */\nexport function poolQuestionsRanking(): PoolQuestionsRankingMode {\n return process.env.POOL_QUESTIONS_RANKING?.trim() === \"on\" ? \"on\" : \"off\";\n}\n\n/** Adjustment factor for candidates on the side the user did NOT choose. */\nexport const POOL_ADJUSTMENT_FACTOR_OTHER = 0.6;\n\n/** Adjustment factor for candidates the miner could not assign (unknown). */\nexport const POOL_ADJUSTMENT_FACTOR_UNKNOWN = 0.9;\n\n/** Floor for the cumulative adjustment multiplier — demoted, never hidden. */\nexport const POOL_ADJUSTMENT_FLOOR = 0.3;\n\n/**\n * Staleness guard (IND-419): when more than this fraction of a question's\n * stored assignments point at opportunities that left the pool (expired /\n * rejected / accepted / archived), skip the re-rank — the snapshot no longer\n * describes the live pool.\n */\nexport const POOL_STALENESS_THRESHOLD = 0.3;\n\n/** Debounce window for answer-triggered re-discovery (Tier 1), per intent. */\nexport const POOL_RERUN_DEBOUNCE_MS = 60_000;\n\n/**\n * k-anonymity floor: axes are only mined when the pool has at least this many\n * candidates, so no axis (or later, question option) can be traced back to a\n * specific individual.\n */\nexport const POOL_DISCRIMINATOR_MIN_POOL_SIZE = 5;\n\n/** Max candidates sent to the miner LLM (top-N by score). */\nexport const POOL_DISCRIMINATOR_MAX_CANDIDATES = 24;\n\n/** Max chars of public context per candidate in the miner prompt. */\nexport const POOL_DISCRIMINATOR_MAX_PUBLIC_CONTEXT_CHARS = 400;\n"]}
|
|
1
|
+
{"version":3,"file":"discriminator.env.js","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,wDAAwD;AACxD,MAAM,UAAU,uBAAuB;IACrC,OAAO,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;AACnF,CAAC;AAKD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AACzE,CAAC;AAKD;;;GAGG;AACH,MAAM,UAAU,yBAAyB;IACvC,OAAO,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AAClF,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC,mFAAmF;AACnF,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,CAAC;AAEnD,gFAAgF;AAChF,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAAC,CAAC;AAElD,uEAAuE;AACvE,MAAM,CAAC,MAAM,oCAAoC,GAAG,CAAC,CAAC;AAKtD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AAC5E,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,CAAC;AAEhD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,CAAC;AAElD,8EAA8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAE5C,8EAA8E;AAC9E,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAM,CAAC;AAE7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAAC,CAAC;AAElD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,CAAC;AAEpD,qEAAqE;AACrE,MAAM,CAAC,MAAM,2CAA2C,GAAG,GAAG,CAAC","sourcesContent":["/**\n * Centralized accessors for pool-question environment variables (IND-416).\n *\n * POOL_QUESTIONS_MINING off | shadow — P1 shadow axis mining on\n * discovery-run completion. `shadow` mines and\n * scores axes and logs them; no questions are\n * generated and no user-facing behavior changes.\n * Any value other than \"shadow\" (including unset)\n * means off.\n *\n * All reads go through this module — do not read these variables via\n * `process.env` elsewhere. Values are read on every call (no caching) so\n * tests and long-lived processes observe changes.\n */\n\n/** Mining mode for pool discriminator axes. */\nexport type PoolQuestionsMiningMode = \"off\" | \"shadow\";\n\n/** Current POOL_QUESTIONS_MINING mode (default off). */\nexport function poolQuestionsMiningMode(): PoolQuestionsMiningMode {\n return process.env.POOL_QUESTIONS_MINING?.trim() === \"shadow\" ? \"shadow\" : \"off\";\n}\n\n/** Question mode for pool discriminators (IND-418). */\nexport type PoolQuestionsMode = \"off\" | \"on\";\n\n/**\n * Current POOL_QUESTIONS_MODE (default off). When \"on\", the mining hook also\n * enqueues a pool_discovery question for the top eligible discriminator\n * (still subject to the QUESTIONER_ENABLED master gate and per-intent budget).\n * \"on\" implies mining runs even when POOL_QUESTIONS_MINING is off.\n */\nexport function poolQuestionsMode(): PoolQuestionsMode {\n return process.env.POOL_QUESTIONS_MODE?.trim() === \"on\" ? \"on\" : \"off\";\n}\n\n/** Newborn-opportunity stamping mode (IND-420 P4b). */\nexport type PoolQuestionsStampNewbornMode = \"off\" | \"on\";\n\n/**\n * Current POOL_QUESTIONS_STAMP_NEWBORN mode (default off). Effective callers\n * additionally require {@link poolQuestionsMode} to be on.\n */\nexport function poolQuestionsStampNewborn(): PoolQuestionsStampNewbornMode {\n return process.env.POOL_QUESTIONS_STAMP_NEWBORN?.trim() === \"on\" ? \"on\" : \"off\";\n}\n\n/** Minimum VoI for a discriminator to become (or chain) a question. */\nexport const POOL_QUESTION_MIN_VOI = 0.2;\n\n/** Minimum evidence-verification rate for a discriminator to become a question. */\nexport const POOL_QUESTION_MIN_EVIDENCE_RATE = 0.6;\n\n/** Max eligible discriminators carried per mining pass (asked + alternates). */\nexport const POOL_QUESTION_MAX_DISCRIMINATORS = 3;\n\n/** Unattended budget: max pending questions of ANY mode per intent. */\nexport const POOL_QUESTION_MAX_PENDING_PER_INTENT = 3;\n\n/** Ranking mode for pool adjustments (IND-419). */\nexport type PoolQuestionsRankingMode = \"off\" | \"on\";\n\n/**\n * Current POOL_QUESTIONS_RANKING (default off). When \"on\", the home feed\n * orders by adjusted confidence (confidence × Π poolAdjustments.factor,\n * floor {@link POOL_ADJUSTMENT_FLOOR}). Adjustments are WRITTEN regardless of\n * this flag — it only gates whether ordering reads them, so the apply path\n * can ship first.\n */\nexport function poolQuestionsRanking(): PoolQuestionsRankingMode {\n return process.env.POOL_QUESTIONS_RANKING?.trim() === \"on\" ? \"on\" : \"off\";\n}\n\n/** Adjustment factor for candidates on the side the user did NOT choose. */\nexport const POOL_ADJUSTMENT_FACTOR_OTHER = 0.6;\n\n/** Adjustment factor for candidates the miner could not assign (unknown). */\nexport const POOL_ADJUSTMENT_FACTOR_UNKNOWN = 0.9;\n\n/** Floor for the cumulative adjustment multiplier — demoted, never hidden. */\nexport const POOL_ADJUSTMENT_FLOOR = 0.3;\n\n/**\n * Staleness guard (IND-419): when more than this fraction of a question's\n * stored assignments point at opportunities that left the pool (expired /\n * rejected / accepted / archived), skip the re-rank — the snapshot no longer\n * describes the live pool.\n */\nexport const POOL_STALENESS_THRESHOLD = 0.3;\n\n/** Debounce window for answer-triggered re-discovery (Tier 1), per intent. */\nexport const POOL_RERUN_DEBOUNCE_MS = 60_000;\n\n/**\n * k-anonymity floor: axes are only mined when the pool has at least this many\n * candidates, so no axis (or later, question option) can be traced back to a\n * specific individual.\n */\nexport const POOL_DISCRIMINATOR_MIN_POOL_SIZE = 5;\n\n/** Max candidates sent to the miner LLM (top-N by score). */\nexport const POOL_DISCRIMINATOR_MAX_CANDIDATES = 24;\n\n/** Max chars of public context per candidate in the miner prompt. */\nexport const POOL_DISCRIMINATOR_MAX_PUBLIC_CONTEXT_CHARS = 400;\n"]}
|
|
@@ -67,6 +67,20 @@ export interface PoolDiscriminatorMinerConfig {
|
|
|
67
67
|
/** Optional model config override (API key / base URL / model). */
|
|
68
68
|
modelConfig?: Parameters<typeof createStructuredModel>[3];
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Collapse whitespace, lowercase, and fold typographic punctuation to ASCII
|
|
72
|
+
* (curly quotes/apostrophes, en/em dashes, ellipsis), so evidence matching
|
|
73
|
+
* tolerates formatting drift without weakening the verbatim requirement.
|
|
74
|
+
*/
|
|
75
|
+
export declare function normalizePoolEvidenceForMatch(s: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Strips wrapping quotes and edge punctuation from an evidence span before
|
|
78
|
+
* matching. LLMs habitually sentence-ize copied spans (append a trailing
|
|
79
|
+
* period, wrap in quotes); the meaningful content must still match verbatim.
|
|
80
|
+
*/
|
|
81
|
+
export declare function stripPoolEvidenceEdgePunctuation(s: string): string;
|
|
82
|
+
/** Evidence verifier shared by mining and newborn assignment. */
|
|
83
|
+
export declare function poolEvidenceMatches(publicContext: string, evidence: string | null): boolean;
|
|
70
84
|
/**
|
|
71
85
|
* Stateless axis-mining agent. One `mine()` call = one structured LLM pass +
|
|
72
86
|
* deterministic evidence verification.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discriminator.miner.d.ts","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.miner.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAI3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAsB,MAAM,0BAA0B,CAAC;AAOjH,QAAA,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoC5B,CAAC;AAEH,KAAK,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAkBnE,sDAAsD;AACtD,MAAM,WAAW,4BAA4B;IAC3C,mEAAmE;IACnE,WAAW,CAAC,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC;CAC3D;
|
|
1
|
+
{"version":3,"file":"discriminator.miner.d.ts","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.miner.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAI3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAsB,MAAM,0BAA0B,CAAC;AAOjH,QAAA,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoC5B,CAAC;AAEH,KAAK,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAkBnE,sDAAsD;AACtD,MAAM,WAAW,4BAA4B;IAC3C,mEAAmE;IACnE,WAAW,CAAC,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC;CAC3D;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAS/D;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAElE;AAKD,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAK3F;AAED;;;GAGG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,KAAK,CAA+D;gBAEhE,MAAM,CAAC,EAAE,4BAA4B;IASjD;;;;;OAKG;IAEG,IAAI,CACR,KAAK,EAAE,wBAAwB,EAC/B,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,OAAO,CAAC,kBAAkB,EAAE,CAAC;CAcjC;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAQzE;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,EACxC,UAAU,EAAE,wBAAwB,CAAC,YAAY,CAAC,GACjD,kBAAkB,CAsCpB"}
|
|
@@ -82,7 +82,7 @@ Prefer axes that split the pool close to evenly and that cover many candidates w
|
|
|
82
82
|
* (curly quotes/apostrophes, en/em dashes, ellipsis), so evidence matching
|
|
83
83
|
* tolerates formatting drift without weakening the verbatim requirement.
|
|
84
84
|
*/
|
|
85
|
-
function
|
|
85
|
+
export function normalizePoolEvidenceForMatch(s) {
|
|
86
86
|
return s
|
|
87
87
|
.toLowerCase()
|
|
88
88
|
.replace(/[\u2018\u2019\u02bc]/g, "'")
|
|
@@ -97,9 +97,19 @@ function normalizeForMatch(s) {
|
|
|
97
97
|
* matching. LLMs habitually sentence-ize copied spans (append a trailing
|
|
98
98
|
* period, wrap in quotes); the meaningful content must still match verbatim.
|
|
99
99
|
*/
|
|
100
|
-
function
|
|
100
|
+
export function stripPoolEvidenceEdgePunctuation(s) {
|
|
101
101
|
return s.replace(/^[\s"'.,;:!?()\u2018\u2019\u201c\u201d]+/, "").replace(/[\s"'.,;:!?()\u2018\u2019\u201c\u201d]+$/, "");
|
|
102
102
|
}
|
|
103
|
+
/** Minimum normalized span that counts as meaningful verbatim evidence. */
|
|
104
|
+
const MIN_POOL_EVIDENCE_CHARS = 8;
|
|
105
|
+
/** Evidence verifier shared by mining and newborn assignment. */
|
|
106
|
+
export function poolEvidenceMatches(publicContext, evidence) {
|
|
107
|
+
if (evidence === null)
|
|
108
|
+
return false;
|
|
109
|
+
const context = normalizePoolEvidenceForMatch(publicContext);
|
|
110
|
+
const evidenceCore = normalizePoolEvidenceForMatch(stripPoolEvidenceEdgePunctuation(evidence));
|
|
111
|
+
return evidenceCore.length >= MIN_POOL_EVIDENCE_CHARS && context.includes(evidenceCore);
|
|
112
|
+
}
|
|
103
113
|
/**
|
|
104
114
|
* Stateless axis-mining agent. One `mine()` call = one structured LLM pass +
|
|
105
115
|
* deterministic evidence verification.
|
|
@@ -150,7 +160,7 @@ export function buildMiningPrompt(input) {
|
|
|
150
160
|
* - computes evidenceRate = verified / side-proposals.
|
|
151
161
|
*/
|
|
152
162
|
export function verifyAxis(axis, candidates) {
|
|
153
|
-
const contextById = new Map(candidates.map((c) => [c.id,
|
|
163
|
+
const contextById = new Map(candidates.map((c) => [c.id, c.publicContext]));
|
|
154
164
|
const byId = new Map();
|
|
155
165
|
let proposed = 0;
|
|
156
166
|
let verified = 0;
|
|
@@ -166,8 +176,7 @@ export function verifyAxis(axis, candidates) {
|
|
|
166
176
|
}
|
|
167
177
|
proposed++;
|
|
168
178
|
const sideOk = axis.sides.includes(a.side);
|
|
169
|
-
const
|
|
170
|
-
const evidenceOk = evidenceCore.length > 0 && context.includes(evidenceCore);
|
|
179
|
+
const evidenceOk = poolEvidenceMatches(context, a.evidence);
|
|
171
180
|
if (sideOk && evidenceOk) {
|
|
172
181
|
verified++;
|
|
173
182
|
byId.set(a.id, { id: a.id, side: a.side, evidence: a.evidence, verified: true });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discriminator.miner.js","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.miner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PoolDiscriminatorMiner — one structured LLM pass that proposes discriminating axes\n * over a discovery-run candidate pool, with code-side evidence verification\n * (IND-417).\n *\n * Anti-hallucination contract: every side assignment must quote a short\n * evidence span from that candidate's supplied publicContext. Verification is\n * done in code (substring match, case/whitespace-insensitive), NOT trusted\n * from the prompt. Failed verifications are demoted to `unknown`, which\n * lowers coverage → lowers VoI → buries the axis. The failure mode\n * self-punishes.\n *\n * Follows the QuestionerAgent pattern: constructor binds the structured\n * model once; single public `mine()` per call.\n */\nimport { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\n\nimport { createStructuredModel } from \"../../shared/agent/model.config.js\";\nimport { invokeWithAbortSignal } from \"../../shared/agent/model-signal.js\";\nimport { protocolLogger } from \"../../shared/observability/protocol.logger.js\";\nimport { Timed } from \"../../shared/observability/performance.js\";\nimport type { MinedDiscriminator, DiscriminatorMiningInput, VerifiedAssignment } from \"./discriminator.types.js\";\n\nconst logger = protocolLogger(\"PoolDiscriminatorMiner\");\n\n/** Max axes requested from (and accepted out of) one mining pass. */\nconst MAX_AXES = 6;\n\nconst AxisMiningResponseSchema = z.object({\n axes: z\n .array(\n z.object({\n axis: z\n .string()\n .describe(\"Short discriminating-axis label, e.g. 'Hands-on builders vs strategic advisors'\"),\n questionSeed: z\n .string()\n .describe(\"A direct question the intent owner could answer to resolve this axis\"),\n sides: z\n .array(z.string())\n .min(2)\n .max(3)\n .describe(\"2-3 mutually exclusive side labels (2-5 words each)\"),\n assignments: z\n .array(\n z.object({\n id: z.string().describe(\"Candidate id exactly as given\"),\n side: z\n .string()\n .nullable()\n .describe(\"One of this axis's side labels, or null when the context is insufficient\"),\n evidence: z\n .string()\n .max(200)\n .nullable()\n .describe(\n \"VERBATIM span (≤80 chars) copied from this candidate's context that justifies the side. null when side is null\",\n ),\n }),\n )\n .describe(\"One entry per candidate\"),\n }),\n )\n .max(MAX_AXES),\n});\n\ntype AxisMiningResponse = z.infer<typeof AxisMiningResponseSchema>;\n\nconst SYSTEM_PROMPT = `You analyze a pool of candidate matches for one user intent and propose the preference axes that best SPLIT the pool into meaningfully different groups.\n\nRules for every axis:\n- Sides must be mutually exclusive readings of one dimension (a candidate belongs to at most one side).\n- The axis must NOT already be determined by the intent text itself — only propose axes the intent leaves genuinely open.\n- The axis must be phrased as a preference the intent OWNER could hold and answer (the questionSeed asks them directly).\n- Side labels are terse (2-5 words), concrete, and derived from patterns across MULTIPLE candidates — never from a single person and never naming anyone.\n- BANNED axis families: protected attributes (race, ethnicity, religion, gender, age, nationality, disability, politics, sexual orientation), and vanity/noise splits (profile completeness, bio length, has photo, name alphabet).\n\nRules for assignments:\n- Assign EVERY candidate: pick a side only when the candidate's context contains clear support; otherwise use side null.\n- When you pick a side you MUST quote a short (≤80 chars) VERBATIM substring of that candidate's context as evidence. Copy it exactly — it is checked mechanically, and paraphrased evidence is discarded. Do NOT add punctuation that is not in the source (no trailing period); cutting off mid-sentence is fine.\n- Never invent context. If unsure, use side null with evidence null.\n\nPrefer axes that split the pool close to evenly and that cover many candidates with verifiable evidence. Propose at most ${MAX_AXES} axes; fewer strong axes beat many weak ones.`;\n\n/** Config for PoolDiscriminatorMiner construction. */\nexport interface PoolDiscriminatorMinerConfig {\n /** Optional model config override (API key / base URL / model). */\n modelConfig?: Parameters<typeof createStructuredModel>[3];\n}\n\n/**\n * Collapse whitespace, lowercase, and fold typographic punctuation to ASCII\n * (curly quotes/apostrophes, en/em dashes, ellipsis), so evidence matching\n * tolerates formatting drift without weakening the verbatim requirement.\n */\nfunction normalizeForMatch(s: string): string {\n return s\n .toLowerCase()\n .replace(/[\\u2018\\u2019\\u02bc]/g, \"'\")\n .replace(/[\\u201c\\u201d]/g, '\"')\n .replace(/[\\u2013\\u2014]/g, \"-\")\n .replace(/\\u2026/g, \"...\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Strips wrapping quotes and edge punctuation from an evidence span before\n * matching. LLMs habitually sentence-ize copied spans (append a trailing\n * period, wrap in quotes); the meaningful content must still match verbatim.\n */\nfunction stripEdgePunctuation(s: string): string {\n return s.replace(/^[\\s\"'.,;:!?()\\u2018\\u2019\\u201c\\u201d]+/, \"\").replace(/[\\s\"'.,;:!?()\\u2018\\u2019\\u201c\\u201d]+$/, \"\");\n}\n\n/**\n * Stateless axis-mining agent. One `mine()` call = one structured LLM pass +\n * deterministic evidence verification.\n */\nexport class PoolDiscriminatorMiner {\n private model: ReturnType<typeof createStructuredModel<AxisMiningResponse>>;\n\n constructor(config?: PoolDiscriminatorMinerConfig) {\n this.model = createStructuredModel<AxisMiningResponse>(\n \"poolDiscriminatorMiner\",\n AxisMiningResponseSchema,\n { name: \"pool_axes\" },\n config?.modelConfig,\n );\n }\n\n /**\n * Mine discriminating axes for one pool.\n *\n * @returns Verified axes (may be empty). Throws on LLM failure — callers\n * run this fire-and-forget and must catch.\n */\n @Timed()\n async mine(\n input: DiscriminatorMiningInput,\n options?: { signal?: AbortSignal },\n ): Promise<MinedDiscriminator[]> {\n const prompt = buildMiningPrompt(input);\n const raw = await invokeWithAbortSignal(\n this.model,\n [new SystemMessage(SYSTEM_PROMPT), new HumanMessage(prompt)],\n options?.signal,\n );\n const parsed = AxisMiningResponseSchema.safeParse(raw);\n if (!parsed.success) {\n logger.warn(\"Mining response failed schema validation\", { issues: parsed.error.issues.length });\n return [];\n }\n return parsed.data.axes.slice(0, MAX_AXES).map((axis) => verifyAxis(axis, input.candidates));\n }\n}\n\n/** Builds the human message: intent text + one numbered block per candidate. */\nexport function buildMiningPrompt(input: DiscriminatorMiningInput): string {\n const candidateBlocks = input.candidates\n .map((c) => `[${c.id}]\\n${c.publicContext}`)\n .join(\"\\n\\n\");\n return [\n `Intent:\\n${input.intentText}`,\n `Candidate pool (${input.candidates.length} candidates, id in brackets):\\n\\n${candidateBlocks}`,\n ].join(\"\\n\\n\");\n}\n\n/**\n * Deterministic post-pass over one LLM axis:\n * - drops assignments for ids not in the pool,\n * - demotes to unknown: sides not in the axis's side list, missing evidence,\n * and evidence that does not substring-match the candidate's publicContext,\n * - guarantees exactly one assignment per pool candidate (missing → unknown),\n * - computes evidenceRate = verified / side-proposals.\n */\nexport function verifyAxis(\n axis: AxisMiningResponse[\"axes\"][number],\n candidates: DiscriminatorMiningInput[\"candidates\"],\n): MinedDiscriminator {\n const contextById = new Map(candidates.map((c) => [c.id, normalizeForMatch(c.publicContext)]));\n const byId = new Map<string, VerifiedAssignment>();\n let proposed = 0;\n let verified = 0;\n\n for (const a of axis.assignments) {\n const context = contextById.get(a.id);\n if (context === undefined) continue; // hallucinated candidate id\n if (byId.has(a.id)) continue; // first proposal wins\n if (a.side === null) {\n byId.set(a.id, { id: a.id, side: null, evidence: null, verified: false });\n continue;\n }\n proposed++;\n const sideOk = axis.sides.includes(a.side);\n const evidenceCore = a.evidence === null ? \"\" : normalizeForMatch(stripEdgePunctuation(a.evidence));\n const evidenceOk = evidenceCore.length > 0 && context.includes(evidenceCore);\n if (sideOk && evidenceOk) {\n verified++;\n byId.set(a.id, { id: a.id, side: a.side, evidence: a.evidence, verified: true });\n } else {\n // Demoted: keep the (failed) evidence for audit logging, but unknown side.\n byId.set(a.id, { id: a.id, side: null, evidence: a.evidence, verified: false });\n }\n }\n\n // Every pool candidate gets exactly one assignment.\n const assignments: VerifiedAssignment[] = candidates.map(\n (c) => byId.get(c.id) ?? { id: c.id, side: null, evidence: null, verified: false },\n );\n\n return {\n label: axis.axis,\n questionSeed: axis.questionSeed,\n sides: axis.sides,\n assignments,\n evidenceRate: proposed > 0 ? verified / proposed : 0,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"discriminator.miner.js","sourceRoot":"/","sources":["opportunity/discriminator/discriminator.miner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PoolDiscriminatorMiner — one structured LLM pass that proposes discriminating axes\n * over a discovery-run candidate pool, with code-side evidence verification\n * (IND-417).\n *\n * Anti-hallucination contract: every side assignment must quote a short\n * evidence span from that candidate's supplied publicContext. Verification is\n * done in code (substring match, case/whitespace-insensitive), NOT trusted\n * from the prompt. Failed verifications are demoted to `unknown`, which\n * lowers coverage → lowers VoI → buries the axis. The failure mode\n * self-punishes.\n *\n * Follows the QuestionerAgent pattern: constructor binds the structured\n * model once; single public `mine()` per call.\n */\nimport { HumanMessage, SystemMessage } from \"@langchain/core/messages\";\nimport { z } from \"zod\";\n\nimport { createStructuredModel } from \"../../shared/agent/model.config.js\";\nimport { invokeWithAbortSignal } from \"../../shared/agent/model-signal.js\";\nimport { protocolLogger } from \"../../shared/observability/protocol.logger.js\";\nimport { Timed } from \"../../shared/observability/performance.js\";\nimport type { MinedDiscriminator, DiscriminatorMiningInput, VerifiedAssignment } from \"./discriminator.types.js\";\n\nconst logger = protocolLogger(\"PoolDiscriminatorMiner\");\n\n/** Max axes requested from (and accepted out of) one mining pass. */\nconst MAX_AXES = 6;\n\nconst AxisMiningResponseSchema = z.object({\n axes: z\n .array(\n z.object({\n axis: z\n .string()\n .describe(\"Short discriminating-axis label, e.g. 'Hands-on builders vs strategic advisors'\"),\n questionSeed: z\n .string()\n .describe(\"A direct question the intent owner could answer to resolve this axis\"),\n sides: z\n .array(z.string())\n .min(2)\n .max(3)\n .describe(\"2-3 mutually exclusive side labels (2-5 words each)\"),\n assignments: z\n .array(\n z.object({\n id: z.string().describe(\"Candidate id exactly as given\"),\n side: z\n .string()\n .nullable()\n .describe(\"One of this axis's side labels, or null when the context is insufficient\"),\n evidence: z\n .string()\n .max(200)\n .nullable()\n .describe(\n \"VERBATIM span (≤80 chars) copied from this candidate's context that justifies the side. null when side is null\",\n ),\n }),\n )\n .describe(\"One entry per candidate\"),\n }),\n )\n .max(MAX_AXES),\n});\n\ntype AxisMiningResponse = z.infer<typeof AxisMiningResponseSchema>;\n\nconst SYSTEM_PROMPT = `You analyze a pool of candidate matches for one user intent and propose the preference axes that best SPLIT the pool into meaningfully different groups.\n\nRules for every axis:\n- Sides must be mutually exclusive readings of one dimension (a candidate belongs to at most one side).\n- The axis must NOT already be determined by the intent text itself — only propose axes the intent leaves genuinely open.\n- The axis must be phrased as a preference the intent OWNER could hold and answer (the questionSeed asks them directly).\n- Side labels are terse (2-5 words), concrete, and derived from patterns across MULTIPLE candidates — never from a single person and never naming anyone.\n- BANNED axis families: protected attributes (race, ethnicity, religion, gender, age, nationality, disability, politics, sexual orientation), and vanity/noise splits (profile completeness, bio length, has photo, name alphabet).\n\nRules for assignments:\n- Assign EVERY candidate: pick a side only when the candidate's context contains clear support; otherwise use side null.\n- When you pick a side you MUST quote a short (≤80 chars) VERBATIM substring of that candidate's context as evidence. Copy it exactly — it is checked mechanically, and paraphrased evidence is discarded. Do NOT add punctuation that is not in the source (no trailing period); cutting off mid-sentence is fine.\n- Never invent context. If unsure, use side null with evidence null.\n\nPrefer axes that split the pool close to evenly and that cover many candidates with verifiable evidence. Propose at most ${MAX_AXES} axes; fewer strong axes beat many weak ones.`;\n\n/** Config for PoolDiscriminatorMiner construction. */\nexport interface PoolDiscriminatorMinerConfig {\n /** Optional model config override (API key / base URL / model). */\n modelConfig?: Parameters<typeof createStructuredModel>[3];\n}\n\n/**\n * Collapse whitespace, lowercase, and fold typographic punctuation to ASCII\n * (curly quotes/apostrophes, en/em dashes, ellipsis), so evidence matching\n * tolerates formatting drift without weakening the verbatim requirement.\n */\nexport function normalizePoolEvidenceForMatch(s: string): string {\n return s\n .toLowerCase()\n .replace(/[\\u2018\\u2019\\u02bc]/g, \"'\")\n .replace(/[\\u201c\\u201d]/g, '\"')\n .replace(/[\\u2013\\u2014]/g, \"-\")\n .replace(/\\u2026/g, \"...\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Strips wrapping quotes and edge punctuation from an evidence span before\n * matching. LLMs habitually sentence-ize copied spans (append a trailing\n * period, wrap in quotes); the meaningful content must still match verbatim.\n */\nexport function stripPoolEvidenceEdgePunctuation(s: string): string {\n return s.replace(/^[\\s\"'.,;:!?()\\u2018\\u2019\\u201c\\u201d]+/, \"\").replace(/[\\s\"'.,;:!?()\\u2018\\u2019\\u201c\\u201d]+$/, \"\");\n}\n\n/** Minimum normalized span that counts as meaningful verbatim evidence. */\nconst MIN_POOL_EVIDENCE_CHARS = 8;\n\n/** Evidence verifier shared by mining and newborn assignment. */\nexport function poolEvidenceMatches(publicContext: string, evidence: string | null): boolean {\n if (evidence === null) return false;\n const context = normalizePoolEvidenceForMatch(publicContext);\n const evidenceCore = normalizePoolEvidenceForMatch(stripPoolEvidenceEdgePunctuation(evidence));\n return evidenceCore.length >= MIN_POOL_EVIDENCE_CHARS && context.includes(evidenceCore);\n}\n\n/**\n * Stateless axis-mining agent. One `mine()` call = one structured LLM pass +\n * deterministic evidence verification.\n */\nexport class PoolDiscriminatorMiner {\n private model: ReturnType<typeof createStructuredModel<AxisMiningResponse>>;\n\n constructor(config?: PoolDiscriminatorMinerConfig) {\n this.model = createStructuredModel<AxisMiningResponse>(\n \"poolDiscriminatorMiner\",\n AxisMiningResponseSchema,\n { name: \"pool_axes\" },\n config?.modelConfig,\n );\n }\n\n /**\n * Mine discriminating axes for one pool.\n *\n * @returns Verified axes (may be empty). Throws on LLM failure — callers\n * run this fire-and-forget and must catch.\n */\n @Timed()\n async mine(\n input: DiscriminatorMiningInput,\n options?: { signal?: AbortSignal },\n ): Promise<MinedDiscriminator[]> {\n const prompt = buildMiningPrompt(input);\n const raw = await invokeWithAbortSignal(\n this.model,\n [new SystemMessage(SYSTEM_PROMPT), new HumanMessage(prompt)],\n options?.signal,\n );\n const parsed = AxisMiningResponseSchema.safeParse(raw);\n if (!parsed.success) {\n logger.warn(\"Mining response failed schema validation\", { issues: parsed.error.issues.length });\n return [];\n }\n return parsed.data.axes.slice(0, MAX_AXES).map((axis) => verifyAxis(axis, input.candidates));\n }\n}\n\n/** Builds the human message: intent text + one numbered block per candidate. */\nexport function buildMiningPrompt(input: DiscriminatorMiningInput): string {\n const candidateBlocks = input.candidates\n .map((c) => `[${c.id}]\\n${c.publicContext}`)\n .join(\"\\n\\n\");\n return [\n `Intent:\\n${input.intentText}`,\n `Candidate pool (${input.candidates.length} candidates, id in brackets):\\n\\n${candidateBlocks}`,\n ].join(\"\\n\\n\");\n}\n\n/**\n * Deterministic post-pass over one LLM axis:\n * - drops assignments for ids not in the pool,\n * - demotes to unknown: sides not in the axis's side list, missing evidence,\n * and evidence that does not substring-match the candidate's publicContext,\n * - guarantees exactly one assignment per pool candidate (missing → unknown),\n * - computes evidenceRate = verified / side-proposals.\n */\nexport function verifyAxis(\n axis: AxisMiningResponse[\"axes\"][number],\n candidates: DiscriminatorMiningInput[\"candidates\"],\n): MinedDiscriminator {\n const contextById = new Map(candidates.map((c) => [c.id, c.publicContext]));\n const byId = new Map<string, VerifiedAssignment>();\n let proposed = 0;\n let verified = 0;\n\n for (const a of axis.assignments) {\n const context = contextById.get(a.id);\n if (context === undefined) continue; // hallucinated candidate id\n if (byId.has(a.id)) continue; // first proposal wins\n if (a.side === null) {\n byId.set(a.id, { id: a.id, side: null, evidence: null, verified: false });\n continue;\n }\n proposed++;\n const sideOk = axis.sides.includes(a.side);\n const evidenceOk = poolEvidenceMatches(context, a.evidence);\n if (sideOk && evidenceOk) {\n verified++;\n byId.set(a.id, { id: a.id, side: a.side, evidence: a.evidence, verified: true });\n } else {\n // Demoted: keep the (failed) evidence for audit logging, but unknown side.\n byId.set(a.id, { id: a.id, side: null, evidence: a.evidence, verified: false });\n }\n }\n\n // Every pool candidate gets exactly one assignment.\n const assignments: VerifiedAssignment[] = candidates.map(\n (c) => byId.get(c.id) ?? { id: c.id, side: null, evidence: null, verified: false },\n );\n\n return {\n label: axis.axis,\n questionSeed: axis.questionSeed,\n sides: axis.sides,\n assignments,\n evidenceRate: proposed > 0 ? verified / proposed : 0,\n };\n}\n"]}
|
|
@@ -41,7 +41,7 @@ export type OpportunityEvaluatorLike = {
|
|
|
41
41
|
}>>;
|
|
42
42
|
};
|
|
43
43
|
import type { Embedder } from '../shared/interfaces/embedder.interface.js';
|
|
44
|
-
import type { Opportunity } from '../shared/interfaces/database.interface.js';
|
|
44
|
+
import type { CreateOpportunityData, Opportunity } from '../shared/interfaces/database.interface.js';
|
|
45
45
|
import type { NegotiationGraphLike } from "../negotiation/negotiation.state.js";
|
|
46
46
|
import type { AgentDispatcher } from "../shared/interfaces/agent-dispatcher.interface.js";
|
|
47
47
|
/** Input shape for the HyDE graph invoke call (query-based embedding). */
|
|
@@ -53,6 +53,17 @@ export interface HydeGeneratorInvokeInput {
|
|
|
53
53
|
}
|
|
54
54
|
/** Optional notifier for opportunity send; when omitted, the real queue is used via dynamic import. */
|
|
55
55
|
export type QueueOpportunityNotificationFn = (opportunityId: string, recipientId: string, priority: 'immediate' | 'high' | 'low') => Promise<unknown>;
|
|
56
|
+
/** Input for the host-side newborn pool-preference stamper (IND-420 P4b). */
|
|
57
|
+
export interface StampNewbornOpportunitiesInput {
|
|
58
|
+
ownerUserId: string;
|
|
59
|
+
intentId: string;
|
|
60
|
+
items: CreateOpportunityData[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Optional host callback that stamps call-local create items before INSERT.
|
|
64
|
+
* It must preserve array length/order and may only enrich metadata/signals.
|
|
65
|
+
*/
|
|
66
|
+
export type StampNewbornOpportunitiesFn = (input: StampNewbornOpportunitiesInput) => Promise<CreateOpportunityData[]>;
|
|
56
67
|
/**
|
|
57
68
|
* Builds a compact text summary of the discoverer's profile and active intents
|
|
58
69
|
* for use as profileContext in HyDE generation.
|
|
@@ -84,6 +95,8 @@ export declare class OpportunityGraphFactory {
|
|
|
84
95
|
* negotiations after introducer approval.
|
|
85
96
|
*/
|
|
86
97
|
private queueNegotiateExisting?;
|
|
98
|
+
/** Host-side P4b stamper. Omitted by manual/introducer/enrichment roots. */
|
|
99
|
+
private stampNewbornOpportunities?;
|
|
87
100
|
constructor(database: OpportunityGraphDatabase, embedder: Embedder, hydeGenerator: {
|
|
88
101
|
invoke: (input: HydeGeneratorInvokeInput) => Promise<{
|
|
89
102
|
hydeEmbeddings: Record<string, number[]>;
|
|
@@ -108,7 +121,9 @@ export declare class OpportunityGraphFactory {
|
|
|
108
121
|
* When provided, negotiate_existing mode uses this to queue follow-up
|
|
109
122
|
* negotiations after introducer approval.
|
|
110
123
|
*/
|
|
111
|
-
queueNegotiateExisting?: ((opportunityId: string, userId: string) => Promise<void>) | undefined
|
|
124
|
+
queueNegotiateExisting?: ((opportunityId: string, userId: string) => Promise<void>) | undefined,
|
|
125
|
+
/** Host-side P4b stamper. Omitted by manual/introducer/enrichment roots. */
|
|
126
|
+
stampNewbornOpportunities?: StampNewbornOpportunitiesFn | undefined);
|
|
112
127
|
createGraph(): import("@langchain/langgraph").CompiledStateGraph<{
|
|
113
128
|
userId: Id<"users">;
|
|
114
129
|
searchQuery: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opportunity.graph.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,4CAA4C,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAyB,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAExO,OAAO,EAAwB,KAAK,gBAAgB,EAAuC,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACzK,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAM3F,yDAAyD;AACzD,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,CAAC,EAAE,CACP,oBAAoB,EAAE,MAAM,EAC5B,UAAU,EAAE,gBAAgB,EAAE,EAC9B,OAAO,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KAC3B,OAAO,CAAC,KAAK,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;KAC3C,CAAC,CAAC,CAAC;IACJ,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,KAAK,CAAC;QAC5F,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC,CAAC;KAC9H,CAAC,CAAC,CAAC;CACL,CAAC;AACF,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,4CAA4C,CAAC;AAC1F,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"opportunity.graph.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -69,6 +69,35 @@ function buildEvaluatorEvidenceKey(candidate) {
|
|
|
69
69
|
candidate.candidateIntentId ?? candidate.candidatePremiseId ?? candidate.sourceContextId ?? 'profile',
|
|
70
70
|
].join(':');
|
|
71
71
|
}
|
|
72
|
+
function copyCreateOpportunityData(item) {
|
|
73
|
+
return {
|
|
74
|
+
...item,
|
|
75
|
+
detection: { ...item.detection },
|
|
76
|
+
actors: item.actors.map((actor) => ({ ...actor })),
|
|
77
|
+
interpretation: {
|
|
78
|
+
...item.interpretation,
|
|
79
|
+
signals: item.interpretation.signals?.map((signal) => ({ ...signal })),
|
|
80
|
+
},
|
|
81
|
+
context: { ...item.context },
|
|
82
|
+
metadata: item.metadata ? { ...item.metadata } : item.metadata,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Fields a stamper is not allowed to change; also protects candidate order. */
|
|
86
|
+
function newbornItemIdentity(item) {
|
|
87
|
+
return JSON.stringify({
|
|
88
|
+
detection: item.detection,
|
|
89
|
+
actors: item.actors,
|
|
90
|
+
interpretation: {
|
|
91
|
+
category: item.interpretation.category,
|
|
92
|
+
reasoning: item.interpretation.reasoning,
|
|
93
|
+
confidence: item.interpretation.confidence,
|
|
94
|
+
},
|
|
95
|
+
context: item.context,
|
|
96
|
+
confidence: item.confidence,
|
|
97
|
+
status: item.status,
|
|
98
|
+
expiresAt: item.expiresAt?.toISOString(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
72
101
|
/**
|
|
73
102
|
* Builds a compact text summary of the discoverer's profile and active intents
|
|
74
103
|
* for use as profileContext in HyDE generation.
|
|
@@ -144,7 +173,9 @@ export class OpportunityGraphFactory {
|
|
|
144
173
|
* When provided, negotiate_existing mode uses this to queue follow-up
|
|
145
174
|
* negotiations after introducer approval.
|
|
146
175
|
*/
|
|
147
|
-
queueNegotiateExisting
|
|
176
|
+
queueNegotiateExisting,
|
|
177
|
+
/** Host-side P4b stamper. Omitted by manual/introducer/enrichment roots. */
|
|
178
|
+
stampNewbornOpportunities) {
|
|
148
179
|
this.database = database;
|
|
149
180
|
this.embedder = embedder;
|
|
150
181
|
this.hydeGenerator = hydeGenerator;
|
|
@@ -153,6 +184,7 @@ export class OpportunityGraphFactory {
|
|
|
153
184
|
this.negotiationGraph = negotiationGraph;
|
|
154
185
|
this.agentDispatcher = agentDispatcher;
|
|
155
186
|
this.queueNegotiateExisting = queueNegotiateExisting;
|
|
187
|
+
this.stampNewbornOpportunities = stampNewbornOpportunities;
|
|
156
188
|
}
|
|
157
189
|
createGraph() {
|
|
158
190
|
const evaluatorAgent = this.optionalEvaluator ?? new OpportunityEvaluator();
|
|
@@ -2754,10 +2786,61 @@ export class OpportunityGraphFactory {
|
|
|
2754
2786
|
}
|
|
2755
2787
|
itemsToPersist.push(data);
|
|
2756
2788
|
}
|
|
2789
|
+
// P4b seam: only genuinely new, create-mode, owned-intent discovery
|
|
2790
|
+
// items reach the host stamper. Dedup reactivations/upgrades have
|
|
2791
|
+
// already continued above; introductions, on-behalf-of, context-only,
|
|
2792
|
+
// manual, and continuation flows are excluded explicitly.
|
|
2793
|
+
let itemsForPersistence = itemsToPersist;
|
|
2794
|
+
const stampIntentId = state.resolvedTriggerIntentId;
|
|
2795
|
+
const mayStamp = Boolean(this.stampNewbornOpportunities
|
|
2796
|
+
&& state.operationMode === 'create'
|
|
2797
|
+
&& !state.introductionContext
|
|
2798
|
+
&& !state.onBehalfOfUserId
|
|
2799
|
+
&& !state.targetUserId
|
|
2800
|
+
&& state.discoverySource === 'intent'
|
|
2801
|
+
&& stampIntentId
|
|
2802
|
+
&& state.indexedIntents.some((intent) => intent.intentId === stampIntentId));
|
|
2803
|
+
if (mayStamp && stampIntentId && itemsToPersist.length > 0) {
|
|
2804
|
+
const eligibleIndexes = itemsToPersist.flatMap((item, index) => item.detection.source === 'opportunity_graph' && item.detection.triggeredBy === stampIntentId
|
|
2805
|
+
? [index]
|
|
2806
|
+
: []);
|
|
2807
|
+
if (eligibleIndexes.length > 0) {
|
|
2808
|
+
const originals = eligibleIndexes.map((index) => itemsToPersist[index]);
|
|
2809
|
+
const callbackItems = originals.map(copyCreateOpportunityData);
|
|
2810
|
+
try {
|
|
2811
|
+
const stamped = await this.stampNewbornOpportunities({
|
|
2812
|
+
ownerUserId: state.userId,
|
|
2813
|
+
intentId: stampIntentId,
|
|
2814
|
+
items: callbackItems,
|
|
2815
|
+
});
|
|
2816
|
+
const valid = Array.isArray(stamped)
|
|
2817
|
+
&& stamped.length === originals.length
|
|
2818
|
+
&& stamped.every((item, index) => newbornItemIdentity(item) === newbornItemIdentity(originals[index]));
|
|
2819
|
+
if (valid) {
|
|
2820
|
+
itemsForPersistence = [...itemsToPersist];
|
|
2821
|
+
eligibleIndexes.forEach((itemIndex, stampedIndex) => {
|
|
2822
|
+
itemsForPersistence[itemIndex] = stamped[stampedIndex];
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
else {
|
|
2826
|
+
persistLog.warn('Newborn stamper returned unsafe length/order; persisting originals', {
|
|
2827
|
+
expected: originals.length,
|
|
2828
|
+
actual: Array.isArray(stamped) ? stamped.length : null,
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
catch (error) {
|
|
2833
|
+
persistLog.warn('Newborn stamper failed; persisting originals', {
|
|
2834
|
+
intentId: stampIntentId,
|
|
2835
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2757
2840
|
const { created: createdList } = await persistOpportunities({
|
|
2758
2841
|
database: this.database,
|
|
2759
2842
|
embedder: this.embedder,
|
|
2760
|
-
items:
|
|
2843
|
+
items: itemsForPersistence,
|
|
2761
2844
|
});
|
|
2762
2845
|
const allOpportunities = [...reactivatedOpportunities, ...createdList];
|
|
2763
2846
|
persistLog.verbose('Persistence complete', {
|