@purveyors/cli 0.24.0 → 0.27.0
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/README.md +87 -19
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +11 -47
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/inventory.d.ts.map +1 -1
- package/dist/commands/inventory.js +17 -44
- package/dist/commands/inventory.js.map +1 -1
- package/dist/commands/market.d.ts +8 -0
- package/dist/commands/market.d.ts.map +1 -0
- package/dist/commands/market.js +190 -0
- package/dist/commands/market.js.map +1 -0
- package/dist/commands/roast.d.ts +3 -0
- package/dist/commands/roast.d.ts.map +1 -1
- package/dist/commands/roast.js +15 -11
- package/dist/commands/roast.js.map +1 -1
- package/dist/commands/sales.d.ts.map +1 -1
- package/dist/commands/sales.js +15 -11
- package/dist/commands/sales.js.map +1 -1
- package/dist/commands/tasting.d.ts.map +1 -1
- package/dist/commands/tasting.js +1 -2
- package/dist/commands/tasting.js.map +1 -1
- package/dist/lib/auth-guard.d.ts.map +1 -1
- package/dist/lib/auth-guard.js +14 -6
- package/dist/lib/auth-guard.js.map +1 -1
- package/dist/lib/catalog.d.ts +12 -14
- package/dist/lib/catalog.d.ts.map +1 -1
- package/dist/lib/catalog.js +177 -520
- package/dist/lib/catalog.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/interactive/forms.d.ts +1 -1
- package/dist/lib/interactive/forms.d.ts.map +1 -1
- package/dist/lib/interactive/forms.js +3 -10
- package/dist/lib/interactive/forms.js.map +1 -1
- package/dist/lib/inventory.d.ts +8 -16
- package/dist/lib/inventory.d.ts.map +1 -1
- package/dist/lib/inventory.js +44 -227
- package/dist/lib/inventory.js.map +1 -1
- package/dist/lib/manifest.d.ts.map +1 -1
- package/dist/lib/manifest.js +91 -16
- package/dist/lib/manifest.js.map +1 -1
- package/dist/lib/market.d.ts +239 -0
- package/dist/lib/market.d.ts.map +1 -0
- package/dist/lib/market.js +29 -0
- package/dist/lib/market.js.map +1 -0
- package/dist/lib/parchment-base.d.ts +3 -0
- package/dist/lib/parchment-base.d.ts.map +1 -0
- package/dist/lib/parchment-base.js +9 -0
- package/dist/lib/parchment-base.js.map +1 -0
- package/dist/lib/parchment.d.ts +10 -1
- package/dist/lib/parchment.d.ts.map +1 -1
- package/dist/lib/parchment.js +18 -7
- package/dist/lib/parchment.js.map +1 -1
- package/dist/lib/roast.d.ts +16 -24
- package/dist/lib/roast.d.ts.map +1 -1
- package/dist/lib/roast.js +33 -224
- package/dist/lib/roast.js.map +1 -1
- package/dist/lib/sales.d.ts +12 -6
- package/dist/lib/sales.d.ts.map +1 -1
- package/dist/lib/sales.js +13 -27
- package/dist/lib/sales.js.map +1 -1
- package/dist/lib/supabase.d.ts.map +1 -1
- package/dist/lib/supabase.js +14 -9
- package/dist/lib/supabase.js.map +1 -1
- package/dist/lib/tasting.d.ts +1 -1
- package/dist/lib/tasting.d.ts.map +1 -1
- package/dist/lib/tasting.js +5 -53
- package/dist/lib/tasting.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +6 -1
- package/dist/program.js.map +1 -1
- package/package.json +3 -2
package/dist/lib/catalog.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { AuthError, PrvrsError } from './errors.js';
|
|
3
|
+
import { createParchmentClient, getParchmentBaseUrl, resolveParchmentToken, unwrapParchment, } from './parchment.js';
|
|
3
4
|
// ─── Zod schemas ──────────────────────────────────────────────────────────────
|
|
4
5
|
export const catalogSortFields = ['price', 'price-desc', 'name', 'origin', 'newest'];
|
|
5
6
|
export const searchCatalogSchema = z.object({
|
|
@@ -32,7 +33,6 @@ export const getCatalogSchema = z.object({
|
|
|
32
33
|
export const getCatalogStatsSchema = z.object({});
|
|
33
34
|
const CATALOG_INTELLIGENCE_MAX_SAMPLE_SIZE = 5000;
|
|
34
35
|
const CATALOG_PREMIUM_DEFAULT_SAMPLE_SIZE = 250;
|
|
35
|
-
const SUPABASE_DATA_API_MAX_PAGE_SIZE = 1000;
|
|
36
36
|
const SUPPLIER_AGGREGATE_DEFAULT_SAMPLE_SIZE = CATALOG_INTELLIGENCE_MAX_SAMPLE_SIZE;
|
|
37
37
|
export const catalogRankPremiumSchema = z.object({
|
|
38
38
|
origin: z.string().optional(),
|
|
@@ -80,13 +80,6 @@ export const catalogFacetsSchema = z.object({
|
|
|
80
80
|
field: z.enum(catalogFacetFields),
|
|
81
81
|
stockedOnly: z.boolean().default(true).optional(),
|
|
82
82
|
limit: z.number().int().min(1).max(100).default(60).optional(),
|
|
83
|
-
sampleSize: z
|
|
84
|
-
.number()
|
|
85
|
-
.int()
|
|
86
|
-
.min(1)
|
|
87
|
-
.max(CATALOG_INTELLIGENCE_MAX_SAMPLE_SIZE)
|
|
88
|
-
.default(SUPPLIER_AGGREGATE_DEFAULT_SAMPLE_SIZE)
|
|
89
|
-
.optional(),
|
|
90
83
|
});
|
|
91
84
|
export const catalogRankObjectives = ['premium', 'value', 'fresh_arrival', 'rare_origin'];
|
|
92
85
|
export const catalogRankSchema = z.object({
|
|
@@ -108,6 +101,9 @@ export const catalogRankSchema = z.object({
|
|
|
108
101
|
.optional(),
|
|
109
102
|
});
|
|
110
103
|
export const catalogSimilarityModes = ['all', 'likely_same', 'similar_profile'];
|
|
104
|
+
const CATALOG_SIMILARITY_MIN_THRESHOLD = 0.5;
|
|
105
|
+
const CATALOG_SIMILARITY_MAX_THRESHOLD = 0.99;
|
|
106
|
+
const CATALOG_SIMILARITY_MAX_RESULTS = 25;
|
|
111
107
|
export const getCatalogSimilaritySchema = z.object({
|
|
112
108
|
coffee_id: z
|
|
113
109
|
.number()
|
|
@@ -116,8 +112,8 @@ export const getCatalogSimilaritySchema = z.object({
|
|
|
116
112
|
.describe('The coffee_catalog ID to request canonical similarity for'),
|
|
117
113
|
threshold: z
|
|
118
114
|
.number()
|
|
119
|
-
.min(
|
|
120
|
-
.max(
|
|
115
|
+
.min(CATALOG_SIMILARITY_MIN_THRESHOLD)
|
|
116
|
+
.max(CATALOG_SIMILARITY_MAX_THRESHOLD)
|
|
121
117
|
.default(0.7)
|
|
122
118
|
.optional()
|
|
123
119
|
.describe('Minimum canonical similarity threshold (0.5-0.99)'),
|
|
@@ -125,7 +121,7 @@ export const getCatalogSimilaritySchema = z.object({
|
|
|
125
121
|
.number()
|
|
126
122
|
.int()
|
|
127
123
|
.min(1)
|
|
128
|
-
.max(
|
|
124
|
+
.max(CATALOG_SIMILARITY_MAX_RESULTS)
|
|
129
125
|
.default(10)
|
|
130
126
|
.optional()
|
|
131
127
|
.describe('Maximum canonical similarity results to request'),
|
|
@@ -144,19 +140,19 @@ export const findSimilarBeansSchema = z.object({
|
|
|
144
140
|
.describe('The coffee_catalog ID to find similar beans for'),
|
|
145
141
|
threshold: z
|
|
146
142
|
.number()
|
|
147
|
-
.min(
|
|
148
|
-
.max(
|
|
143
|
+
.min(CATALOG_SIMILARITY_MIN_THRESHOLD)
|
|
144
|
+
.max(CATALOG_SIMILARITY_MAX_THRESHOLD)
|
|
149
145
|
.default(0.7)
|
|
150
146
|
.optional()
|
|
151
|
-
.describe('Minimum similarity
|
|
147
|
+
.describe('Minimum canonical similarity threshold (0.5-0.99)'),
|
|
152
148
|
limit: z
|
|
153
149
|
.number()
|
|
154
150
|
.int()
|
|
155
151
|
.min(1)
|
|
156
|
-
.max(
|
|
152
|
+
.max(CATALOG_SIMILARITY_MAX_RESULTS)
|
|
157
153
|
.default(10)
|
|
158
154
|
.optional()
|
|
159
|
-
.describe('Maximum results to return'),
|
|
155
|
+
.describe('Maximum canonical similarity results to return'),
|
|
160
156
|
});
|
|
161
157
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
162
158
|
/**
|
|
@@ -369,22 +365,8 @@ const CATALOG_API_SORT_MAP = {
|
|
|
369
365
|
'price-desc': { field: 'price_per_lb', direction: 'desc' },
|
|
370
366
|
name: { field: 'name', direction: 'asc' },
|
|
371
367
|
origin: { field: 'country', direction: 'asc' },
|
|
372
|
-
newest: { field: '
|
|
368
|
+
newest: { field: 'last_updated', direction: 'desc' },
|
|
373
369
|
};
|
|
374
|
-
function getCatalogApiBaseUrl() {
|
|
375
|
-
return (process.env.PURVEYORS_BASE_URL ?? 'https://www.purveyors.io').replace(/\/+$/, '');
|
|
376
|
-
}
|
|
377
|
-
async function getCatalogApiAuthHeader(supabase) {
|
|
378
|
-
const apiKey = process.env.PARCHMENT_API_KEY ?? process.env.PURVEYORS_API_KEY;
|
|
379
|
-
if (apiKey) {
|
|
380
|
-
return `Bearer ${apiKey}`;
|
|
381
|
-
}
|
|
382
|
-
const { data: { session }, } = await supabase.auth.getSession();
|
|
383
|
-
if (!session?.access_token) {
|
|
384
|
-
throw new AuthError('Catalog API reads require a Purveyors session or API key. Run `purvey auth login`, or set PARCHMENT_API_KEY/PURVEYORS_API_KEY for API-backed catalog reads.');
|
|
385
|
-
}
|
|
386
|
-
return `Bearer ${session.access_token}`;
|
|
387
|
-
}
|
|
388
370
|
function appendSearchParam(params, name, value) {
|
|
389
371
|
if (value === undefined)
|
|
390
372
|
return;
|
|
@@ -415,7 +397,7 @@ function assertCatalogApiCompatibleSearch(parsed) {
|
|
|
415
397
|
}
|
|
416
398
|
function buildCatalogApiUrl(parsed) {
|
|
417
399
|
assertCatalogApiCompatibleSearch(parsed);
|
|
418
|
-
const url = new URL('/v1/catalog',
|
|
400
|
+
const url = new URL('/v1/catalog', getParchmentBaseUrl());
|
|
419
401
|
const params = url.searchParams;
|
|
420
402
|
params.set('include', 'proof');
|
|
421
403
|
params.set('stocked', parsed.stocked ? 'true' : 'all');
|
|
@@ -481,7 +463,7 @@ async function parseCatalogApiError(response, context = 'include=proof') {
|
|
|
481
463
|
return new PrvrsError('GENERAL_ERROR', `Catalog API request failed (${response.status}): ${serverMessage}`, details);
|
|
482
464
|
}
|
|
483
465
|
function buildCatalogSimilarityApiUrl(parsed) {
|
|
484
|
-
const url = new URL(`/v1/catalog/${parsed.coffee_id}/similar`,
|
|
466
|
+
const url = new URL(`/v1/catalog/${parsed.coffee_id}/similar`, getParchmentBaseUrl());
|
|
485
467
|
const params = url.searchParams;
|
|
486
468
|
params.set('threshold', String(parsed.threshold ?? 0.7));
|
|
487
469
|
params.set('limit', String(parsed.limit ?? 10));
|
|
@@ -508,12 +490,12 @@ function isCatalogSimilarityResponse(value) {
|
|
|
508
490
|
typeof meta.classification_version === 'string' &&
|
|
509
491
|
typeof meta.query_strategy === 'string');
|
|
510
492
|
}
|
|
511
|
-
async function fetchCatalogSimilarityApi(
|
|
493
|
+
async function fetchCatalogSimilarityApi(parsed) {
|
|
512
494
|
const url = buildCatalogSimilarityApiUrl(parsed);
|
|
513
495
|
const response = await fetch(url, {
|
|
514
496
|
headers: {
|
|
515
497
|
Accept: 'application/json',
|
|
516
|
-
Authorization: await
|
|
498
|
+
Authorization: `Bearer ${await resolveParchmentToken('member')}`,
|
|
517
499
|
},
|
|
518
500
|
});
|
|
519
501
|
if (!response.ok) {
|
|
@@ -525,12 +507,18 @@ async function fetchCatalogSimilarityApi(supabase, parsed) {
|
|
|
525
507
|
}
|
|
526
508
|
return envelope;
|
|
527
509
|
}
|
|
528
|
-
async function fetchCatalogApiItems(
|
|
510
|
+
async function fetchCatalogApiItems(parsed) {
|
|
529
511
|
const url = buildCatalogApiUrl(parsed);
|
|
530
512
|
const response = await fetch(url, {
|
|
531
513
|
headers: {
|
|
532
514
|
Accept: 'application/json',
|
|
533
|
-
Authorization: await
|
|
515
|
+
Authorization: `Bearer ${await resolveParchmentToken(parsed.processingBaseMethod ||
|
|
516
|
+
parsed.fermentationType ||
|
|
517
|
+
parsed.processAdditive ||
|
|
518
|
+
parsed.processingDisclosureLevel ||
|
|
519
|
+
parsed.processingConfidenceMin !== undefined
|
|
520
|
+
? 'member'
|
|
521
|
+
: 'viewer')}`,
|
|
534
522
|
},
|
|
535
523
|
});
|
|
536
524
|
if (!response.ok) {
|
|
@@ -567,121 +555,59 @@ export function computeCatalogStats(items) {
|
|
|
567
555
|
/**
|
|
568
556
|
* Search the coffee catalog with optional filters.
|
|
569
557
|
*/
|
|
570
|
-
export async function searchCatalog(
|
|
558
|
+
export async function searchCatalog(opts) {
|
|
571
559
|
const parsed = searchCatalogSchema.parse(opts);
|
|
572
560
|
if (parsed.includeProof) {
|
|
573
|
-
return fetchCatalogApiItems(
|
|
574
|
-
}
|
|
575
|
-
let query = supabase.from('coffee_catalog').select('*');
|
|
576
|
-
if (parsed.origin) {
|
|
577
|
-
const o = sanitizeFilterValue(parsed.origin);
|
|
578
|
-
query = query.or(`country.ilike.%${o}%,continent.ilike.%${o}%,region.ilike.%${o}%`);
|
|
579
|
-
}
|
|
580
|
-
if (parsed.process) {
|
|
581
|
-
const p = sanitizeFilterValue(parsed.process);
|
|
582
|
-
query = query.ilike('processing', `%${p}%`);
|
|
583
|
-
}
|
|
584
|
-
if (parsed.processingBaseMethod) {
|
|
585
|
-
query = query.eq('processing_base_method', sanitizeFilterValue(parsed.processingBaseMethod));
|
|
586
|
-
}
|
|
587
|
-
if (parsed.fermentationType) {
|
|
588
|
-
query = query.eq('fermentation_type', sanitizeFilterValue(parsed.fermentationType));
|
|
589
|
-
}
|
|
590
|
-
if (parsed.processAdditive) {
|
|
591
|
-
query = query.contains('process_additives', [sanitizeFilterValue(parsed.processAdditive)]);
|
|
592
|
-
}
|
|
593
|
-
if (parsed.processingDisclosureLevel) {
|
|
594
|
-
query = query.eq('processing_disclosure_level', sanitizeFilterValue(parsed.processingDisclosureLevel));
|
|
595
|
-
}
|
|
596
|
-
if (parsed.processingConfidenceMin !== undefined) {
|
|
597
|
-
query = query.gte('processing_confidence', parsed.processingConfidenceMin);
|
|
598
|
-
}
|
|
599
|
-
if (parsed.priceMin !== undefined) {
|
|
600
|
-
query = query.gte('price_per_lb', parsed.priceMin);
|
|
601
|
-
}
|
|
602
|
-
if (parsed.priceMax !== undefined) {
|
|
603
|
-
query = query.lte('price_per_lb', parsed.priceMax);
|
|
604
|
-
}
|
|
605
|
-
if (parsed.flavor) {
|
|
606
|
-
const keywords = parsed.flavor
|
|
607
|
-
.split(',')
|
|
608
|
-
.map((k) => sanitizeFilterValue(k.trim()))
|
|
609
|
-
.filter(Boolean);
|
|
610
|
-
const flavorFilters = keywords
|
|
611
|
-
.flatMap((kw) => [
|
|
612
|
-
`description_short.ilike.%${kw}%`,
|
|
613
|
-
`description_long.ilike.%${kw}%`,
|
|
614
|
-
`cupping_notes.ilike.%${kw}%`,
|
|
615
|
-
`farm_notes.ilike.%${kw}%`,
|
|
616
|
-
])
|
|
617
|
-
.join(',');
|
|
618
|
-
query = query.or(flavorFilters);
|
|
619
|
-
}
|
|
620
|
-
if (parsed.name) {
|
|
621
|
-
const n = sanitizeFilterValue(parsed.name);
|
|
622
|
-
query = query.ilike('name', `%${n}%`);
|
|
623
|
-
}
|
|
624
|
-
if (parsed.supplier) {
|
|
625
|
-
const s = sanitizeFilterValue(parsed.supplier);
|
|
626
|
-
query = query.ilike('source', `%${s}%`);
|
|
627
|
-
}
|
|
628
|
-
if (parsed.ids && parsed.ids.length > 0) {
|
|
629
|
-
// coffee_catalog PK is `id`; `catalog_id` is the FK name on other tables
|
|
630
|
-
query = query.in('id', parsed.ids);
|
|
631
|
-
}
|
|
632
|
-
if (parsed.variety) {
|
|
633
|
-
const v = sanitizeFilterValue(parsed.variety);
|
|
634
|
-
query = query.ilike('cultivar_detail', `%${v}%`);
|
|
635
|
-
}
|
|
636
|
-
if (parsed.dryingMethod) {
|
|
637
|
-
const d = sanitizeFilterValue(parsed.dryingMethod);
|
|
638
|
-
query = query.ilike('drying_method', `%${d}%`);
|
|
639
|
-
}
|
|
640
|
-
if (parsed.stockedDays) {
|
|
641
|
-
const cutoff = new Date();
|
|
642
|
-
cutoff.setDate(cutoff.getDate() - parsed.stockedDays);
|
|
643
|
-
query = query.gte('stocked_date', cutoff.toISOString().slice(0, 10));
|
|
561
|
+
return fetchCatalogApiItems(parsed);
|
|
644
562
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
// Apply sort order
|
|
649
|
-
if (parsed.sort) {
|
|
650
|
-
switch (parsed.sort) {
|
|
651
|
-
case 'price':
|
|
652
|
-
query = query.order('price_per_lb', { ascending: true, nullsFirst: false });
|
|
653
|
-
break;
|
|
654
|
-
case 'price-desc':
|
|
655
|
-
query = query.order('price_per_lb', { ascending: false, nullsFirst: false });
|
|
656
|
-
break;
|
|
657
|
-
case 'name':
|
|
658
|
-
query = query.order('name', { ascending: true, nullsFirst: false });
|
|
659
|
-
break;
|
|
660
|
-
case 'origin':
|
|
661
|
-
query = query.order('country', { ascending: true, nullsFirst: false });
|
|
662
|
-
break;
|
|
663
|
-
case 'newest':
|
|
664
|
-
query = query.order('last_updated', { ascending: false, nullsFirst: false });
|
|
665
|
-
break;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
// Apply offset/limit for pagination (skip when fetching specific IDs)
|
|
669
|
-
if (!parsed.ids || parsed.ids.length === 0) {
|
|
670
|
-
const offset = parsed.offset ?? 0;
|
|
671
|
-
query = query.range(offset, offset + parsed.limit - 1);
|
|
563
|
+
const offset = parsed.offset ?? 0;
|
|
564
|
+
if (!hasCatalogIdFilter(parsed) && offset % parsed.limit !== 0) {
|
|
565
|
+
throw new PrvrsError('INVALID_ARGUMENT', `Canonical catalog pagination requires --offset to be a multiple of --limit. Received offset=${offset}, limit=${parsed.limit}.`);
|
|
672
566
|
}
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
567
|
+
const sort = parsed.sort ? CATALOG_API_SORT_MAP[parsed.sort] : undefined;
|
|
568
|
+
const client = await createParchmentClient(parsed.processingBaseMethod ||
|
|
569
|
+
parsed.fermentationType ||
|
|
570
|
+
parsed.processAdditive ||
|
|
571
|
+
parsed.processingDisclosureLevel ||
|
|
572
|
+
parsed.processingConfidenceMin !== undefined
|
|
573
|
+
? 'member'
|
|
574
|
+
: 'viewer');
|
|
575
|
+
const result = await client.catalog.list({
|
|
576
|
+
origin: parsed.origin,
|
|
577
|
+
processing: parsed.process,
|
|
578
|
+
processing_base_method: parsed.processingBaseMethod,
|
|
579
|
+
fermentation_type: parsed.fermentationType,
|
|
580
|
+
process_additive: parsed.processAdditive,
|
|
581
|
+
processing_disclosure_level: parsed.processingDisclosureLevel,
|
|
582
|
+
processing_confidence_min: parsed.processingConfidenceMin,
|
|
583
|
+
pricePerLbMin: parsed.priceMin,
|
|
584
|
+
pricePerLbMax: parsed.priceMax,
|
|
585
|
+
flavorKeywords: parsed.flavor
|
|
586
|
+
?.split(',')
|
|
587
|
+
.map((value) => value.trim())
|
|
588
|
+
.filter(Boolean),
|
|
589
|
+
name: parsed.name,
|
|
590
|
+
supplier: parsed.supplier,
|
|
591
|
+
coffeeIds: parsed.ids?.join(','),
|
|
592
|
+
variety: parsed.variety,
|
|
593
|
+
dryingMethod: parsed.dryingMethod,
|
|
594
|
+
stockedDays: parsed.stockedDays,
|
|
595
|
+
stocked: parsed.stocked ? 'true' : 'all',
|
|
596
|
+
sort: sort?.field,
|
|
597
|
+
order: sort?.direction,
|
|
598
|
+
limit: hasCatalogIdFilter(parsed) ? Math.min(parsed.ids?.length ?? 1, 100) : parsed.limit,
|
|
599
|
+
page: hasCatalogIdFilter(parsed) ? 1 : Math.floor(offset / parsed.limit) + 1,
|
|
600
|
+
});
|
|
601
|
+
const envelope = unwrapParchment(result, 'Catalog search');
|
|
602
|
+
return envelope.data;
|
|
677
603
|
}
|
|
678
604
|
/**
|
|
679
605
|
* Fetch a single catalog item by ID.
|
|
680
606
|
*/
|
|
681
|
-
export async function getCatalog(
|
|
607
|
+
export async function getCatalog(id, opts = {}) {
|
|
682
608
|
const parsed = getCatalogSchema.parse({ id, includeProof: opts.includeProof });
|
|
683
609
|
if (parsed.includeProof) {
|
|
684
|
-
const rows = await fetchCatalogApiItems(
|
|
610
|
+
const rows = await fetchCatalogApiItems({
|
|
685
611
|
ids: [parsed.id],
|
|
686
612
|
limit: 1,
|
|
687
613
|
includeProof: true,
|
|
@@ -692,120 +618,21 @@ export async function getCatalog(supabase, id, opts = {}) {
|
|
|
692
618
|
}
|
|
693
619
|
return item;
|
|
694
620
|
}
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
621
|
+
const rows = await searchCatalog({ ids: [parsed.id], limit: 1 });
|
|
622
|
+
const item = rows[0];
|
|
623
|
+
if (!item) {
|
|
624
|
+
throw new PrvrsError('NOT_FOUND', `Coffee ID ${parsed.id} not found in catalog.`);
|
|
625
|
+
}
|
|
626
|
+
return item;
|
|
699
627
|
}
|
|
700
628
|
/**
|
|
701
629
|
* Fetch aggregate statistics for the coffee catalog.
|
|
702
630
|
*/
|
|
703
|
-
export async function getCatalogStats(
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (error)
|
|
708
|
-
throw error;
|
|
709
|
-
return computeCatalogStats((data ?? []));
|
|
631
|
+
export async function getCatalogStats() {
|
|
632
|
+
const client = await createParchmentClient('viewer');
|
|
633
|
+
const envelope = unwrapParchment(await client.catalog.stats({ stocked: 'all' }), 'Catalog stats');
|
|
634
|
+
return envelope.stats;
|
|
710
635
|
}
|
|
711
|
-
function buildCatalogIntelligenceQuery(supabase, parsed, options = {}) {
|
|
712
|
-
let query = supabase.from('coffee_catalog').select('*');
|
|
713
|
-
if (parsed.origin) {
|
|
714
|
-
const origin = sanitizeFilterValue(parsed.origin);
|
|
715
|
-
query = query.or(`country.ilike.%${origin}%,continent.ilike.%${origin}%,region.ilike.%${origin}%`);
|
|
716
|
-
}
|
|
717
|
-
if (parsed.process) {
|
|
718
|
-
const process = sanitizeFilterValue(parsed.process);
|
|
719
|
-
query = query.or(`processing.ilike.%${process}%,processing_base_method.ilike.%${process}%`);
|
|
720
|
-
}
|
|
721
|
-
if (parsed.supplier) {
|
|
722
|
-
query = query.ilike('source', `%${sanitizeFilterValue(parsed.supplier)}%`);
|
|
723
|
-
}
|
|
724
|
-
if (parsed.country) {
|
|
725
|
-
query = query.ilike('country', `%${sanitizeFilterValue(parsed.country)}%`);
|
|
726
|
-
}
|
|
727
|
-
if (parsed.stocked !== undefined) {
|
|
728
|
-
query = query.eq('stocked', parsed.stocked);
|
|
729
|
-
}
|
|
730
|
-
if (parsed.priceMax !== undefined) {
|
|
731
|
-
query = query.lte('price_per_lb', parsed.priceMax);
|
|
732
|
-
}
|
|
733
|
-
if (parsed.minScore !== undefined) {
|
|
734
|
-
query = query.gte('purveyor_score', parsed.minScore);
|
|
735
|
-
}
|
|
736
|
-
if (parsed.nonWholesaleOnly) {
|
|
737
|
-
query = query.or('wholesale.is.null,wholesale.eq.false');
|
|
738
|
-
}
|
|
739
|
-
if (parsed.orderByScore) {
|
|
740
|
-
query = query.order('purveyor_score', { ascending: false, nullsFirst: false });
|
|
741
|
-
}
|
|
742
|
-
else {
|
|
743
|
-
query = query.order('source', { ascending: true, nullsFirst: false });
|
|
744
|
-
}
|
|
745
|
-
if (options.applyRange ?? true) {
|
|
746
|
-
const sampleSize = parsed.sampleSize ?? CATALOG_PREMIUM_DEFAULT_SAMPLE_SIZE;
|
|
747
|
-
query = query.range(0, sampleSize - 1);
|
|
748
|
-
}
|
|
749
|
-
return query;
|
|
750
|
-
}
|
|
751
|
-
async function fetchSupplierAggregateRows(supabase, parsed) {
|
|
752
|
-
const sampleSize = parsed.sampleSize ?? SUPPLIER_AGGREGATE_DEFAULT_SAMPLE_SIZE;
|
|
753
|
-
const pageSize = Math.min(sampleSize, SUPABASE_DATA_API_MAX_PAGE_SIZE);
|
|
754
|
-
const rows = [];
|
|
755
|
-
for (let offset = 0;; offset += pageSize) {
|
|
756
|
-
const { data, error } = await buildCatalogIntelligenceQuery(supabase, {
|
|
757
|
-
supplier: parsed.supplier,
|
|
758
|
-
country: parsed.country,
|
|
759
|
-
stocked: parsed.stocked,
|
|
760
|
-
nonWholesaleOnly: parsed.nonWholesaleOnly,
|
|
761
|
-
}, { applyRange: false })
|
|
762
|
-
.order('id', { ascending: true })
|
|
763
|
-
.range(offset, offset + pageSize - 1);
|
|
764
|
-
if (error)
|
|
765
|
-
throw error;
|
|
766
|
-
const page = (data ?? []);
|
|
767
|
-
rows.push(...page);
|
|
768
|
-
if (page.length < pageSize)
|
|
769
|
-
break;
|
|
770
|
-
}
|
|
771
|
-
return rows;
|
|
772
|
-
}
|
|
773
|
-
async function fetchCatalogPremiumSampleRows(supabase, parsed, sampleSize) {
|
|
774
|
-
const targetRows = sampleSize + 1;
|
|
775
|
-
const pageSize = Math.min(targetRows, SUPABASE_DATA_API_MAX_PAGE_SIZE);
|
|
776
|
-
const rows = [];
|
|
777
|
-
for (let offset = 0; rows.length < targetRows; offset += pageSize) {
|
|
778
|
-
const pageEnd = Math.min(offset + pageSize - 1, targetRows - 1);
|
|
779
|
-
const { data, error } = await buildCatalogIntelligenceQuery(supabase, {
|
|
780
|
-
...parsed,
|
|
781
|
-
orderByScore: true,
|
|
782
|
-
}, { applyRange: false })
|
|
783
|
-
.order('id', { ascending: true })
|
|
784
|
-
.range(offset, pageEnd);
|
|
785
|
-
if (error)
|
|
786
|
-
throw error;
|
|
787
|
-
const page = (data ?? []);
|
|
788
|
-
rows.push(...page);
|
|
789
|
-
if (page.length < pageEnd - offset + 1)
|
|
790
|
-
break;
|
|
791
|
-
}
|
|
792
|
-
return {
|
|
793
|
-
sampledRows: rows.slice(0, sampleSize),
|
|
794
|
-
truncated: rows.length > sampleSize,
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
const catalogIntelligenceCaveats = [
|
|
798
|
-
'Purveyor Score is read from coffee_catalog.purveyor_score with confidence, tier, factor breakdown, version, and update metadata; the CLI does not recompute the upstream score model.',
|
|
799
|
-
'Ranking is catalog-only and does not account for a roaster’s owned inventory, roast history, or target menu fit.',
|
|
800
|
-
];
|
|
801
|
-
const catalogPremiumRankingCaveats = [
|
|
802
|
-
...catalogIntelligenceCaveats,
|
|
803
|
-
'Premium ranking samples catalog rows ordered by purveyor_score descending, then id ascending, before applying agent-facing ranking logic; meta.truncated indicates more rows matched than the requested sample_size.',
|
|
804
|
-
];
|
|
805
|
-
const supplierAggregateCaveats = [
|
|
806
|
-
...catalogIntelligenceCaveats,
|
|
807
|
-
'Supplier aggregates paginate catalog rows ordered by source ascending, then id ascending; meta.sample_size is the requested fetch page size and meta.rows_examined reports the rows included in aggregation.',
|
|
808
|
-
];
|
|
809
636
|
const catalogFacetColumns = {
|
|
810
637
|
supplier: 'source',
|
|
811
638
|
country: 'country',
|
|
@@ -818,324 +645,154 @@ const catalogFacetColumns = {
|
|
|
818
645
|
const catalogFacetCaveats = [
|
|
819
646
|
'Facet counts are computed from catalog rows visible to the current client and are intended for value discovery, not inventory guarantees.',
|
|
820
647
|
];
|
|
821
|
-
const catalogRankingCaveats = [
|
|
822
|
-
...catalogIntelligenceCaveats,
|
|
823
|
-
'Generic catalog ranking samples catalog rows ordered by id ascending before applying deterministic objective-specific ranking; meta.truncated indicates more rows matched than the requested sample_size.',
|
|
824
|
-
];
|
|
825
|
-
function asPositiveNumber(value) {
|
|
826
|
-
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
|
|
827
|
-
}
|
|
828
|
-
function scoreBasis(item) {
|
|
829
|
-
const score = summarizePurveyorScore(item);
|
|
830
|
-
if (score.value === null)
|
|
831
|
-
return 'no Purveyor Score yet';
|
|
832
|
-
const tier = score.tier ? `, ${score.tier}` : '';
|
|
833
|
-
return `Purveyor Score ${score.value}${tier}`;
|
|
834
|
-
}
|
|
835
|
-
async function fetchCatalogFacetRows(supabase, parsed) {
|
|
836
|
-
const sampleSize = parsed.sampleSize ?? SUPPLIER_AGGREGATE_DEFAULT_SAMPLE_SIZE;
|
|
837
|
-
const targetRows = sampleSize + 1;
|
|
838
|
-
const pageSize = Math.min(targetRows, SUPABASE_DATA_API_MAX_PAGE_SIZE);
|
|
839
|
-
const rows = [];
|
|
840
|
-
const column = catalogFacetColumns[parsed.field];
|
|
841
|
-
for (let offset = 0; rows.length < targetRows; offset += pageSize) {
|
|
842
|
-
const pageEnd = Math.min(offset + pageSize - 1, targetRows - 1);
|
|
843
|
-
let query = supabase.from('coffee_catalog').select(`id, ${String(column)}`);
|
|
844
|
-
if (parsed.stockedOnly ?? true)
|
|
845
|
-
query = query.eq('stocked', true);
|
|
846
|
-
const { data, error } = await query.order('id', { ascending: true }).range(offset, pageEnd);
|
|
847
|
-
if (error)
|
|
848
|
-
throw error;
|
|
849
|
-
const page = (data ?? []);
|
|
850
|
-
rows.push(...page);
|
|
851
|
-
if (page.length < pageEnd - offset + 1)
|
|
852
|
-
break;
|
|
853
|
-
}
|
|
854
|
-
return { rows: rows.slice(0, sampleSize), truncated: rows.length > sampleSize };
|
|
855
|
-
}
|
|
856
|
-
async function fetchCatalogRankRows(supabase, parsed) {
|
|
857
|
-
const sampleSize = parsed.sampleSize ?? SUPPLIER_AGGREGATE_DEFAULT_SAMPLE_SIZE;
|
|
858
|
-
const targetRows = sampleSize + 1;
|
|
859
|
-
const pageSize = Math.min(targetRows, SUPABASE_DATA_API_MAX_PAGE_SIZE);
|
|
860
|
-
const rows = [];
|
|
861
|
-
for (let offset = 0; rows.length < targetRows; offset += pageSize) {
|
|
862
|
-
const pageEnd = Math.min(offset + pageSize - 1, targetRows - 1);
|
|
863
|
-
let query = supabase.from('coffee_catalog').select('*');
|
|
864
|
-
if (parsed.stockedOnly ?? true)
|
|
865
|
-
query = query.eq('stocked', true);
|
|
866
|
-
if (parsed.supplier)
|
|
867
|
-
query = query.ilike('source', `%${sanitizeFilterValue(parsed.supplier)}%`);
|
|
868
|
-
if (parsed.country)
|
|
869
|
-
query = query.ilike('country', `%${sanitizeFilterValue(parsed.country)}%`);
|
|
870
|
-
if (parsed.process) {
|
|
871
|
-
const process = sanitizeFilterValue(parsed.process);
|
|
872
|
-
query = query.or(`processing.ilike.%${process}%,processing_base_method.ilike.%${process}%`);
|
|
873
|
-
}
|
|
874
|
-
if (parsed.priceMax !== undefined)
|
|
875
|
-
query = query.lte('price_per_lb', parsed.priceMax);
|
|
876
|
-
if (parsed.minScore !== undefined)
|
|
877
|
-
query = query.gte('purveyor_score', parsed.minScore);
|
|
878
|
-
if (parsed.nonWholesaleOnly)
|
|
879
|
-
query = query.or('wholesale.is.null,wholesale.eq.false');
|
|
880
|
-
const { data, error } = await query.order('id', { ascending: true }).range(offset, pageEnd);
|
|
881
|
-
if (error)
|
|
882
|
-
throw error;
|
|
883
|
-
const page = (data ?? []);
|
|
884
|
-
rows.push(...page);
|
|
885
|
-
if (page.length < pageEnd - offset + 1)
|
|
886
|
-
break;
|
|
887
|
-
}
|
|
888
|
-
return { rows: rows.slice(0, sampleSize), truncated: rows.length > sampleSize };
|
|
889
|
-
}
|
|
890
|
-
function rankCatalogRows(rows, objective, limit) {
|
|
891
|
-
const byScoreDesc = (a, b) => (getPurveyorScoreValue(b) ?? -Infinity) - (getPurveyorScoreValue(a) ?? -Infinity) ||
|
|
892
|
-
String(b.stocked_date ?? '').localeCompare(String(a.stocked_date ?? '')) ||
|
|
893
|
-
a.id - b.id;
|
|
894
|
-
let ranked;
|
|
895
|
-
switch (objective) {
|
|
896
|
-
case 'premium':
|
|
897
|
-
ranked = [...rows].sort(byScoreDesc).map((row) => ({ row, basis: scoreBasis(row) }));
|
|
898
|
-
break;
|
|
899
|
-
case 'value': {
|
|
900
|
-
ranked = rows
|
|
901
|
-
.map((row) => {
|
|
902
|
-
const score = getPurveyorScoreValue(row);
|
|
903
|
-
const price = asPositiveNumber(getPerLbPrice(row));
|
|
904
|
-
if (score === null || price === null)
|
|
905
|
-
return null;
|
|
906
|
-
return { row, ratio: score / price };
|
|
907
|
-
})
|
|
908
|
-
.filter((entry) => entry !== null)
|
|
909
|
-
.sort((a, b) => b.ratio - a.ratio || byScoreDesc(a.row, b.row))
|
|
910
|
-
.map(({ row, ratio }) => ({
|
|
911
|
-
row,
|
|
912
|
-
basis: `${scoreBasis(row)} at $${getPerLbPrice(row)}/lb (${round(ratio, 1)} score points per dollar)`,
|
|
913
|
-
}));
|
|
914
|
-
break;
|
|
915
|
-
}
|
|
916
|
-
case 'fresh_arrival':
|
|
917
|
-
ranked = [...rows]
|
|
918
|
-
.sort((a, b) => String(b.stocked_date ?? '').localeCompare(String(a.stocked_date ?? '')) ||
|
|
919
|
-
String(b.arrival_date ?? '').localeCompare(String(a.arrival_date ?? '')) ||
|
|
920
|
-
byScoreDesc(a, b))
|
|
921
|
-
.map((row) => ({
|
|
922
|
-
row,
|
|
923
|
-
basis: `stocked ${row.stocked_date ?? 'date unknown'}; ${scoreBasis(row)}`,
|
|
924
|
-
}));
|
|
925
|
-
break;
|
|
926
|
-
case 'rare_origin': {
|
|
927
|
-
const originCounts = new Map();
|
|
928
|
-
for (const row of rows) {
|
|
929
|
-
const country = row.country?.trim();
|
|
930
|
-
if (country)
|
|
931
|
-
originCounts.set(country, (originCounts.get(country) ?? 0) + 1);
|
|
932
|
-
}
|
|
933
|
-
const rarity = (row) => {
|
|
934
|
-
const country = row.country?.trim();
|
|
935
|
-
return country
|
|
936
|
-
? (originCounts.get(country) ?? Number.MAX_SAFE_INTEGER)
|
|
937
|
-
: Number.MAX_SAFE_INTEGER;
|
|
938
|
-
};
|
|
939
|
-
ranked = [...rows]
|
|
940
|
-
.filter((row) => Boolean(row.country?.trim()))
|
|
941
|
-
.sort((a, b) => rarity(a) - rarity(b) || byScoreDesc(a, b))
|
|
942
|
-
.map((row) => ({
|
|
943
|
-
row,
|
|
944
|
-
basis: `${row.country} has ${rarity(row)} matching listing(s); ${scoreBasis(row)}`,
|
|
945
|
-
}));
|
|
946
|
-
break;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
return ranked.slice(0, limit).map(({ row, basis }, index) => ({
|
|
950
|
-
...row,
|
|
951
|
-
rank: index + 1,
|
|
952
|
-
rank_basis: basis,
|
|
953
|
-
}));
|
|
954
|
-
}
|
|
955
648
|
/** List distinct catalog facet values with counts for agent/client filter discovery. */
|
|
956
|
-
export async function listCatalogFacets(
|
|
649
|
+
export async function listCatalogFacets(input) {
|
|
957
650
|
const parsed = catalogFacetsSchema.parse(input);
|
|
958
651
|
const limit = parsed.limit ?? 60;
|
|
959
|
-
const sampleSize = parsed.sampleSize ?? SUPPLIER_AGGREGATE_DEFAULT_SAMPLE_SIZE;
|
|
960
|
-
const { rows, truncated } = await fetchCatalogFacetRows(supabase, parsed);
|
|
961
652
|
const column = catalogFacetColumns[parsed.field];
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
653
|
+
const facetKey = {
|
|
654
|
+
supplier: 'sources',
|
|
655
|
+
country: 'countries',
|
|
656
|
+
processing_base_method: 'processing_base_method',
|
|
657
|
+
fermentation_type: 'fermentation_type',
|
|
658
|
+
drying_method: 'drying_method',
|
|
659
|
+
grade: 'grade',
|
|
660
|
+
wholesale: 'wholesale',
|
|
661
|
+
}[parsed.field];
|
|
662
|
+
const client = await createParchmentClient('viewer');
|
|
663
|
+
const envelope = unwrapParchment(await client.catalog.facets({ stocked: parsed.stockedOnly === false ? 'all' : 'true' }), 'Catalog facets');
|
|
664
|
+
const facets = envelope.facets;
|
|
665
|
+
const values = (facets[facetKey] ?? []).slice(0, limit);
|
|
666
|
+
const rowsExamined = (facets[facetKey] ?? []).reduce((total, facet) => total + facet.count, 0);
|
|
973
667
|
return {
|
|
974
|
-
data: values
|
|
668
|
+
data: values,
|
|
975
669
|
meta: {
|
|
976
670
|
resource: 'catalog-facets',
|
|
977
671
|
field: parsed.field,
|
|
978
672
|
source_column: String(column),
|
|
979
673
|
stocked_only: parsed.stockedOnly ?? true,
|
|
980
674
|
scope: (parsed.stockedOnly ?? true) ? 'stocked_only' : 'all_visible',
|
|
981
|
-
sample_size:
|
|
982
|
-
sample_limited:
|
|
675
|
+
sample_size: rowsExamined,
|
|
676
|
+
sample_limited: false,
|
|
983
677
|
sample_order: 'id_asc',
|
|
984
|
-
truncated:
|
|
985
|
-
rows_examined:
|
|
986
|
-
distinct_values:
|
|
987
|
-
returned:
|
|
678
|
+
truncated: (facets[facetKey]?.length ?? 0) > limit,
|
|
679
|
+
rows_examined: rowsExamined,
|
|
680
|
+
distinct_values: facets[facetKey]?.length ?? 0,
|
|
681
|
+
returned: values.length,
|
|
988
682
|
caveats: catalogFacetCaveats,
|
|
989
683
|
},
|
|
990
684
|
};
|
|
991
685
|
}
|
|
992
686
|
/** Rank catalog candidates by deterministic product-intelligence objectives. */
|
|
993
|
-
export async function rankCatalog(
|
|
687
|
+
export async function rankCatalog(input = {}) {
|
|
994
688
|
const parsed = catalogRankSchema.parse(input);
|
|
995
|
-
const
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
meta: {
|
|
1010
|
-
resource: 'catalog-ranking',
|
|
1011
|
-
objective,
|
|
1012
|
-
scoring_source: 'coffee_catalog.purveyor_score',
|
|
1013
|
-
sample_size: sampleSize,
|
|
1014
|
-
sample_limited: true,
|
|
1015
|
-
sample_order: 'id_asc',
|
|
1016
|
-
truncated,
|
|
1017
|
-
candidates_considered: rows.length,
|
|
1018
|
-
returned: data.length,
|
|
1019
|
-
filters: {
|
|
1020
|
-
supplier: parsed.supplier,
|
|
1021
|
-
country: parsed.country,
|
|
1022
|
-
process: parsed.process,
|
|
1023
|
-
stocked_only: parsed.stockedOnly ?? true,
|
|
1024
|
-
scope: (parsed.stockedOnly ?? true) ? 'stocked_only' : 'all_visible',
|
|
1025
|
-
priceMax: parsed.priceMax,
|
|
1026
|
-
minScore: parsed.minScore,
|
|
1027
|
-
nonWholesaleOnly: parsed.nonWholesaleOnly ?? false,
|
|
1028
|
-
},
|
|
1029
|
-
caveats,
|
|
1030
|
-
},
|
|
1031
|
-
};
|
|
689
|
+
const client = await createParchmentClient('viewer');
|
|
690
|
+
const envelope = unwrapParchment(await client.catalog.rank({
|
|
691
|
+
objective: parsed.objective,
|
|
692
|
+
stockedOnly: parsed.stockedOnly === false ? 'false' : 'true',
|
|
693
|
+
supplier: parsed.supplier,
|
|
694
|
+
country: parsed.country,
|
|
695
|
+
process: parsed.process,
|
|
696
|
+
priceMax: parsed.priceMax,
|
|
697
|
+
minScore: parsed.minScore,
|
|
698
|
+
nonWholesaleOnly: parsed.nonWholesaleOnly ? 'true' : 'false',
|
|
699
|
+
limit: parsed.limit,
|
|
700
|
+
sampleSize: parsed.sampleSize,
|
|
701
|
+
}), 'Catalog ranking');
|
|
702
|
+
return envelope;
|
|
1032
703
|
}
|
|
1033
704
|
/**
|
|
1034
705
|
* Rank premium catalog candidates by Purveyor Score, with pricing and sourcing signals.
|
|
1035
706
|
*/
|
|
1036
|
-
export async function catalogRankPremium(
|
|
707
|
+
export async function catalogRankPremium(input = {}) {
|
|
1037
708
|
const parsed = catalogRankPremiumSchema.parse(input);
|
|
1038
|
-
const
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
709
|
+
const client = await createParchmentClient('viewer');
|
|
710
|
+
const envelope = unwrapParchment(await client.catalog.rankPremium({
|
|
711
|
+
origin: parsed.origin,
|
|
712
|
+
process: parsed.process,
|
|
713
|
+
supplier: parsed.supplier,
|
|
714
|
+
stocked: parsed.stocked === undefined ? undefined : parsed.stocked ? 'true' : 'false',
|
|
715
|
+
priceMax: parsed.priceMax,
|
|
1043
716
|
minScore: parsed.minScore,
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
scoring_source: 'coffee_catalog.purveyor_score',
|
|
1050
|
-
sample_size: sampleSize,
|
|
1051
|
-
sample_limited: true,
|
|
1052
|
-
sample_order: 'purveyor_score_desc_nulls_last',
|
|
1053
|
-
truncated,
|
|
1054
|
-
returned: ranking.length,
|
|
1055
|
-
filters: {
|
|
1056
|
-
origin: parsed.origin,
|
|
1057
|
-
process: parsed.process,
|
|
1058
|
-
supplier: parsed.supplier,
|
|
1059
|
-
stocked: parsed.stocked,
|
|
1060
|
-
priceMax: parsed.priceMax,
|
|
1061
|
-
minScore: parsed.minScore,
|
|
1062
|
-
includeUnscored: parsed.includeUnscored ?? false,
|
|
1063
|
-
},
|
|
1064
|
-
caveats: catalogPremiumRankingCaveats,
|
|
1065
|
-
},
|
|
1066
|
-
};
|
|
717
|
+
includeUnscored: parsed.includeUnscored ? 'true' : 'false',
|
|
718
|
+
limit: parsed.limit,
|
|
719
|
+
sampleSize: parsed.sampleSize,
|
|
720
|
+
}), 'Catalog premium ranking');
|
|
721
|
+
return envelope;
|
|
1067
722
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const sampleSize = parsed.sampleSize ?? SUPPLIER_AGGREGATE_DEFAULT_SAMPLE_SIZE;
|
|
1071
|
-
const rows = await fetchSupplierAggregateRows(supabase, {
|
|
723
|
+
function toSupplierQuery(parsed) {
|
|
724
|
+
return {
|
|
1072
725
|
supplier: parsed.supplier,
|
|
1073
726
|
country: parsed.country,
|
|
1074
|
-
stocked: parsed.stocked
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
minCoffees: parsed.minCoffees
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
meta: {
|
|
1085
|
-
resource,
|
|
1086
|
-
sample_size: sampleSize,
|
|
1087
|
-
sample_limited: false,
|
|
1088
|
-
sample_order: 'source_asc_nulls_last',
|
|
1089
|
-
truncated: false,
|
|
1090
|
-
rows_examined: rows.length,
|
|
1091
|
-
returned: aggregates.length,
|
|
1092
|
-
filters: {
|
|
1093
|
-
supplier: parsed.supplier,
|
|
1094
|
-
country: parsed.country,
|
|
1095
|
-
stocked: parsed.stocked,
|
|
1096
|
-
minCoffees: parsed.minCoffees ?? 1,
|
|
1097
|
-
nonWholesaleOnly: parsed.nonWholesaleOnly ?? false,
|
|
1098
|
-
},
|
|
1099
|
-
caveats: supplierAggregateCaveats,
|
|
1100
|
-
},
|
|
727
|
+
stocked: parsed.stocked === undefined
|
|
728
|
+
? undefined
|
|
729
|
+
: parsed.stocked
|
|
730
|
+
? 'true'
|
|
731
|
+
: 'false',
|
|
732
|
+
nonWholesaleOnly: parsed.nonWholesaleOnly ? 'true' : 'false',
|
|
733
|
+
minCoffees: parsed.minCoffees,
|
|
734
|
+
topCoffees: parsed.topCoffees,
|
|
735
|
+
limit: parsed.limit,
|
|
736
|
+
sampleSize: parsed.sampleSize,
|
|
1101
737
|
};
|
|
1102
738
|
}
|
|
1103
739
|
/** List supplier aggregates from catalog rows. */
|
|
1104
|
-
export async function supplierList(
|
|
1105
|
-
|
|
740
|
+
export async function supplierList(input = {}) {
|
|
741
|
+
const parsed = supplierAggregateSchema.parse(input);
|
|
742
|
+
const client = await createParchmentClient('viewer');
|
|
743
|
+
return unwrapParchment(await client.catalog.suppliers(toSupplierQuery(parsed)), 'Catalog suppliers');
|
|
1106
744
|
}
|
|
1107
745
|
/** Return aggregate detail for a supplier query. */
|
|
1108
|
-
export async function supplierDetail(
|
|
746
|
+
export async function supplierDetail(input) {
|
|
1109
747
|
const parsed = supplierAggregateSchema.parse(input);
|
|
1110
748
|
if (!parsed.supplier?.trim()) {
|
|
1111
749
|
throw new PrvrsError('INVALID_ARGUMENT', 'supplierDetail requires a non-empty supplier name.');
|
|
1112
750
|
}
|
|
1113
|
-
|
|
751
|
+
const client = await createParchmentClient('viewer');
|
|
752
|
+
return unwrapParchment(await client.catalog.supplierDetail({
|
|
753
|
+
...toSupplierQuery({ ...parsed, limit: parsed.limit ?? 10 }),
|
|
754
|
+
supplier: parsed.supplier,
|
|
755
|
+
}), 'Catalog supplier detail');
|
|
1114
756
|
}
|
|
1115
757
|
/** Rank suppliers by average Purveyor Score, then currently stocked coverage. */
|
|
1116
|
-
export async function supplierRank(
|
|
1117
|
-
|
|
758
|
+
export async function supplierRank(input = {}) {
|
|
759
|
+
const parsed = supplierAggregateSchema.parse(input);
|
|
760
|
+
const client = await createParchmentClient('viewer');
|
|
761
|
+
return unwrapParchment(await client.catalog.supplierRank(toSupplierQuery(parsed)), 'Catalog supplier ranking');
|
|
1118
762
|
}
|
|
1119
763
|
/**
|
|
1120
764
|
* Fetch canonical catalog similarity groups from the beta /v1/catalog/{id}/similar API.
|
|
1121
765
|
*/
|
|
1122
|
-
export async function getCatalogSimilarity(
|
|
766
|
+
export async function getCatalogSimilarity(input) {
|
|
1123
767
|
const parsed = getCatalogSimilaritySchema.parse(input);
|
|
1124
|
-
return fetchCatalogSimilarityApi(
|
|
768
|
+
return fetchCatalogSimilarityApi(parsed);
|
|
1125
769
|
}
|
|
1126
770
|
/**
|
|
1127
771
|
* Find beans similar to a target coffee using pgvector embedding similarity.
|
|
1128
772
|
* Calls the `find_similar_beans_aggregated` RPC and returns ranked matches.
|
|
1129
773
|
*/
|
|
1130
|
-
export async function findSimilarBeans(
|
|
774
|
+
export async function findSimilarBeans(input) {
|
|
1131
775
|
const parsed = findSimilarBeansSchema.parse(input);
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
776
|
+
const response = await getCatalogSimilarity({
|
|
777
|
+
coffee_id: parsed.coffee_id,
|
|
778
|
+
threshold: parsed.threshold,
|
|
779
|
+
limit: parsed.limit,
|
|
1136
780
|
});
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
781
|
+
const matches = response.data.matches ?? [
|
|
782
|
+
...response.data.groups.canonical_candidates,
|
|
783
|
+
...response.data.groups.similar_recommendations,
|
|
784
|
+
];
|
|
785
|
+
return matches.map((match) => ({
|
|
786
|
+
coffee_id: match.coffee.id,
|
|
787
|
+
coffee_name: match.coffee.name,
|
|
788
|
+
source: match.coffee.source ?? '',
|
|
789
|
+
origin: match.coffee.origin,
|
|
790
|
+
processing: match.coffee.processing,
|
|
791
|
+
cost_lb: match.compatibility.cost_lb,
|
|
792
|
+
price_per_lb: match.pricing.price_per_lb,
|
|
793
|
+
stocked: match.coffee.stocked ?? false,
|
|
794
|
+
avg_similarity: match.score.average,
|
|
795
|
+
chunk_matches: match.score.chunk_matches,
|
|
796
|
+
}));
|
|
1140
797
|
}
|
|
1141
798
|
//# sourceMappingURL=catalog.js.map
|