@land-catalyst/batch-data-sdk 1.2.6 → 1.2.9

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.
@@ -212,19 +212,73 @@ function buildSearchCriteriaSystemPrompt(domains, options) {
212
212
  ${domainDocs}
213
213
 
214
214
  FILTER TYPES:
215
- - StringFilter: { equals?, contains?, startsWith?, endsWith?, inList?, matches? }
215
+ - StringFilter: { equals?, contains?, startsWith?, endsWith?, inList?, notInList?, matches? }
216
+ * equals: Exact string match - use for precise value matching
217
+ * contains: Substring match - matches if the field value contains the specified string anywhere
218
+ * startsWith: Prefix match - matches if the field value starts with the specified string
219
+ * endsWith: Suffix match - matches if the field value ends with the specified string
220
+ * inList: Array of allowed values - matches if field value is in the list (PREFERRED for fields with predefined possible values)
221
+ * notInList: Array of excluded values - matches if field value is NOT in the list (PREFERRED for excluding predefined values)
222
+ * matches: Array of pattern matches - similar to inList but for pattern-based matching
223
+ * For fields with predefined possible values (shown in field documentation), prefer inList (match any) or notInList (exclude) over equals/contains
224
+ * Example: { inList: ["Central", "Window Unit"] } or { notInList: ["None"] } or { contains: "Street" }
225
+
216
226
  - 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" })
227
+ * min: Minimum value (inclusive) - matches if field value >= min
228
+ * max: Maximum value (inclusive) - matches if field value <= max
229
+ * Can use both min and max together for a range, or just one for one-sided filtering
230
+ * Example: { min: 3, max: 5 } for "3 to 5 bedrooms", { min: 500000 } for "at least $500k", { max: 1000000 } for "under $1M"
231
+
232
+ - DateRangeFilter: { minDate?, maxDate? }
233
+ * minDate: Minimum date (inclusive) - dates in ISO 8601 format YYYY-MM-DD
234
+ * maxDate: Maximum date (inclusive) - dates in ISO 8601 format YYYY-MM-DD
235
+ * For "after [year]" queries, use next year's January 1st: "after 2015" = { minDate: "2016-01-01" }
236
+ * For "before [year]" queries, use that year's December 31st: "before 2020" = { maxDate: "2020-12-31" }
237
+ * Example: { minDate: "2016-01-01" } for "built after 2015", { maxDate: "2020-12-31" } for "sold before 2021"
238
+
218
239
  - 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 }] }
240
+ * equals: Exact boolean match - must be true or false
241
+ * Use for fields that only have true/false values (e.g., ownerOccupied, hasChildren)
242
+ * Example: { equals: true } for "owner occupied properties", { equals: false } for "non-owner occupied"
243
+
244
+ - QuickListValue: string
245
+ * Single quickList value string - matches properties with that specific characteristic
246
+ * Can prefix with "not-" to exclude: "not-vacant" excludes vacant properties
247
+ * Valid values include: "vacant", "preforeclosure", "tax-default", "fix-and-flip", "absentee-owner", etc.
248
+ * See QUICKLIST VALUES section for complete list and descriptions
249
+ * Example: "vacant" or "not-active-listing"
250
+
251
+ - GeoLocationDistance: { geoPoint: { latitude, longitude }, distanceMiles?, distanceKilometers?, distanceMeters?, distanceFeet?, distanceYards? }
252
+ * geoPoint: Center point with latitude and longitude (both required)
253
+ * distanceMiles: Search radius in miles (one distance unit required)
254
+ * distanceKilometers: Search radius in kilometers (alternative to miles)
255
+ * distanceMeters: Search radius in meters (alternative to miles)
256
+ * distanceFeet: Search radius in feet (alternative to miles)
257
+ * distanceYards: Search radius in yards (alternative to miles)
258
+ * Matches properties within the specified distance of the center point
259
+ * Example: { geoPoint: { latitude: 33.4484, longitude: -112.0740 }, distanceMiles: "5" } for "within 5 miles of Phoenix"
260
+
261
+ - GeoLocationBoundingBox: { nwGeoPoint: { latitude, longitude }, seGeoPoint: { latitude, longitude } }
262
+ * nwGeoPoint: Northwest corner of the bounding box (top-left)
263
+ * seGeoPoint: Southeast corner of the bounding box (bottom-right)
264
+ * Matches properties within the rectangular area defined by these two points
265
+ * Useful for filtering by geographic regions or specific areas
266
+ * Example: { nwGeoPoint: { latitude: 33.5, longitude: -112.1 }, seGeoPoint: { latitude: 33.3, longitude: -112.0 } }
267
+
268
+ - GeoLocationPolygon: { geoPoints: [{ latitude, longitude }, ...] }
269
+ * geoPoints: Array of GeoPoint objects defining the polygon vertices
270
+ * Matches properties within the polygon area defined by connecting the points in order
271
+ * Requires at least 3 points to form a valid polygon
272
+ * Useful for irregularly shaped geographic areas
273
+ * 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
274
 
224
275
  IMPORTANT RULES:
225
276
  1. Always include a "query" field with geographic scope (state, county, city, or "US")
226
277
  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
278
+ 3. Use string filters appropriately:
279
+ - For fields with predefined possible values (shown in field documentation): Use inList (match any) or notInList (exclude) instead of equals/contains
280
+ - For free-text fields: Use equals (exact match), contains (substring), startsWith, endsWith, or inList
281
+ - Example: building.airConditioningSource has predefined values ["Central", "Window Unit", "Wall", ...] - use { inList: ["Central", "Window Unit"] } instead of { equals: "Central" }
228
282
  4. Be specific and realistic:
229
283
  - "3 bedrooms" = building.bedroomCount: {min: 3, max: 3}
230
284
  - "at least 3 bedrooms" = building.bedroomCount: {min: 3}
@@ -283,6 +337,55 @@ Example response format:
283
337
  If existing criteria is provided, merge intelligently - update fields mentioned in the prompt, preserve others.
284
338
  Return only valid JSON, no markdown formatting or code blocks.`;
285
339
  }
340
+ /**
341
+ * Consolidate repetitive openLien loan fields into groups
342
+ * Groups fields like firstLoanType, secondLoanType, thirdLoanType, fourthLoanType together
343
+ */
344
+ function consolidateOpenLienFields(fields) {
345
+ const result = [];
346
+ const processed = new Set();
347
+ // Patterns to consolidate: [first|second|third|fourth]Loan[Type|InterestRate]
348
+ const loanTypePattern = /^(first|second|third|fourth)LoanType$/;
349
+ const loanInterestRatePattern = /^(first|second|third|fourth)LoanInterestRate$/;
350
+ // Group loan type fields
351
+ const loanTypeFields = fields.filter((f) => loanTypePattern.test(f.name));
352
+ if (loanTypeFields.length >= 2) {
353
+ // All should have same type and similar descriptions
354
+ const firstField = loanTypeFields[0];
355
+ const allSameType = loanTypeFields.every((f) => f.type === firstField.type);
356
+ if (allSameType) {
357
+ result.push({
358
+ type: "grouped",
359
+ pattern: firstField.description.replace(/^(First|Second|Third|Fourth)\s+/i, "[First/Second/Third/Fourth] "),
360
+ fieldNames: loanTypeFields.map((f) => f.name).sort(),
361
+ templateField: firstField,
362
+ });
363
+ loanTypeFields.forEach((f) => processed.add(f.name));
364
+ }
365
+ }
366
+ // Group loan interest rate fields
367
+ const loanInterestRateFields = fields.filter((f) => loanInterestRatePattern.test(f.name));
368
+ if (loanInterestRateFields.length >= 2) {
369
+ const firstField = loanInterestRateFields[0];
370
+ const allSameType = loanInterestRateFields.every((f) => f.type === firstField.type);
371
+ if (allSameType) {
372
+ result.push({
373
+ type: "grouped",
374
+ pattern: firstField.description.replace(/^(first|second|third|fourth)/i, "[first/second/third/fourth]") + " (applies to all listed fields)",
375
+ fieldNames: loanInterestRateFields.map((f) => f.name).sort(),
376
+ templateField: firstField,
377
+ });
378
+ loanInterestRateFields.forEach((f) => processed.add(f.name));
379
+ }
380
+ }
381
+ // Add remaining unprocessed fields
382
+ for (const field of fields) {
383
+ if (!processed.has(field.name)) {
384
+ result.push(field);
385
+ }
386
+ }
387
+ return result;
388
+ }
286
389
  /**
287
390
  * Get context documentation for a specific domain
288
391
  * @param domainName The domain name (e.g., "address", "building", "assessment")
@@ -327,6 +430,15 @@ function getDomainContext(domainName, options = {}) {
327
430
  doc += "\n Available fields:\n";
328
431
  for (const field of fields) {
329
432
  doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
433
+ // Add possible values information if available
434
+ const fieldContext = (0, search_criteria_filter_context_1.getSearchCriteriaFilterContext)(field.name);
435
+ if (fieldContext?.possibleValues &&
436
+ fieldContext.possibleValues.length > 0) {
437
+ doc += ` Possible Values (${fieldContext.possibleValues.length} total): `;
438
+ // Show all possible values inline
439
+ doc += fieldContext.possibleValues.map((v) => `"${v}"`).join(", ");
440
+ doc += `\n`;
441
+ }
330
442
  // Removed filterGuidance - filter types are already defined in FILTER TYPES section
331
443
  if (field.examples && field.examples.length > 0) {
332
444
  doc += ` Examples: ${field.examples.join(", ")}\n`;
@@ -392,6 +504,8 @@ function buildDomainDocumentation(domainNames, options = {}) {
392
504
  else if (optionalDomains.length > 0) {
393
505
  doc += "OPTIONAL DOMAINS:\n";
394
506
  }
507
+ // Note: Possible values are shown inline with each field for better context
508
+ // We no longer need a separate constants section since values appear with their fields
395
509
  // Add QuickList values section once if quickList domain is included
396
510
  if (hasQuickListDomain) {
397
511
  doc += "\n";
@@ -405,6 +519,15 @@ function buildDomainDocumentation(domainNames, options = {}) {
405
519
  doc += "\n Available fields:\n";
406
520
  for (const field of quickListDomain.fields) {
407
521
  doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
522
+ // Add possible values information if available
523
+ const fieldContext = (0, search_criteria_filter_context_1.getSearchCriteriaFilterContext)(field.name);
524
+ if (fieldContext?.possibleValues &&
525
+ fieldContext.possibleValues.length > 0) {
526
+ doc += ` Possible Values (${fieldContext.possibleValues.length} total): `;
527
+ // Show all possible values inline
528
+ doc += fieldContext.possibleValues.map((v) => `"${v}"`).join(", ");
529
+ doc += `\n`;
530
+ }
408
531
  // Removed filterGuidance - filter types are already defined in FILTER TYPES section
409
532
  if (field.examples && field.examples.length > 0) {
410
533
  doc += ` Examples: ${field.examples.join(", ")}\n`;
@@ -417,11 +540,69 @@ function buildDomainDocumentation(domainNames, options = {}) {
417
540
  doc += `\n- ${domain.name}: ${domain.description}`;
418
541
  if (domain.fields.length > 0) {
419
542
  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`;
543
+ // Special handling for openLien domain to consolidate repetitive loan fields
544
+ if (domain.name === "openLien") {
545
+ const consolidatedFields = consolidateOpenLienFields(domain.fields);
546
+ for (const fieldOrGroup of consolidatedFields) {
547
+ if (fieldOrGroup.type === "grouped") {
548
+ // Handle grouped fields (e.g., firstLoanType, secondLoanType, etc.)
549
+ const group = fieldOrGroup;
550
+ doc += ` - ${group.fieldNames.join(", ")} (${group.templateField.type}): ${group.pattern}\n`;
551
+ const templateContext = (0, search_criteria_filter_context_1.getDomainFilterContexts)(domain.name).find((ctx) => ctx.searchCriteriaPath.split(".").pop() ===
552
+ group.templateField.name ||
553
+ ctx.searchCriteriaPath === group.templateField.name);
554
+ if (templateContext?.possibleValues &&
555
+ templateContext.possibleValues.length > 0) {
556
+ doc += ` Possible Values (${templateContext.possibleValues.length} total): `;
557
+ // Show all possible values inline
558
+ doc +=
559
+ templateContext.possibleValues.map((v) => `"${v}"`).join(", ") +
560
+ `\n`;
561
+ }
562
+ if (group.templateField.examples &&
563
+ group.templateField.examples.length > 0) {
564
+ doc += ` Examples: ${group.templateField.examples.join(", ")}\n`;
565
+ }
566
+ }
567
+ else {
568
+ // Handle individual fields
569
+ const field = fieldOrGroup;
570
+ doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
571
+ const fieldContext = (0, search_criteria_filter_context_1.getDomainFilterContexts)(domain.name).find((ctx) => ctx.searchCriteriaPath.split(".").pop() === field.name ||
572
+ ctx.searchCriteriaPath === field.name);
573
+ if (fieldContext?.possibleValues &&
574
+ fieldContext.possibleValues.length > 0) {
575
+ doc += ` Possible Values (${fieldContext.possibleValues.length} total): `;
576
+ // Show all possible values inline
577
+ doc +=
578
+ fieldContext.possibleValues.map((v) => `"${v}"`).join(", ") +
579
+ `\n`;
580
+ }
581
+ if (field.examples && field.examples.length > 0) {
582
+ doc += ` Examples: ${field.examples.join(", ")}\n`;
583
+ }
584
+ }
585
+ }
586
+ }
587
+ else {
588
+ // Standard output for other domains
589
+ for (const field of domain.fields) {
590
+ doc += ` - ${field.name} (${field.type}): ${field.description}\n`;
591
+ // Add possible values information if available
592
+ const fieldContext = (0, search_criteria_filter_context_1.getDomainFilterContexts)(domain.name).find((ctx) => ctx.searchCriteriaPath.split(".").pop() === field.name ||
593
+ ctx.searchCriteriaPath === field.name);
594
+ if (fieldContext?.possibleValues &&
595
+ fieldContext.possibleValues.length > 0) {
596
+ doc += ` Possible Values (${fieldContext.possibleValues.length} total): `;
597
+ // Show all possible values inline
598
+ doc +=
599
+ fieldContext.possibleValues.map((v) => `"${v}"`).join(", ") +
600
+ `\n`;
601
+ }
602
+ // Removed filterGuidance - filter types are already defined in FILTER TYPES section
603
+ if (field.examples && field.examples.length > 0) {
604
+ doc += ` Examples: ${field.examples.join(", ")}\n`;
605
+ }
425
606
  }
426
607
  }
427
608
  }
@@ -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.6",
3
+ "version": "1.2.9",
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",
@@ -21,6 +21,7 @@
21
21
  "generate:metadata": "node scripts/generate-property-field-metadata.js",
22
22
  "generate:docs": "npx tsx scripts/generate-property-type-docs.ts",
23
23
  "add:jsdoc": "node scripts/add-jsdoc-to-types.js",
24
+ "preversion": "npm run build",
24
25
  "v:patch": "npm version patch && git push --follow-tags",
25
26
  "v:minor": "npm version minor && git push --follow-tags",
26
27
  "v:major": "npm version major && git push --follow-tags"