@buildinternet/releases-api-types 0.18.0 → 0.19.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/package.json +1 -1
- package/src/api-types.ts +68 -0
- package/src/schemas/orgs.ts +1 -0
- package/src/schemas/releases.ts +4 -0
- package/src/schemas/search.ts +35 -0
- package/src/schemas/shared.ts +15 -0
package/package.json
CHANGED
package/src/api-types.ts
CHANGED
|
@@ -148,6 +148,7 @@ import type {
|
|
|
148
148
|
SearchSourceHitSchema,
|
|
149
149
|
SearchReleaseHitSchema,
|
|
150
150
|
SearchChunkHitSchema,
|
|
151
|
+
SearchCollectionHitSchema,
|
|
151
152
|
LookupResultPayloadSchema,
|
|
152
153
|
UnifiedSearchResponseSchema,
|
|
153
154
|
} from "./schemas/search.js";
|
|
@@ -367,6 +368,7 @@ export {
|
|
|
367
368
|
SearchSourceHitSchema,
|
|
368
369
|
SearchReleaseHitSchema,
|
|
369
370
|
SearchChunkHitSchema,
|
|
371
|
+
SearchCollectionHitSchema,
|
|
370
372
|
LookupResultPayloadSchema,
|
|
371
373
|
UnifiedSearchResponseSchema,
|
|
372
374
|
} from "./schemas/search.js";
|
|
@@ -676,6 +678,63 @@ export function foldSourcesIntoCatalog(
|
|
|
676
678
|
|
|
677
679
|
export type SearchReleaseHit = z.infer<typeof SearchReleaseHitSchema>;
|
|
678
680
|
export type SearchChunkHit = z.infer<typeof SearchChunkHitSchema>;
|
|
681
|
+
export type SearchCollectionHit = z.infer<typeof SearchCollectionHitSchema>;
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Merge collection hits from three independent sources into one ordered,
|
|
685
|
+
* deduped array on the wire:
|
|
686
|
+
*
|
|
687
|
+
* 1. Direct lexical matches — name/slug/description LIKE.
|
|
688
|
+
* 2. Direct semantic matches — vector hit (hybrid/semantic only).
|
|
689
|
+
* 3. Member rollups — collections containing one of the result orgs.
|
|
690
|
+
*
|
|
691
|
+
* A direct row always wins for a given slug; member rollups attach their
|
|
692
|
+
* `matchedOrgSlugs` to a winning direct row so the UI keeps the "shown
|
|
693
|
+
* because" affordance. Within each `via` bucket, rows order by score desc
|
|
694
|
+
* (missing scores last) then by name.
|
|
695
|
+
*
|
|
696
|
+
* Pure: no DB, no Workers runtime. Used by both `/v1/search` and the MCP
|
|
697
|
+
* `search` tool so the merge stays in lockstep across surfaces.
|
|
698
|
+
*/
|
|
699
|
+
export function mergeCollectionHits(
|
|
700
|
+
direct: SearchCollectionHit[],
|
|
701
|
+
semantic: SearchCollectionHit[],
|
|
702
|
+
member: SearchCollectionHit[],
|
|
703
|
+
limit: number,
|
|
704
|
+
): SearchCollectionHit[] {
|
|
705
|
+
// Pure: never mutate caller-owned objects. Always replace map entries with
|
|
706
|
+
// shallow copies so two callers (e.g. API and MCP merging from the same
|
|
707
|
+
// upstream list) can't see each other's writes.
|
|
708
|
+
const bySlug = new Map<string, SearchCollectionHit>();
|
|
709
|
+
for (const c of direct) bySlug.set(c.slug, { ...c });
|
|
710
|
+
for (const c of semantic) {
|
|
711
|
+
const existing = bySlug.get(c.slug);
|
|
712
|
+
if (existing) {
|
|
713
|
+
if (c.score !== undefined && (existing.score === undefined || c.score > existing.score)) {
|
|
714
|
+
bySlug.set(c.slug, { ...existing, score: c.score });
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
bySlug.set(c.slug, { ...c });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
for (const c of member) {
|
|
721
|
+
const existing = bySlug.get(c.slug);
|
|
722
|
+
if (existing) {
|
|
723
|
+
bySlug.set(c.slug, { ...existing, matchedOrgSlugs: c.matchedOrgSlugs });
|
|
724
|
+
} else {
|
|
725
|
+
bySlug.set(c.slug, { ...c });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return [...bySlug.values()]
|
|
729
|
+
.toSorted((a, b) => {
|
|
730
|
+
if (a.via !== b.via) return a.via === "direct" ? -1 : 1;
|
|
731
|
+
const sa = a.score ?? -Infinity;
|
|
732
|
+
const sb = b.score ?? -Infinity;
|
|
733
|
+
if (sa !== sb) return sb - sa;
|
|
734
|
+
return a.name.localeCompare(b.name);
|
|
735
|
+
})
|
|
736
|
+
.slice(0, limit);
|
|
737
|
+
}
|
|
679
738
|
|
|
680
739
|
// ── Lookups ──
|
|
681
740
|
|
|
@@ -931,6 +990,15 @@ export interface LatestRelease {
|
|
|
931
990
|
/** AI-generated smart-brevity headline (#852, renamed in #860). */
|
|
932
991
|
titleShort?: string | null;
|
|
933
992
|
media: MediaItem[];
|
|
993
|
+
/**
|
|
994
|
+
* Cached release-body size — `LENGTH(content)` and `countTokensSafe(content)`
|
|
995
|
+
* snapshotted at ingest, so clients can advertise "this release is ~1.5K
|
|
996
|
+
* tokens" without round-tripping the body. Both fields are optional and
|
|
997
|
+
* nullable: pre-existing rows land null until the backfill script
|
|
998
|
+
* populates them. See #958.
|
|
999
|
+
*/
|
|
1000
|
+
contentChars?: number | null;
|
|
1001
|
+
contentTokens?: number | null;
|
|
934
1002
|
}
|
|
935
1003
|
|
|
936
1004
|
// ── Stats ──
|
package/src/schemas/orgs.ts
CHANGED
|
@@ -75,6 +75,7 @@ export const UpdateOrgBodySchema = z.object({
|
|
|
75
75
|
domain: z.string().nullable().optional(),
|
|
76
76
|
description: z.string().nullable().optional(),
|
|
77
77
|
category: z.string().min(1).nullable().optional(),
|
|
78
|
+
avatarUrl: z.string().nullable().optional(),
|
|
78
79
|
tags: z.array(z.string()).optional(),
|
|
79
80
|
aliases: z.array(z.string()).optional(),
|
|
80
81
|
});
|
package/src/schemas/releases.ts
CHANGED
|
@@ -32,6 +32,10 @@ export const ReleaseLatestItemSchema = z.object({
|
|
|
32
32
|
media: z.array(MediaItemSchema),
|
|
33
33
|
source: ReleaseLatestSourceSchema,
|
|
34
34
|
coverageCount: z.number().int().min(0).optional(),
|
|
35
|
+
// Cached release-body size hint — see {@link ReleaseItemSchema} for the
|
|
36
|
+
// same fields on the org / collection feeds. #958.
|
|
37
|
+
contentChars: z.number().int().min(0).nullable().optional(),
|
|
38
|
+
contentTokens: z.number().int().min(0).nullable().optional(),
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
export const ReleaseLatestResponseSchema = z.object({
|
package/src/schemas/search.ts
CHANGED
|
@@ -80,6 +80,34 @@ export const SearchReleaseHitSchema = z.object({
|
|
|
80
80
|
coverageCount: z.number().int().min(0).optional(),
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Collection hit on the unified `/v1/search` response. Two origins are folded
|
|
85
|
+
* into the same wire shape via the `via` discriminator:
|
|
86
|
+
*
|
|
87
|
+
* - `direct` — the collection itself matched (name/description via LIKE in
|
|
88
|
+
* every mode, plus vector match in hybrid/semantic mode).
|
|
89
|
+
* - `member` — the collection was surfaced because one of the orgs that
|
|
90
|
+
* *did* match the query is a member of it; `matchedOrgSlugs`
|
|
91
|
+
* carries the subset of result-set org slugs that triggered
|
|
92
|
+
* the rollup so the UI can render an "includes X, Y" hint.
|
|
93
|
+
*
|
|
94
|
+
* `direct` rows always sort ahead of `member` rows; ties break on
|
|
95
|
+
* `score` then `memberCount`. Slugs are unique across the array — a
|
|
96
|
+
* direct hit that's also a member rollup keeps only the `direct` row
|
|
97
|
+
* with `matchedOrgSlugs` carried over.
|
|
98
|
+
*/
|
|
99
|
+
export const SearchCollectionHitSchema = z.object({
|
|
100
|
+
slug: z.string(),
|
|
101
|
+
name: z.string(),
|
|
102
|
+
description: z.string().nullable(),
|
|
103
|
+
memberCount: z.number().int().min(0),
|
|
104
|
+
via: z.enum(["direct", "member"]),
|
|
105
|
+
/** Hybrid/semantic fusion score; absent on pure-lexical hits. */
|
|
106
|
+
score: z.number().optional(),
|
|
107
|
+
/** Result-set org slugs that triggered this rollup. Always present on `via=member`. */
|
|
108
|
+
matchedOrgSlugs: z.array(z.string()).optional(),
|
|
109
|
+
});
|
|
110
|
+
|
|
83
111
|
/**
|
|
84
112
|
* Heading-aware CHANGELOG.md slice returned by hybrid / semantic search.
|
|
85
113
|
* Clients can deep-link to
|
|
@@ -149,6 +177,13 @@ export const UnifiedSearchResponseSchema = z.object({
|
|
|
149
177
|
catalog: z.array(SearchCatalogHitSchema),
|
|
150
178
|
sources: z.array(SearchSourceHitSchema),
|
|
151
179
|
releases: z.array(SearchReleaseHitSchema),
|
|
180
|
+
/**
|
|
181
|
+
* Curated collections matching the query, either directly (name/description
|
|
182
|
+
* match or vector hit) or via member rollup (one of the result orgs is in
|
|
183
|
+
* the collection). Optional on the wire so older workers mid-rollout don't
|
|
184
|
+
* trip the schema check; clients should treat missing and `[]` identically.
|
|
185
|
+
*/
|
|
186
|
+
collections: z.array(SearchCollectionHitSchema).optional(),
|
|
152
187
|
chunks: z.array(SearchChunkHitSchema).optional(),
|
|
153
188
|
mode: z.enum(["lexical", "semantic", "hybrid"]).optional(),
|
|
154
189
|
degraded: z.boolean().optional(),
|
package/src/schemas/shared.ts
CHANGED
|
@@ -29,6 +29,12 @@ export const ListResponseSchema = <T extends z.ZodTypeAny>(item: T) =>
|
|
|
29
29
|
export const ErrorResponseSchema = z.object({
|
|
30
30
|
error: z.string(),
|
|
31
31
|
message: z.string(),
|
|
32
|
+
// Stable D1 error classification from `classifyDbError()` in
|
|
33
|
+
// `@releases/lib/db-errors`. Set on 5xx responses that originate from a
|
|
34
|
+
// classified D1 failure so callers can branch on a stable code instead of
|
|
35
|
+
// substring-matching the upstream message. Absent on non-D1 errors and on
|
|
36
|
+
// routes that have not yet been wired. See DbErrorCode in packages/lib.
|
|
37
|
+
errorCode: z.string().optional(),
|
|
32
38
|
});
|
|
33
39
|
|
|
34
40
|
export const StatsSchema = z.object({
|
|
@@ -74,6 +80,15 @@ export const ReleaseItemSchema = z.object({
|
|
|
74
80
|
// detail-page round trip. `.optional()` — older API responses and pinned
|
|
75
81
|
// workers omit the field; treat undefined as 0.
|
|
76
82
|
coverageCount: z.number().int().min(0).optional(),
|
|
83
|
+
// Cached `LENGTH(content)` and `countTokensSafe(content)` for the release
|
|
84
|
+
// body — lets feed surfaces ("releases get <org>", `/v1/releases/latest`,
|
|
85
|
+
// MCP `get_latest_releases`) advertise "this release is ~1.5K tokens" so
|
|
86
|
+
// agents can decide whether to pull the full body. `.optional()` and
|
|
87
|
+
// nullable because pre-existing rows landed without the columns; the
|
|
88
|
+
// backfill script populates them and renderers degrade to "size unknown"
|
|
89
|
+
// when null. See #958.
|
|
90
|
+
contentChars: z.number().int().min(0).nullable().optional(),
|
|
91
|
+
contentTokens: z.number().int().min(0).nullable().optional(),
|
|
77
92
|
});
|
|
78
93
|
|
|
79
94
|
export const ReleaseSummaryItemSchema = z.object({
|