@land-catalyst/batch-data-sdk 1.2.8 → 1.2.10

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.
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * Builds complete context for AI services to generate SearchCriteria from natural language.
5
5
  * This includes all domain documentation, filter types, examples, and rules.
6
+ *
7
+ * IMPORTANT: This module only documents SEARCHABLE fields (fields that can be used in SearchCriteria).
8
+ * Response-only fields (like openLien.mortgages[], owner.names[], sale.lastSale.mortgages[], etc.)
9
+ * are intentionally excluded as they appear in Property responses but cannot be used as search filters.
10
+ * All fields documented here correspond to the SearchCriteria type interfaces in types.ts.
6
11
  */
7
12
  /**
8
13
  * Domain metadata with description
@@ -4,6 +4,11 @@
4
4
  *
5
5
  * Builds complete context for AI services to generate SearchCriteria from natural language.
6
6
  * This includes all domain documentation, filter types, examples, and rules.
7
+ *
8
+ * IMPORTANT: This module only documents SEARCHABLE fields (fields that can be used in SearchCriteria).
9
+ * Response-only fields (like openLien.mortgages[], owner.names[], sale.lastSale.mortgages[], etc.)
10
+ * are intentionally excluded as they appear in Property responses but cannot be used as search filters.
11
+ * All fields documented here correspond to the SearchCriteria type interfaces in types.ts.
7
12
  */
8
13
  Object.defineProperty(exports, "__esModule", { value: true });
9
14
  exports.SEARCH_CRITERIA_DOMAIN_NAMES = exports.SEARCH_CRITERIA_DOMAINS = void 0;
@@ -212,19 +217,73 @@ function buildSearchCriteriaSystemPrompt(domains, options) {
212
217
  ${domainDocs}
213
218
 
214
219
  FILTER TYPES:
215
- - StringFilter: { equals?, contains?, startsWith?, endsWith?, inList?, matches? }
220
+ - StringFilter: { equals?, contains?, startsWith?, endsWith?, inList?, notInList?, matches? }
221
+ * equals: Exact string match - use for precise value matching
222
+ * contains: Substring match - matches if the field value contains the specified string anywhere
223
+ * startsWith: Prefix match - matches if the field value starts with the specified string
224
+ * endsWith: Suffix match - matches if the field value ends with the specified string
225
+ * inList: Array of allowed values - matches if field value is in the list (PREFERRED for fields with predefined possible values)
226
+ * notInList: Array of excluded values - matches if field value is NOT in the list (PREFERRED for excluding predefined values)
227
+ * matches: Array of pattern matches - similar to inList but for pattern-based matching
228
+ * For fields with predefined possible values (shown in field documentation), prefer inList (match any) or notInList (exclude) over equals/contains
229
+ * Example: { inList: ["Central", "Window Unit"] } or { notInList: ["None"] } or { contains: "Street" }
230
+
216
231
  - NumericRangeFilter: { min?, max? }
217
- - DateRangeFilter: { minDate?, maxDate? } (Dates in ISO 8601 format YYYY-MM-DD. For "after [year]", use next year's January 1st, e.g., "after 2015" = { minDate: "2016-01-01" })
232
+ * min: Minimum value (inclusive) - matches if field value >= min
233
+ * max: Maximum value (inclusive) - matches if field value <= max
234
+ * Can use both min and max together for a range, or just one for one-sided filtering
235
+ * Example: { min: 3, max: 5 } for "3 to 5 bedrooms", { min: 500000 } for "at least $500k", { max: 1000000 } for "under $1M"
236
+
237
+ - DateRangeFilter: { minDate?, maxDate? }
238
+ * minDate: Minimum date (inclusive) - dates in ISO 8601 format YYYY-MM-DD
239
+ * maxDate: Maximum date (inclusive) - dates in ISO 8601 format YYYY-MM-DD
240
+ * For "after [year]" queries, use next year's January 1st: "after 2015" = { minDate: "2016-01-01" }
241
+ * For "before [year]" queries, use that year's December 31st: "before 2020" = { maxDate: "2020-12-31" }
242
+ * Example: { minDate: "2016-01-01" } for "built after 2015", { maxDate: "2020-12-31" } for "sold before 2021"
243
+
218
244
  - BooleanFilter: { equals: boolean }
219
- - QuickListValue: string (can prefix with "not-" to exclude, e.g., "not-vacant")
220
- - GeoLocationDistance: { latitude, longitude, distanceMiles }
221
- - GeoLocationBoundingBox: { minLat, maxLat, minLon, maxLon }
222
- - GeoLocationPolygon: { geoPoints: [{ latitude, longitude }] }
245
+ * equals: Exact boolean match - must be true or false
246
+ * Use for fields that only have true/false values (e.g., ownerOccupied, hasChildren)
247
+ * Example: { equals: true } for "owner occupied properties", { equals: false } for "non-owner occupied"
248
+
249
+ - QuickListValue: string
250
+ * Single quickList value string - matches properties with that specific characteristic
251
+ * Can prefix with "not-" to exclude: "not-vacant" excludes vacant properties
252
+ * Valid values include: "vacant", "preforeclosure", "tax-default", "fix-and-flip", "absentee-owner", etc.
253
+ * See QUICKLIST VALUES section for complete list and descriptions
254
+ * Example: "vacant" or "not-active-listing"
255
+
256
+ - GeoLocationDistance: { geoPoint: { latitude, longitude }, distanceMiles?, distanceKilometers?, distanceMeters?, distanceFeet?, distanceYards? }
257
+ * geoPoint: Center point with latitude and longitude (both required)
258
+ * distanceMiles: Search radius in miles (one distance unit required)
259
+ * distanceKilometers: Search radius in kilometers (alternative to miles)
260
+ * distanceMeters: Search radius in meters (alternative to miles)
261
+ * distanceFeet: Search radius in feet (alternative to miles)
262
+ * distanceYards: Search radius in yards (alternative to miles)
263
+ * Matches properties within the specified distance of the center point
264
+ * Example: { geoPoint: { latitude: 33.4484, longitude: -112.0740 }, distanceMiles: "5" } for "within 5 miles of Phoenix"
265
+
266
+ - GeoLocationBoundingBox: { nwGeoPoint: { latitude, longitude }, seGeoPoint: { latitude, longitude } }
267
+ * nwGeoPoint: Northwest corner of the bounding box (top-left)
268
+ * seGeoPoint: Southeast corner of the bounding box (bottom-right)
269
+ * Matches properties within the rectangular area defined by these two points
270
+ * Useful for filtering by geographic regions or specific areas
271
+ * Example: { nwGeoPoint: { latitude: 33.5, longitude: -112.1 }, seGeoPoint: { latitude: 33.3, longitude: -112.0 } }
272
+
273
+ - GeoLocationPolygon: { geoPoints: [{ latitude, longitude }, ...] }
274
+ * geoPoints: Array of GeoPoint objects defining the polygon vertices
275
+ * Matches properties within the polygon area defined by connecting the points in order
276
+ * Requires at least 3 points to form a valid polygon
277
+ * Useful for irregularly shaped geographic areas
278
+ * Example: { geoPoints: [{ latitude: 33.5, longitude: -112.1 }, { latitude: 33.4, longitude: -112.1 }, { latitude: 33.4, longitude: -112.0 }, { latitude: 33.5, longitude: -112.0 }] }
223
279
 
224
280
  IMPORTANT RULES:
225
281
  1. Always include a "query" field with geographic scope (state, county, city, or "US")
226
282
  2. Use numeric ranges (min/max) for numeric filters like yearBuilt, bedroomCount, price, etc.
227
- 3. Use string filters (equals, contains, inList) for text fields
283
+ 3. Use string filters appropriately:
284
+ - For fields with predefined possible values (shown in field documentation): Use inList (match any) or notInList (exclude) instead of equals/contains
285
+ - For free-text fields: Use equals (exact match), contains (substring), startsWith, endsWith, or inList
286
+ - Example: building.airConditioningSource has predefined values ["Central", "Window Unit", "Wall", ...] - use { inList: ["Central", "Window Unit"] } instead of { equals: "Central" }
228
287
  4. Be specific and realistic:
229
288
  - "3 bedrooms" = building.bedroomCount: {min: 3, max: 3}
230
289
  - "at least 3 bedrooms" = building.bedroomCount: {min: 3}
@@ -283,6 +342,55 @@ Example response format:
283
342
  If existing criteria is provided, merge intelligently - update fields mentioned in the prompt, preserve others.
284
343
  Return only valid JSON, no markdown formatting or code blocks.`;
285
344
  }
345
+ /**
346
+ * Consolidate repetitive openLien loan fields into groups
347
+ * Groups fields like firstLoanType, secondLoanType, thirdLoanType, fourthLoanType together
348
+ */
349
+ function consolidateOpenLienFields(fields) {
350
+ const result = [];
351
+ const processed = new Set();
352
+ // Patterns to consolidate: [first|second|third|fourth]Loan[Type|InterestRate]
353
+ const loanTypePattern = /^(first|second|third|fourth)LoanType$/;
354
+ const loanInterestRatePattern = /^(first|second|third|fourth)LoanInterestRate$/;
355
+ // Group loan type fields
356
+ const loanTypeFields = fields.filter((f) => loanTypePattern.test(f.name));
357
+ if (loanTypeFields.length >= 2) {
358
+ // All should have same type and similar descriptions
359
+ const firstField = loanTypeFields[0];
360
+ const allSameType = loanTypeFields.every((f) => f.type === firstField.type);
361
+ if (allSameType) {
362
+ result.push({
363
+ type: "grouped",
364
+ pattern: firstField.description.replace(/^(First|Second|Third|Fourth)\s+/i, "[First/Second/Third/Fourth] "),
365
+ fieldNames: loanTypeFields.map((f) => f.name).sort(),
366
+ templateField: firstField,
367
+ });
368
+ loanTypeFields.forEach((f) => processed.add(f.name));
369
+ }
370
+ }
371
+ // Group loan interest rate fields
372
+ const loanInterestRateFields = fields.filter((f) => loanInterestRatePattern.test(f.name));
373
+ if (loanInterestRateFields.length >= 2) {
374
+ const firstField = loanInterestRateFields[0];
375
+ const allSameType = loanInterestRateFields.every((f) => f.type === firstField.type);
376
+ if (allSameType) {
377
+ result.push({
378
+ type: "grouped",
379
+ pattern: firstField.description.replace(/^(first|second|third|fourth)/i, "[first/second/third/fourth]") + " (applies to all listed fields)",
380
+ fieldNames: loanInterestRateFields.map((f) => f.name).sort(),
381
+ templateField: firstField,
382
+ });
383
+ loanInterestRateFields.forEach((f) => processed.add(f.name));
384
+ }
385
+ }
386
+ // Add remaining unprocessed fields
387
+ for (const field of fields) {
388
+ if (!processed.has(field.name)) {
389
+ result.push(field);
390
+ }
391
+ }
392
+ return result;
393
+ }
286
394
  /**
287
395
  * Get context documentation for a specific domain
288
396
  * @param domainName The domain name (e.g., "address", "building", "assessment")
@@ -327,6 +435,15 @@ function getDomainContext(domainName, options = {}) {
327
435
  doc += "\n Available fields:\n";
328
436
  for (const field of fields) {
329
437
  doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
438
+ // Add possible values information if available
439
+ const fieldContext = (0, search_criteria_filter_context_1.getSearchCriteriaFilterContext)(field.name);
440
+ if (fieldContext?.possibleValues &&
441
+ fieldContext.possibleValues.length > 0) {
442
+ doc += ` Possible Values (${fieldContext.possibleValues.length} total): `;
443
+ // Show all possible values inline
444
+ doc += fieldContext.possibleValues.map((v) => `"${v}"`).join(", ");
445
+ doc += `\n`;
446
+ }
330
447
  // Removed filterGuidance - filter types are already defined in FILTER TYPES section
331
448
  if (field.examples && field.examples.length > 0) {
332
449
  doc += ` Examples: ${field.examples.join(", ")}\n`;
@@ -392,6 +509,8 @@ function buildDomainDocumentation(domainNames, options = {}) {
392
509
  else if (optionalDomains.length > 0) {
393
510
  doc += "OPTIONAL DOMAINS:\n";
394
511
  }
512
+ // Note: Possible values are shown inline with each field for better context
513
+ // We no longer need a separate constants section since values appear with their fields
395
514
  // Add QuickList values section once if quickList domain is included
396
515
  if (hasQuickListDomain) {
397
516
  doc += "\n";
@@ -405,6 +524,15 @@ function buildDomainDocumentation(domainNames, options = {}) {
405
524
  doc += "\n Available fields:\n";
406
525
  for (const field of quickListDomain.fields) {
407
526
  doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
527
+ // Add possible values information if available
528
+ const fieldContext = (0, search_criteria_filter_context_1.getSearchCriteriaFilterContext)(field.name);
529
+ if (fieldContext?.possibleValues &&
530
+ fieldContext.possibleValues.length > 0) {
531
+ doc += ` Possible Values (${fieldContext.possibleValues.length} total): `;
532
+ // Show all possible values inline
533
+ doc += fieldContext.possibleValues.map((v) => `"${v}"`).join(", ");
534
+ doc += `\n`;
535
+ }
408
536
  // Removed filterGuidance - filter types are already defined in FILTER TYPES section
409
537
  if (field.examples && field.examples.length > 0) {
410
538
  doc += ` Examples: ${field.examples.join(", ")}\n`;
@@ -417,11 +545,69 @@ function buildDomainDocumentation(domainNames, options = {}) {
417
545
  doc += `\n- ${domain.name}: ${domain.description}`;
418
546
  if (domain.fields.length > 0) {
419
547
  doc += "\n Available fields:\n";
420
- for (const field of domain.fields) {
421
- doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
422
- // Removed filterGuidance - filter types are already defined in FILTER TYPES section
423
- if (field.examples && field.examples.length > 0) {
424
- doc += ` Examples: ${field.examples.join(", ")}\n`;
548
+ // Special handling for openLien domain to consolidate repetitive loan fields
549
+ if (domain.name === "openLien") {
550
+ const consolidatedFields = consolidateOpenLienFields(domain.fields);
551
+ for (const fieldOrGroup of consolidatedFields) {
552
+ if (fieldOrGroup.type === "grouped") {
553
+ // Handle grouped fields (e.g., firstLoanType, secondLoanType, etc.)
554
+ const group = fieldOrGroup;
555
+ doc += ` - ${group.fieldNames.join(", ")} (${group.templateField.type}): ${group.pattern}\n`;
556
+ const templateContext = (0, search_criteria_filter_context_1.getDomainFilterContexts)(domain.name).find((ctx) => ctx.searchCriteriaPath.split(".").pop() ===
557
+ group.templateField.name ||
558
+ ctx.searchCriteriaPath === group.templateField.name);
559
+ if (templateContext?.possibleValues &&
560
+ templateContext.possibleValues.length > 0) {
561
+ doc += ` Possible Values (${templateContext.possibleValues.length} total): `;
562
+ // Show all possible values inline
563
+ doc +=
564
+ templateContext.possibleValues.map((v) => `"${v}"`).join(", ") +
565
+ `\n`;
566
+ }
567
+ if (group.templateField.examples &&
568
+ group.templateField.examples.length > 0) {
569
+ doc += ` Examples: ${group.templateField.examples.join(", ")}\n`;
570
+ }
571
+ }
572
+ else {
573
+ // Handle individual fields
574
+ const field = fieldOrGroup;
575
+ doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
576
+ const fieldContext = (0, search_criteria_filter_context_1.getDomainFilterContexts)(domain.name).find((ctx) => ctx.searchCriteriaPath.split(".").pop() === field.name ||
577
+ ctx.searchCriteriaPath === field.name);
578
+ if (fieldContext?.possibleValues &&
579
+ fieldContext.possibleValues.length > 0) {
580
+ doc += ` Possible Values (${fieldContext.possibleValues.length} total): `;
581
+ // Show all possible values inline
582
+ doc +=
583
+ fieldContext.possibleValues.map((v) => `"${v}"`).join(", ") +
584
+ `\n`;
585
+ }
586
+ if (field.examples && field.examples.length > 0) {
587
+ doc += ` Examples: ${field.examples.join(", ")}\n`;
588
+ }
589
+ }
590
+ }
591
+ }
592
+ else {
593
+ // Standard output for other domains
594
+ for (const field of domain.fields) {
595
+ doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
596
+ // Add possible values information if available
597
+ const fieldContext = (0, search_criteria_filter_context_1.getDomainFilterContexts)(domain.name).find((ctx) => ctx.searchCriteriaPath.split(".").pop() === field.name ||
598
+ ctx.searchCriteriaPath === field.name);
599
+ if (fieldContext?.possibleValues &&
600
+ fieldContext.possibleValues.length > 0) {
601
+ doc += ` Possible Values (${fieldContext.possibleValues.length} total): `;
602
+ // Show all possible values inline
603
+ doc +=
604
+ fieldContext.possibleValues.map((v) => `"${v}"`).join(", ") +
605
+ `\n`;
606
+ }
607
+ // Removed filterGuidance - filter types are already defined in FILTER TYPES section
608
+ if (field.examples && field.examples.length > 0) {
609
+ doc += ` Examples: ${field.examples.join(", ")}\n`;
610
+ }
425
611
  }
426
612
  }
427
613
  }
@@ -41,6 +41,14 @@ export interface SearchCriteriaFilterContext {
41
41
  * Additional context about how this filter should be used
42
42
  */
43
43
  filterGuidance: string;
44
+ /**
45
+ * Possible values for this field (if it has a limited set of allowed values)
46
+ */
47
+ possibleValues?: string[];
48
+ /**
49
+ * Reference to the constant name for possible values (if applicable)
50
+ */
51
+ possibleValuesRef?: string;
44
52
  }
45
53
  /**
46
54
  * Get filter context for a SearchCriteria field path
@@ -75,6 +83,12 @@ export declare function getSearchCriteriaFilterContextString(searchCriteriaPath:
75
83
  * @returns Formatted string with all QuickList values and descriptions
76
84
  */
77
85
  export declare function getQuickListValuesSection(): string;
86
+ /**
87
+ * Get formatted Possible Values Constants section for documentation
88
+ * This defines all possible value constants once, which can then be referenced by fields
89
+ * @returns Formatted string with all possible value constants
90
+ */
91
+ export declare function getPossibleValuesConstantsSection(): string;
78
92
  /**
79
93
  * Get filter context for all fields in a SearchCriteria domain/group
80
94
  * @param domainName The domain name (e.g., "address", "building", "assessment", "quickList")
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.getSearchCriteriaFilterContext = getSearchCriteriaFilterContext;
10
10
  exports.getSearchCriteriaFilterContextString = getSearchCriteriaFilterContextString;
11
11
  exports.getQuickListValuesSection = getQuickListValuesSection;
12
+ exports.getPossibleValuesConstantsSection = getPossibleValuesConstantsSection;
12
13
  exports.getDomainFilterContexts = getDomainFilterContexts;
13
14
  exports.getDomainFilterContextDocumentation = getDomainFilterContextDocumentation;
14
15
  const metadata_1 = require("./metadata");
@@ -191,11 +192,14 @@ const SEARCH_CRITERIA_FILTER_TYPE_MAP = {
191
192
  "valuation.equityPercent": "NumericRangeFilter",
192
193
  };
193
194
  /**
194
- * Get filter guidance text based on filter type
195
+ * Get filter guidance text based on filter type and whether field has possible values
195
196
  */
196
- function getFilterGuidance(filterType) {
197
+ function getFilterGuidance(filterType, hasPossibleValues) {
197
198
  switch (filterType) {
198
199
  case "StringFilter":
200
+ if (hasPossibleValues) {
201
+ return `Use StringFilter with: equals (exact match), inList (array of allowed values - RECOMMENDED for fields with predefined values), or notInList (array of excluded values). For fields with multiple possible values, prefer inList/notInList over equals/contains. Example: { equals: "Central" } or { inList: ["Central", "Window Unit", "Wall"] } or { notInList: ["None"] }`;
202
+ }
199
203
  return `Use StringFilter with: equals (exact match), contains (substring), startsWith, endsWith, inList (array of values), or matches (regex). Example: { equals: "Phoenix" } or { inList: ["Phoenix", "Tucson"] }`;
200
204
  case "NumericRangeFilter":
201
205
  return `Use NumericRangeFilter with: min (minimum value), max (maximum value), or both. Example: { min: 3, max: 5 } for "3-5 bedrooms" or { min: 2010 } for "built after 2010"`;
@@ -212,39 +216,62 @@ function getFilterGuidance(filterType) {
212
216
  /**
213
217
  * Get example values based on field and filter type
214
218
  */
215
- function getExamples(fieldPath, filterType, _propertyMetadata) {
219
+ function getExamples(fieldPath, filterType, propertyMetadata) {
216
220
  const examples = [];
217
221
  switch (filterType) {
218
- case "StringFilter":
219
- if (fieldPath.includes("city")) {
220
- examples.push('{ equals: "Phoenix" }');
221
- examples.push('{ inList: ["Phoenix", "Tucson", "Mesa"] }');
222
- }
223
- else if (fieldPath.includes("state")) {
224
- examples.push('{ equals: "AZ" }');
225
- }
226
- else if (fieldPath.includes("status")) {
227
- examples.push('{ equals: "active" }');
228
- }
229
- else if (fieldPath.includes("street")) {
230
- examples.push('{ equals: "123 Main Street" }');
231
- examples.push('{ contains: "Main" } // streets containing "Main"');
232
- }
233
- else if (fieldPath.includes("description")) {
234
- examples.push('{ contains: "pool" } // listings containing "pool"');
235
- examples.push('{ contains: "renovated" } // listings containing "renovated"');
236
- }
237
- else if (fieldPath.includes("subdivisionName")) {
238
- examples.push('{ equals: "Sunset Hills" }');
239
- examples.push('{ contains: "Hills" } // subdivisions containing "Hills"');
222
+ case "StringFilter": {
223
+ // If field has possible values, prioritize inList/notInList examples
224
+ const possibleValues = propertyMetadata
225
+ ? (0, metadata_1.getPossibleValues)(propertyMetadata)
226
+ : undefined;
227
+ if (possibleValues && possibleValues.length > 0) {
228
+ // Show examples using inList and notInList for fields with predefined values
229
+ const sampleValues = possibleValues.slice(0, 3);
230
+ examples.push(`{ equals: "${sampleValues[0]}" } // exact match`);
231
+ examples.push(`{ inList: [${sampleValues.map((v) => `"${v}"`).join(", ")}] } // match any of these values`);
232
+ if (possibleValues.length > 3) {
233
+ examples.push(`{ inList: [${sampleValues.map((v) => `"${v}"`).join(", ")}, ...] } // can include more values from the allowed set`);
234
+ }
235
+ examples.push(`{ notInList: ["${sampleValues[0]}"] } // exclude this value`);
236
+ if (possibleValues.length > 1) {
237
+ examples.push(`{ notInList: [${sampleValues
238
+ .slice(0, 2)
239
+ .map((v) => `"${v}"`)
240
+ .join(", ")}] } // exclude multiple values`);
241
+ }
240
242
  }
241
- else if (fieldPath.includes("Name") &&
242
- (fieldPath.includes("firstName") || fieldPath.includes("lastName"))) {
243
- examples.push('{ equals: "Smith" }');
244
- examples.push('{ contains: "Smith" } // names containing "Smith"');
243
+ else {
244
+ // Generic string filter examples for fields without predefined values
245
+ if (fieldPath.includes("city")) {
246
+ examples.push('{ equals: "Phoenix" }');
247
+ examples.push('{ inList: ["Phoenix", "Tucson", "Mesa"] }');
248
+ }
249
+ else if (fieldPath.includes("state")) {
250
+ examples.push('{ equals: "AZ" }');
251
+ }
252
+ else if (fieldPath.includes("status")) {
253
+ examples.push('{ equals: "active" }');
254
+ }
255
+ else if (fieldPath.includes("street")) {
256
+ examples.push('{ equals: "123 Main Street" }');
257
+ examples.push('{ contains: "Main" } // streets containing "Main"');
258
+ }
259
+ else if (fieldPath.includes("description")) {
260
+ examples.push('{ contains: "pool" } // listings containing "pool"');
261
+ examples.push('{ contains: "renovated" } // listings containing "renovated"');
262
+ }
263
+ else if (fieldPath.includes("subdivisionName")) {
264
+ examples.push('{ equals: "Sunset Hills" }');
265
+ examples.push('{ contains: "Hills" } // subdivisions containing "Hills"');
266
+ }
267
+ else if (fieldPath.includes("Name") &&
268
+ (fieldPath.includes("firstName") || fieldPath.includes("lastName"))) {
269
+ examples.push('{ equals: "Smith" }');
270
+ examples.push('{ contains: "Smith" } // names containing "Smith"');
271
+ }
245
272
  }
246
- // Removed generic examples - only include meaningful, field-specific ones
247
273
  break;
274
+ }
248
275
  case "NumericRangeFilter":
249
276
  if (fieldPath.includes("salePropensity")) {
250
277
  // Sale propensity is a 0-100 AVM score predicting likelihood to go on market and sell
@@ -513,7 +540,7 @@ function getSearchCriteriaFilterContext(searchCriteriaPath) {
513
540
  ]
514
541
  : []),
515
542
  ],
516
- filterGuidance: getFilterGuidance("QuickListValue"),
543
+ filterGuidance: getFilterGuidance("QuickListValue", false), // hasPossibleValues not used for QuickListValue
517
544
  };
518
545
  }
519
546
  const filterType = SEARCH_CRITERIA_FILTER_TYPE_MAP[searchCriteriaPath] || "StringFilter";
@@ -562,9 +589,14 @@ function getSearchCriteriaFilterContext(searchCriteriaPath) {
562
589
  const fieldName = parts[parts.length - 1] || searchCriteriaPath;
563
590
  description = `Filter on ${fieldName} field`;
564
591
  }
592
+ // Get possible values if available
593
+ const possibleValues = propertyMetadata
594
+ ? (0, metadata_1.getPossibleValues)(propertyMetadata)
595
+ : undefined;
596
+ const possibleValuesRef = propertyMetadata?.possibleValuesRef;
565
597
  // Get examples and guidance
566
598
  const examples = getExamples(searchCriteriaPath, filterType, propertyMetadata);
567
- const filterGuidance = getFilterGuidance(filterType);
599
+ const filterGuidance = getFilterGuidance(filterType, !!possibleValues && possibleValues.length > 0);
568
600
  return {
569
601
  searchCriteriaPath,
570
602
  filterType,
@@ -573,6 +605,8 @@ function getSearchCriteriaFilterContext(searchCriteriaPath) {
573
605
  description,
574
606
  examples,
575
607
  filterGuidance,
608
+ possibleValues,
609
+ possibleValuesRef,
576
610
  };
577
611
  }
578
612
  /**
@@ -595,6 +629,20 @@ function getSearchCriteriaFilterContextString(searchCriteriaPath) {
595
629
  if (context.propertyMetadata) {
596
630
  parts.push(`Property Field: ${context.propertyPath} (${context.propertyMetadata.dataType})`);
597
631
  }
632
+ // Add possible values information if available
633
+ if (context.possibleValues && context.possibleValues.length > 0) {
634
+ parts.push(`\nPossible Values:`);
635
+ if (context.possibleValuesRef) {
636
+ parts.push(` Reference: ${context.possibleValuesRef} (see POSSIBLE VALUES CONSTANTS section for complete list)`);
637
+ }
638
+ // Show first 3-5 values as sample for quick reference
639
+ const sampleSize = Math.min(5, context.possibleValues.length);
640
+ const sampleValues = context.possibleValues.slice(0, sampleSize);
641
+ parts.push(` Sample (${sampleSize} of ${context.possibleValues.length}): ${sampleValues.map((v) => `"${v}"`).join(", ")}${context.possibleValues.length > sampleSize
642
+ ? ` ... (see ${context.possibleValuesRef || "constants"} for all ${context.possibleValues.length} values)`
643
+ : ""}`);
644
+ parts.push(` Note: Use inList (match any) or notInList (exclude) for fields with predefined values`);
645
+ }
598
646
  parts.push(`\nFilter Guidance:\n${context.filterGuidance}`);
599
647
  if (context.examples.length > 0) {
600
648
  parts.push(`\nExamples:`);
@@ -615,6 +663,28 @@ function getQuickListValuesSection() {
615
663
  }
616
664
  return section;
617
665
  }
666
+ /**
667
+ * Get formatted Possible Values Constants section for documentation
668
+ * This defines all possible value constants once, which can then be referenced by fields
669
+ * @returns Formatted string with all possible value constants
670
+ */
671
+ function getPossibleValuesConstantsSection() {
672
+ let section = `POSSIBLE VALUES CONSTANTS:\n`;
673
+ section += `These constants define allowed values for fields with predefined value sets. Fields reference these constants by name.\n\n`;
674
+ const constants = Object.entries(metadata_1.POSSIBLE_VALUES_CONSTANTS)
675
+ .filter(([constantName]) => constantName !== "StateAbbreviation") // Exclude common knowledge constants
676
+ .sort((a, b) => a[0].localeCompare(b[0]));
677
+ for (const [constantName, values] of constants) {
678
+ const valueArray = values;
679
+ section += `${constantName} (${valueArray.length} values):\n`;
680
+ // Show ALL values - this is the authoritative definition, no truncation
681
+ valueArray.forEach((value) => {
682
+ section += ` - "${value}"\n`;
683
+ });
684
+ section += "\n";
685
+ }
686
+ return section;
687
+ }
618
688
  /**
619
689
  * Get filter context for all fields in a SearchCriteria domain/group
620
690
  * @param domainName The domain name (e.g., "address", "building", "assessment", "quickList")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@land-catalyst/batch-data-sdk",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
4
4
  "description": "TypeScript SDK for BatchData.io Property API - Types, Builders, and Utilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",