@buildinternet/releases-api-types 0.18.0 → 0.20.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 +2 -2
- package/src/api-types.ts +73 -0
- package/src/schemas/orgs.ts +3 -0
- package/src/schemas/releases.ts +12 -1
- package/src/schemas/search.ts +35 -0
- package/src/schemas/shared.ts +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinternet/releases-api-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Wire protocol — request/response types for the Releases registry API. Shared by web, workers, and clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@buildinternet/releases-core": "^0.21.0",
|
|
21
|
-
"zod": "^4.3
|
|
21
|
+
"zod": "^4.4.3"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
package/src/api-types.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
StatsSchema,
|
|
17
17
|
ErrorResponseSchema,
|
|
18
18
|
ReleaseTypeSchema,
|
|
19
|
+
ReleaseCompositionSchema,
|
|
19
20
|
ReleaseItemSchema,
|
|
20
21
|
ReleaseSummaryItemSchema,
|
|
21
22
|
OverviewPageItemSchema,
|
|
@@ -148,6 +149,7 @@ import type {
|
|
|
148
149
|
SearchSourceHitSchema,
|
|
149
150
|
SearchReleaseHitSchema,
|
|
150
151
|
SearchChunkHitSchema,
|
|
152
|
+
SearchCollectionHitSchema,
|
|
151
153
|
LookupResultPayloadSchema,
|
|
152
154
|
UnifiedSearchResponseSchema,
|
|
153
155
|
} from "./schemas/search.js";
|
|
@@ -221,6 +223,7 @@ export {
|
|
|
221
223
|
StatsSchema,
|
|
222
224
|
ErrorResponseSchema,
|
|
223
225
|
ReleaseTypeSchema,
|
|
226
|
+
ReleaseCompositionSchema,
|
|
224
227
|
ReleaseItemSchema,
|
|
225
228
|
ReleaseSummaryItemSchema,
|
|
226
229
|
OverviewPageItemSchema,
|
|
@@ -367,6 +370,7 @@ export {
|
|
|
367
370
|
SearchSourceHitSchema,
|
|
368
371
|
SearchReleaseHitSchema,
|
|
369
372
|
SearchChunkHitSchema,
|
|
373
|
+
SearchCollectionHitSchema,
|
|
370
374
|
LookupResultPayloadSchema,
|
|
371
375
|
UnifiedSearchResponseSchema,
|
|
372
376
|
} from "./schemas/search.js";
|
|
@@ -569,6 +573,7 @@ export interface OrgsRollupResponse extends ListResponse<OrgsRollupRow> {
|
|
|
569
573
|
export type ReleaseType = z.infer<typeof ReleaseTypeSchema>;
|
|
570
574
|
|
|
571
575
|
export type ReleaseItem = z.infer<typeof ReleaseItemSchema>;
|
|
576
|
+
export type ReleaseComposition = z.infer<typeof ReleaseCompositionSchema>;
|
|
572
577
|
|
|
573
578
|
export interface ReleaseDetail {
|
|
574
579
|
id: string;
|
|
@@ -597,6 +602,8 @@ export interface ReleaseDetail {
|
|
|
597
602
|
org: { slug: string; name: string } | null;
|
|
598
603
|
/** Release type. See {@link ReleaseType}. */
|
|
599
604
|
type?: ReleaseType;
|
|
605
|
+
/** Per-category item counts from the AI release-content pass. See {@link ReleaseComposition}. */
|
|
606
|
+
composition?: ReleaseComposition | null;
|
|
600
607
|
}
|
|
601
608
|
|
|
602
609
|
export type ReleaseCoverageRow = z.infer<typeof ReleaseCoverageRowSchema>;
|
|
@@ -676,6 +683,63 @@ export function foldSourcesIntoCatalog(
|
|
|
676
683
|
|
|
677
684
|
export type SearchReleaseHit = z.infer<typeof SearchReleaseHitSchema>;
|
|
678
685
|
export type SearchChunkHit = z.infer<typeof SearchChunkHitSchema>;
|
|
686
|
+
export type SearchCollectionHit = z.infer<typeof SearchCollectionHitSchema>;
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Merge collection hits from three independent sources into one ordered,
|
|
690
|
+
* deduped array on the wire:
|
|
691
|
+
*
|
|
692
|
+
* 1. Direct lexical matches — name/slug/description LIKE.
|
|
693
|
+
* 2. Direct semantic matches — vector hit (hybrid/semantic only).
|
|
694
|
+
* 3. Member rollups — collections containing one of the result orgs.
|
|
695
|
+
*
|
|
696
|
+
* A direct row always wins for a given slug; member rollups attach their
|
|
697
|
+
* `matchedOrgSlugs` to a winning direct row so the UI keeps the "shown
|
|
698
|
+
* because" affordance. Within each `via` bucket, rows order by score desc
|
|
699
|
+
* (missing scores last) then by name.
|
|
700
|
+
*
|
|
701
|
+
* Pure: no DB, no Workers runtime. Used by both `/v1/search` and the MCP
|
|
702
|
+
* `search` tool so the merge stays in lockstep across surfaces.
|
|
703
|
+
*/
|
|
704
|
+
export function mergeCollectionHits(
|
|
705
|
+
direct: SearchCollectionHit[],
|
|
706
|
+
semantic: SearchCollectionHit[],
|
|
707
|
+
member: SearchCollectionHit[],
|
|
708
|
+
limit: number,
|
|
709
|
+
): SearchCollectionHit[] {
|
|
710
|
+
// Pure: never mutate caller-owned objects. Always replace map entries with
|
|
711
|
+
// shallow copies so two callers (e.g. API and MCP merging from the same
|
|
712
|
+
// upstream list) can't see each other's writes.
|
|
713
|
+
const bySlug = new Map<string, SearchCollectionHit>();
|
|
714
|
+
for (const c of direct) bySlug.set(c.slug, { ...c });
|
|
715
|
+
for (const c of semantic) {
|
|
716
|
+
const existing = bySlug.get(c.slug);
|
|
717
|
+
if (existing) {
|
|
718
|
+
if (c.score !== undefined && (existing.score === undefined || c.score > existing.score)) {
|
|
719
|
+
bySlug.set(c.slug, { ...existing, score: c.score });
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
bySlug.set(c.slug, { ...c });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
for (const c of member) {
|
|
726
|
+
const existing = bySlug.get(c.slug);
|
|
727
|
+
if (existing) {
|
|
728
|
+
bySlug.set(c.slug, { ...existing, matchedOrgSlugs: c.matchedOrgSlugs });
|
|
729
|
+
} else {
|
|
730
|
+
bySlug.set(c.slug, { ...c });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return [...bySlug.values()]
|
|
734
|
+
.toSorted((a, b) => {
|
|
735
|
+
if (a.via !== b.via) return a.via === "direct" ? -1 : 1;
|
|
736
|
+
const sa = a.score ?? -Infinity;
|
|
737
|
+
const sb = b.score ?? -Infinity;
|
|
738
|
+
if (sa !== sb) return sb - sa;
|
|
739
|
+
return a.name.localeCompare(b.name);
|
|
740
|
+
})
|
|
741
|
+
.slice(0, limit);
|
|
742
|
+
}
|
|
679
743
|
|
|
680
744
|
// ── Lookups ──
|
|
681
745
|
|
|
@@ -931,6 +995,15 @@ export interface LatestRelease {
|
|
|
931
995
|
/** AI-generated smart-brevity headline (#852, renamed in #860). */
|
|
932
996
|
titleShort?: string | null;
|
|
933
997
|
media: MediaItem[];
|
|
998
|
+
/**
|
|
999
|
+
* Cached release-body size — `LENGTH(content)` and `countTokensSafe(content)`
|
|
1000
|
+
* snapshotted at ingest, so clients can advertise "this release is ~1.5K
|
|
1001
|
+
* tokens" without round-tripping the body. Both fields are optional and
|
|
1002
|
+
* nullable: pre-existing rows land null until the backfill script
|
|
1003
|
+
* populates them. See #958.
|
|
1004
|
+
*/
|
|
1005
|
+
contentChars?: number | null;
|
|
1006
|
+
contentTokens?: number | null;
|
|
934
1007
|
}
|
|
935
1008
|
|
|
936
1009
|
// ── Stats ──
|
package/src/schemas/orgs.ts
CHANGED
|
@@ -75,8 +75,11 @@ 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(),
|
|
81
|
+
/** Admin-only: pause/unpause all ingest for this org without touching sources (#1057). */
|
|
82
|
+
fetchPaused: z.boolean().optional(),
|
|
80
83
|
});
|
|
81
84
|
|
|
82
85
|
// Org detail's products query selects a strict subset of `ProductListItem` —
|
package/src/schemas/releases.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SOURCE_TYPES } from "@buildinternet/releases-core/source-enums";
|
|
3
|
-
import { MediaItemSchema, ReleaseTypeSchema } from "./shared.js";
|
|
3
|
+
import { MediaItemSchema, ReleaseCompositionSchema, ReleaseTypeSchema } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
// ReleaseCompositionSchema lives in ./shared.js (single source of truth) — both
|
|
6
|
+
// this file and ReleaseItemSchema reference it from there.
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Per-row shape returned by `GET /v1/releases/latest`. Distinct from the
|
|
@@ -32,6 +35,11 @@ export const ReleaseLatestItemSchema = z.object({
|
|
|
32
35
|
media: z.array(MediaItemSchema),
|
|
33
36
|
source: ReleaseLatestSourceSchema,
|
|
34
37
|
coverageCount: z.number().int().min(0).optional(),
|
|
38
|
+
// Cached release-body size hint — see {@link ReleaseItemSchema} for the
|
|
39
|
+
// same fields on the org / collection feeds. #958.
|
|
40
|
+
contentChars: z.number().int().min(0).nullable().optional(),
|
|
41
|
+
contentTokens: z.number().int().min(0).nullable().optional(),
|
|
42
|
+
composition: ReleaseCompositionSchema.nullable().optional(),
|
|
35
43
|
});
|
|
36
44
|
|
|
37
45
|
export const ReleaseLatestResponseSchema = z.object({
|
|
@@ -151,6 +159,7 @@ export const ReleaseDetailResponseSchema = z.object({
|
|
|
151
159
|
sourceSlug: z.string().nullable(),
|
|
152
160
|
sourceType: z.enum(SOURCE_TYPES).nullable(),
|
|
153
161
|
org: ReleaseDetailOrgSchema.nullable(),
|
|
162
|
+
composition: ReleaseCompositionSchema.nullable(),
|
|
154
163
|
});
|
|
155
164
|
|
|
156
165
|
/**
|
|
@@ -173,6 +182,8 @@ export const UpdateReleaseBodySchema = z
|
|
|
173
182
|
summary: z.string().nullable().optional(),
|
|
174
183
|
titleGenerated: z.string().nullable().optional(),
|
|
175
184
|
titleShort: z.string().nullable().optional(),
|
|
185
|
+
// `null` removes `$.composition` from metadata; an object replaces it.
|
|
186
|
+
composition: ReleaseCompositionSchema.nullable().optional(),
|
|
176
187
|
})
|
|
177
188
|
.strict();
|
|
178
189
|
|
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({
|
|
@@ -40,6 +46,19 @@ export const StatsSchema = z.object({
|
|
|
40
46
|
|
|
41
47
|
export const ReleaseTypeSchema = z.enum(RELEASE_TYPES);
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Per-category item counts produced by the AI release-content pass and
|
|
51
|
+
* persisted on `releases.metadata.composition`. Surfaces as a chip on the
|
|
52
|
+
* release detail UI ("12 fixes · 3 features"); `null` when the row hasn't
|
|
53
|
+
* been through the AI pass yet, the body was empty/boilerplate, or all
|
|
54
|
+
* three counts came back zero (we filter that case out on the parse side).
|
|
55
|
+
*/
|
|
56
|
+
export const ReleaseCompositionSchema = z.object({
|
|
57
|
+
bugs: z.number().int().min(0),
|
|
58
|
+
features: z.number().int().min(0),
|
|
59
|
+
enhancements: z.number().int().min(0),
|
|
60
|
+
});
|
|
61
|
+
|
|
43
62
|
export const ReleaseItemSchema = z.object({
|
|
44
63
|
id: z.string().optional(),
|
|
45
64
|
version: z.string().nullable(),
|
|
@@ -74,6 +93,19 @@ export const ReleaseItemSchema = z.object({
|
|
|
74
93
|
// detail-page round trip. `.optional()` — older API responses and pinned
|
|
75
94
|
// workers omit the field; treat undefined as 0.
|
|
76
95
|
coverageCount: z.number().int().min(0).optional(),
|
|
96
|
+
// Cached `LENGTH(content)` and `countTokensSafe(content)` for the release
|
|
97
|
+
// body — lets feed surfaces ("releases get <org>", `/v1/releases/latest`,
|
|
98
|
+
// MCP `get_latest_releases`) advertise "this release is ~1.5K tokens" so
|
|
99
|
+
// agents can decide whether to pull the full body. `.optional()` and
|
|
100
|
+
// nullable because pre-existing rows landed without the columns; the
|
|
101
|
+
// backfill script populates them and renderers degrade to "size unknown"
|
|
102
|
+
// when null. See #958.
|
|
103
|
+
contentChars: z.number().int().min(0).nullable().optional(),
|
|
104
|
+
contentTokens: z.number().int().min(0).nullable().optional(),
|
|
105
|
+
// Per-category item counts produced by the AI release-content pass and
|
|
106
|
+
// stored on `releases.metadata.composition`. `null`/`undefined` when the
|
|
107
|
+
// row hasn't been through the AI pass yet or the body was boilerplate.
|
|
108
|
+
composition: ReleaseCompositionSchema.nullable().optional(),
|
|
77
109
|
});
|
|
78
110
|
|
|
79
111
|
export const ReleaseSummaryItemSchema = z.object({
|