@buildinternet/releases-api-types 0.6.0 → 0.8.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/releases-api-types",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",
@@ -16,6 +16,10 @@
16
16
  "src",
17
17
  "README.md"
18
18
  ],
19
+ "dependencies": {
20
+ "@buildinternet/releases-core": "workspace:*",
21
+ "zod": "^4.3.6"
22
+ },
19
23
  "publishConfig": {
20
24
  "access": "public"
21
25
  }
package/src/api-types.ts CHANGED
@@ -1,44 +1,132 @@
1
1
  /**
2
- * Shared API response types.
3
- *
4
- * This is the single source of truth for all API response shapes.
5
- * Consumed by: web frontend, CLI client, and (optionally) API worker routes.
2
+ * Shared API response types — single source of truth for the wire protocol.
3
+ * Consumed by: web frontend, MCP worker, OSS CLI, and the API worker.
6
4
  */
7
5
 
6
+ import type { z } from "zod";
7
+
8
+ export type {
9
+ SourceType,
10
+ SourceDiscovery,
11
+ SourceFetchPriority,
12
+ } from "@buildinternet/releases-core/source-enums";
13
+ import type {
14
+ MediaItemSchema,
15
+ PaginationSchema,
16
+ StatsSchema,
17
+ ErrorResponseSchema,
18
+ ReleaseTypeSchema,
19
+ ReleaseItemSchema,
20
+ ReleaseSummaryItemSchema,
21
+ OverviewPageItemSchema,
22
+ } from "./schemas/shared.js";
23
+ import type {
24
+ OrgListItemSchema,
25
+ OrgListResponseSchema,
26
+ OrgAccountItemSchema,
27
+ OrgAccountsResponseSchema,
28
+ OrgTagsResponseSchema,
29
+ CreateOrgBodySchema,
30
+ UpdateOrgBodySchema,
31
+ OrgDetailSchema,
32
+ } from "./schemas/orgs.js";
33
+ import type {
34
+ SourceListItemSchema,
35
+ SourceWithOrgSchema,
36
+ SourceListResponseSchema,
37
+ SourceListResultSchema,
38
+ SourceDetailSchema,
39
+ SourceMutationResponseSchema,
40
+ SourceOrgRefSchema,
41
+ SourcePatchInputSchema,
42
+ CreateSourceBodySchema,
43
+ ChangelogFileSummarySchema,
44
+ SourceChangelogResponseSchema,
45
+ } from "./schemas/sources.js";
46
+ import type {
47
+ ProductRowSchema,
48
+ ProductListItemSchema,
49
+ ProductListResponseSchema,
50
+ ProductDetailSourceSchema,
51
+ ProductDetailSchema,
52
+ CreateProductBodySchema,
53
+ UpdateProductBodySchema,
54
+ AdoptProductBodySchema,
55
+ ProductAdoptResultSchema,
56
+ ProductAdoptDryRunSchema,
57
+ ProductAdoptResponseSchema,
58
+ ProductDeleteResponseSchema,
59
+ } from "./schemas/products.js";
60
+
61
+ export {
62
+ MediaItemSchema,
63
+ PaginationSchema,
64
+ ListResponseSchema,
65
+ StatsSchema,
66
+ ErrorResponseSchema,
67
+ ReleaseTypeSchema,
68
+ ReleaseItemSchema,
69
+ ReleaseSummaryItemSchema,
70
+ OverviewPageItemSchema,
71
+ CategorySchema,
72
+ } from "./schemas/shared.js";
73
+ export {
74
+ OrgListItemSchema,
75
+ OrgListResponseSchema,
76
+ OrgAccountItemSchema,
77
+ OrgAccountsResponseSchema,
78
+ OrgTagsResponseSchema,
79
+ CreateOrgBodySchema,
80
+ UpdateOrgBodySchema,
81
+ OrgDetailSchema,
82
+ } from "./schemas/orgs.js";
83
+ export {
84
+ SourceListItemSchema,
85
+ SourceWithOrgSchema,
86
+ SourceListResponseSchema,
87
+ SourceListResultSchema,
88
+ SourceDetailSchema,
89
+ SourceMutationResponseSchema,
90
+ SourceOrgRefSchema,
91
+ SourcePatchInputSchema,
92
+ CreateSourceBodySchema,
93
+ ChangelogFileSummarySchema,
94
+ SourceChangelogResponseSchema,
95
+ } from "./schemas/sources.js";
96
+ export {
97
+ ProductRowSchema,
98
+ ProductListItemSchema,
99
+ ProductListResponseSchema,
100
+ ProductDetailSourceSchema,
101
+ ProductDetailSchema,
102
+ CreateProductBodySchema,
103
+ UpdateProductBodySchema,
104
+ AdoptProductBodySchema,
105
+ ProductAdoptResultSchema,
106
+ ProductAdoptDryRunSchema,
107
+ ProductAdoptResponseSchema,
108
+ ProductDeleteResponseSchema,
109
+ } from "./schemas/products.js";
110
+
8
111
  // ── Media ──
9
112
 
10
- export interface MediaItem {
11
- type: "image" | "video" | "gif";
12
- url: string;
13
- alt?: string;
14
- r2Url?: string;
15
- }
113
+ export type MediaItem = z.infer<typeof MediaItemSchema>;
16
114
 
17
115
  // ── Stats ──
18
116
 
19
- export interface Stats {
20
- orgs: number;
21
- sources: number;
22
- releases: number;
23
- products: number;
24
- }
117
+ export type Stats = z.infer<typeof StatsSchema>;
25
118
 
26
119
  // ── Pagination ──
27
120
 
28
- export interface Pagination {
29
- page: number;
30
- pageSize: number;
31
- returned: number;
32
- totalItems?: number;
33
- totalPages?: number;
34
- hasMore: boolean;
35
- }
121
+ export type Pagination = z.infer<typeof PaginationSchema>;
36
122
 
37
123
  export interface ListResponse<T> {
38
124
  items: T[];
39
125
  pagination: Pagination;
40
126
  }
41
127
 
128
+ export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
129
+
42
130
  // ── Sitemap (bulk URL emission) ──
43
131
 
44
132
  export interface SitemapPayload {
@@ -49,225 +137,54 @@ export interface SitemapPayload {
49
137
 
50
138
  // ── Organizations ──
51
139
 
52
- export interface OrgListItem {
53
- slug: string;
54
- name: string;
55
- domain: string | null;
56
- avatarUrl: string | null;
57
- githubHandle: string | null;
58
- sourceCount: number;
59
- releaseCount: number;
60
- recentReleaseCount: number;
61
- lastActivity: string | null;
62
- topProducts: string[];
63
- sparkline: number[];
64
- }
65
-
66
- export type OrgListResponse = ListResponse<OrgListItem>;
140
+ export type OrgListItem = z.infer<typeof OrgListItemSchema>;
141
+ export type OrgListResponse = z.infer<typeof OrgListResponseSchema>;
142
+ export type OrgAccountItem = z.infer<typeof OrgAccountItemSchema>;
143
+ export type OrgAccountsResponse = z.infer<typeof OrgAccountsResponseSchema>;
144
+ export type OrgTagsResponse = z.infer<typeof OrgTagsResponseSchema>;
145
+ export type CreateOrgBody = z.infer<typeof CreateOrgBodySchema>;
146
+ export type UpdateOrgBody = z.infer<typeof UpdateOrgBodySchema>;
67
147
 
68
- export interface OrgAccountItem {
69
- platform: string;
70
- handle: string;
71
- }
72
-
73
- export type OrgAccountsResponse = ListResponse<OrgAccountItem>;
74
- export type OrgTagsResponse = ListResponse<string>;
75
-
76
- export interface OrgDetail {
77
- id?: string;
78
- slug: string;
79
- name: string;
80
- domain: string | null;
81
- description?: string | null;
82
- category?: string | null;
83
- avatarUrl: string | null;
84
- tags?: string[];
85
- sourceCount: number;
86
- releaseCount: number;
87
- releasesLast30Days: number;
88
- avgReleasesPerWeek: number;
89
- lastFetchedAt: string | null;
90
- lastPolledAt: string | null;
91
- trackingSince: string;
92
- aliases?: string[];
93
- accounts: OrgAccountItem[];
94
- products: Array<{
95
- id: string;
96
- slug: string;
97
- name: string;
98
- url: string | null;
99
- description: string | null;
100
- sourceCount: number;
101
- }>;
102
- sources: SourceListItem[];
103
- overview?: OverviewPageItem | null;
104
- playbook?: { scope: "playbook"; content: string; updatedAt: string } | null;
105
- }
148
+ export type OrgDetail = z.infer<typeof OrgDetailSchema>;
106
149
 
107
150
  // ── Sources ──
108
151
 
109
- export interface SourceListItem {
110
- slug: string;
111
- name: string;
112
- type: string;
113
- url?: string;
114
- orgSlug?: string | null;
115
- releaseCount: number;
116
- latestVersion: string | null;
117
- latestDate: string | null;
118
- latestAddedAt?: string | null;
119
- isPrimary?: boolean;
120
- isHidden?: boolean;
121
- /**
122
- * How the row was created. Optional on the wire so older API responses
123
- * (mid-deploy or pinned old workers) degrade gracefully — consumers that
124
- * see `undefined` should treat it as `"curated"`.
125
- */
126
- discovery?: "curated" | "agent" | "on_demand";
127
- fetchPriority?: "normal" | "low" | "paused" | null;
128
- lastFetchedAt?: string | null;
129
- lastPolledAt?: string | null;
130
- changeDetectedAt?: string | null;
131
- consecutiveNoChange?: number | null;
132
- consecutiveErrors?: number | null;
133
- nextFetchAfter?: string | null;
134
- medianGapDays?: number | null;
135
- lastRetieredAt?: string | null;
136
- metadata?: string | null;
137
- productName?: string | null;
138
- productSlug?: string | null;
139
- }
140
-
141
- /**
142
- * Canonical shape returned by GET /v1/sources (list) and used by both
143
- * local-mode queries and the remote API client. Superset of SourceListItem
144
- * with required fields (id, orgName, orgSlug) that the CLI `list` command needs.
145
- */
146
- export interface SourceWithOrg {
147
- id: string;
148
- name: string;
149
- slug: string;
150
- type: string;
151
- url: string;
152
- orgName: string | null;
153
- orgSlug: string | null;
154
- productName: string | null;
155
- productSlug: string | null;
156
- isPrimary: boolean;
157
- isHidden: boolean | null;
158
- discovery?: "curated" | "agent" | "on_demand";
159
- metadata: string | null;
160
- releaseCount: number;
161
- latestVersion: string | null;
162
- latestDate: string | null;
163
- lastFetchedAt: string | null;
164
- lastPolledAt: string | null;
165
- fetchPriority: string | null;
166
- changeDetectedAt: string | null;
167
- consecutiveNoChange: number | null;
168
- consecutiveErrors: number | null;
169
- nextFetchAfter: string | null;
170
- medianGapDays: number | null;
171
- lastRetieredAt: string | null;
172
- }
173
-
174
- /** Fields accepted by PATCH /v1/sources/:slug. */
175
- export interface SourcePatchInput {
176
- name?: string;
177
- url?: string;
178
- type?: string;
179
- slug?: string;
180
- metadata?: string;
181
- orgId?: string | null;
182
- productId?: string | null;
183
- lastFetchedAt?: string | null;
184
- lastContentHash?: string | null;
185
- fetchPriority?: string;
186
- consecutiveNoChange?: number;
187
- consecutiveErrors?: number;
188
- nextFetchAfter?: string | null;
189
- isPrimary?: boolean;
190
- isHidden?: boolean;
191
- changeDetectedAt?: string | null;
192
- lastPolledAt?: string | null;
193
- }
194
-
195
- /** Lightweight summary of a changelog file — used for the file index. */
196
- export interface ChangelogFileSummary {
197
- path: string;
198
- filename: string;
199
- url: string;
200
- bytes: number;
201
- fetchedAt: string;
202
- }
203
-
204
- export interface SourceChangelogResponse {
205
- path: string;
206
- filename: string;
207
- url: string;
208
- rawUrl: string;
209
- content: string;
210
- bytes: number;
211
- fetchedAt: string;
212
- /** Character offset of the first character in `content` within the full file. */
213
- offset: number;
214
- /** The limit (in chars) that was applied to produce this slice. */
215
- limit: number;
216
- /** Next offset to request for the next slice, or null if `content` is the tail. */
217
- nextOffset: number | null;
218
- /** Total length of the full file in characters. */
219
- totalChars: number;
220
- /** Requested token budget when in token mode (cl100k_base). */
221
- tokens?: number;
222
- /** Encoded token count of the returned `content`. Set in token mode. */
223
- sliceTokens?: number;
224
- /** Full-file token count (cl100k_base). Always populated. */
225
- totalTokens: number;
226
- /** True when the upstream file exceeded the 1MB cap and content was sliced. */
227
- truncated: boolean;
228
- /** Byte offset where the file was truncated, or null when not truncated. */
229
- truncatedAt: number | null;
230
- /**
231
- * Index of every changelog file tracked for this source (root plus any
232
- * discovered per-package files). Always present even for single-file
233
- * sources so clients can lazily render a file picker.
234
- */
235
- files: ChangelogFileSummary[];
236
- }
237
-
238
- export interface SourceDetail {
239
- slug: string;
240
- name: string;
241
- type: string;
242
- url: string;
243
- /**
244
- * Hidden sources are reachable by direct URL but excluded from listings,
245
- * sitemap, and AI features. Set on on-demand lookups and admin-suppressed
246
- * rows; absent on canonical curated/agent sources.
247
- */
248
- isHidden?: boolean;
249
- /**
250
- * Pairs with `isHidden` to distinguish admin-suppressed rows (`curated` /
251
- * `agent` + hidden) from rows materialized by `/v1/lookups` (`on_demand`).
252
- * Optional for graceful degradation against older API responses.
253
- */
254
- discovery?: "curated" | "agent" | "on_demand";
255
- changelogUrl?: string | null;
256
- hasChangelogFile?: boolean;
257
- org: { slug: string; name: string } | null;
258
- releaseCount: number;
259
- releasesLast30Days: number;
260
- avgReleasesPerWeek: number;
261
- latestVersion: string | null;
262
- latestDate: string | null;
263
- lastFetchedAt: string | null;
264
- lastPolledAt: string | null;
265
- trackingSince: string;
266
- releases: ReleaseItem[];
267
- pagination: { page: number; pageSize: number; totalPages: number; totalItems: number };
268
- summaries: {
269
- rolling: ReleaseSummaryItem | null;
270
- monthly: ReleaseSummaryItem[];
152
+ export type SourceListItem = z.infer<typeof SourceListItemSchema>;
153
+ export type SourceWithOrg = z.infer<typeof SourceWithOrgSchema>;
154
+ export type SourceListResponse = z.infer<typeof SourceListResponseSchema>;
155
+ export type SourceListResult = z.infer<typeof SourceListResultSchema>;
156
+ export type SourcePatchInput = z.infer<typeof SourcePatchInputSchema>;
157
+ export type CreateSourceBody = z.infer<typeof CreateSourceBodySchema>;
158
+ export type ChangelogFileSummary = z.infer<typeof ChangelogFileSummarySchema>;
159
+ export type SourceChangelogResponse = z.infer<typeof SourceChangelogResponseSchema>;
160
+ export type SourceDetail = z.infer<typeof SourceDetailSchema>;
161
+ export type SourceMutationResponse = z.infer<typeof SourceMutationResponseSchema>;
162
+ export type SourceOrgRef = z.infer<typeof SourceOrgRefSchema>;
163
+
164
+ // ── Admin telemetry: orgs rollup ──
165
+
166
+ export interface OrgsRollupRow {
167
+ /** Org slug, or "—" for sources without an org. */
168
+ orgSlug: string;
169
+ sourceCount: number;
170
+ /** Sources with no release on file or `latestDate` older than `staleDays`. */
171
+ staleCount: number;
172
+ /** Most-recent release across all of the org's sources, or null. */
173
+ mostRecentRelease: string | null;
174
+ mostRecentAgeDays: number | null;
175
+ /** True iff every source in the org is stale (and the org has at least one source). */
176
+ allStale: boolean;
177
+ }
178
+
179
+ export interface OrgsRollupResponse extends ListResponse<OrgsRollupRow> {
180
+ meta: {
181
+ /** Stale cutoff used server-side, in days. */
182
+ staleDays: number;
183
+ totalOrgs: number;
184
+ /** Orgs where every source is stale. */
185
+ dormantOrgs: number;
186
+ /** Orgs with at least one stale source. */
187
+ anyStaleOrgs: number;
271
188
  };
272
189
  }
273
190
 
@@ -278,20 +195,9 @@ export interface SourceDetail {
278
195
  * Optional on the wire so older API responses (mid-deploy or pinned old workers)
279
196
  * degrade gracefully — consumers that see `undefined` should treat it as `"feature"`.
280
197
  */
281
- export type ReleaseType = "feature" | "rollup";
198
+ export type ReleaseType = z.infer<typeof ReleaseTypeSchema>;
282
199
 
283
- export interface ReleaseItem {
284
- id?: string;
285
- version: string | null;
286
- title: string;
287
- summary: string;
288
- content?: string;
289
- publishedAt: string | null;
290
- url: string | null;
291
- media?: MediaItem[];
292
- /** Release type. See {@link ReleaseType}. */
293
- type?: ReleaseType;
294
- }
200
+ export type ReleaseItem = z.infer<typeof ReleaseItemSchema>;
295
201
 
296
202
  export interface ReleaseDetail {
297
203
  id: string;
@@ -325,14 +231,7 @@ export type ReleaseCoverageResponse =
325
231
  | { role: "coverage"; canonical: ReleaseCoverageRow; covers: [] }
326
232
  | { role: "canonical"; canonical: null; covers: ReleaseCoverageRow[] };
327
233
 
328
- export interface ReleaseSummaryItem {
329
- year?: number | null;
330
- month?: number | null;
331
- windowDays?: number | null;
332
- summary: string;
333
- releaseCount: number;
334
- generatedAt: string;
335
- }
234
+ export type ReleaseSummaryItem = z.infer<typeof ReleaseSummaryItemSchema>;
336
235
 
337
236
  // ── Search ──
338
237
 
@@ -470,6 +369,50 @@ export interface SearchChunkHit {
470
369
  score: number;
471
370
  }
472
371
 
372
+ // ── Lookup by domain (entity resolution) ──
373
+
374
+ /**
375
+ * Returned by GET /v1/lookups/by-domain. Pure resolution: the server
376
+ * normalizes the input domain, exact-matches against
377
+ * `organizations.domain` (primary) and `domain_aliases.domain` (alias for
378
+ * either an org or a product), and returns whatever it finds. Unknown
379
+ * domains 404 — there is no on-demand probing for domains, unlike the
380
+ * GitHub coordinate path.
381
+ */
382
+ export interface DomainLookupResponse {
383
+ /** Normalized domain that was matched (lowercased, no scheme/path/www). */
384
+ domain: string;
385
+ /**
386
+ * Owning organization, if one exists. `matchedVia` distinguishes a hit
387
+ * on `organizations.domain` (`"primary"`) from an alias hit
388
+ * (`"alias"`) so the UI can render the difference.
389
+ */
390
+ org: {
391
+ id: string;
392
+ slug: string;
393
+ name: string;
394
+ domain: string | null;
395
+ description: string | null;
396
+ category: string | null;
397
+ avatarUrl: string | null;
398
+ matchedVia: "primary" | "alias";
399
+ } | null;
400
+ /**
401
+ * Products whose alias points at this domain. Multiple are possible
402
+ * when an org owns several products that all set the same alias —
403
+ * unusual, but allowed.
404
+ */
405
+ products: Array<{
406
+ id: string;
407
+ slug: string;
408
+ name: string;
409
+ orgId: string;
410
+ orgSlug: string;
411
+ orgName: string;
412
+ category: string | null;
413
+ }>;
414
+ }
415
+
473
416
  // ── Lookup (on-demand GitHub index) ──
474
417
 
475
418
  export type LookupStatus = "indexed" | "existing" | "empty" | "not_found" | "deferred";
@@ -508,6 +451,21 @@ export interface LookupResultPayload {
508
451
 
509
452
  export interface UnifiedSearchResponse {
510
453
  query: string;
454
+ /**
455
+ * Normalized form of `?domain=` when the caller passed it. The server
456
+ * does the normalization once and echoes the canonical form back so
457
+ * clients don't have to re-normalize for analytics or display. Absent
458
+ * when no domain filter was applied.
459
+ */
460
+ domain?: string;
461
+ /**
462
+ * Outcome of the `?domain=` resolution step. `"matched"` means the
463
+ * domain resolved to a known org and results below are scoped to it.
464
+ * `"not_found"` means the domain didn't match anything; in that case
465
+ * orgs/catalog/releases will all be empty arrays — the caller can
466
+ * distinguish "no hits in scope" from "scope didn't exist."
467
+ */
468
+ domainStatus?: "matched" | "not_found";
511
469
  orgs: SearchOrgHit[];
512
470
  /** Products and standalone sources folded into a single list. */
513
471
  catalog: SearchCatalogHit[];
@@ -532,16 +490,7 @@ export interface UnifiedSearchResponse {
532
490
 
533
491
  // ── Overview Pages ──
534
492
 
535
- export interface OverviewPageItem {
536
- scope: "org" | "product";
537
- orgSlug?: string | null;
538
- productSlug?: string | null;
539
- content: string;
540
- releaseCount: number;
541
- lastContributingReleaseAt: string | null;
542
- generatedAt: string;
543
- updatedAt: string;
544
- }
493
+ export type OverviewPageItem = z.infer<typeof OverviewPageItemSchema>;
545
494
 
546
495
  /** @deprecated Use OverviewPageItem */
547
496
  export type KnowledgePageItem = OverviewPageItem;
@@ -673,39 +622,20 @@ export interface OrgReleasesResponse {
673
622
 
674
623
  // ── Products ──
675
624
 
676
- export interface ProductListItem {
677
- id: string;
678
- name: string;
679
- slug: string;
680
- orgId: string;
681
- url: string | null;
682
- description: string | null;
683
- category: string | null;
684
- createdAt: string;
685
- sourceCount: number;
686
- }
687
-
688
- export type ProductListResponse = ListResponse<ProductListItem>;
689
-
690
- export interface ProductDetail {
691
- id: string;
692
- name: string;
693
- slug: string;
694
- orgId: string;
695
- url: string | null;
696
- description: string | null;
697
- category: string | null;
698
- createdAt: string;
699
- sources: Array<{ id: string; slug: string; name: string; type: string; url: string }>;
700
- tags: string[];
701
- }
702
-
703
- export interface ProductAdoptResult {
704
- product: ProductDetail;
705
- sourcesMoved: number;
706
- accountsMoved: number;
707
- sourceOrgDeleted: string;
708
- }
625
+ // `Category` lives in @buildinternet/releases-core/categories — import it from
626
+ // there. CategorySchema is re-exported here for OpenAPI / Zod consumers only.
627
+ export type ProductRow = z.infer<typeof ProductRowSchema>;
628
+ export type ProductListItem = z.infer<typeof ProductListItemSchema>;
629
+ export type ProductListResponse = z.infer<typeof ProductListResponseSchema>;
630
+ export type ProductDetailSource = z.infer<typeof ProductDetailSourceSchema>;
631
+ export type ProductDetail = z.infer<typeof ProductDetailSchema>;
632
+ export type CreateProductBody = z.infer<typeof CreateProductBodySchema>;
633
+ export type UpdateProductBody = z.infer<typeof UpdateProductBodySchema>;
634
+ export type AdoptProductBody = z.infer<typeof AdoptProductBodySchema>;
635
+ export type ProductAdoptResult = z.infer<typeof ProductAdoptResultSchema>;
636
+ export type ProductAdoptDryRun = z.infer<typeof ProductAdoptDryRunSchema>;
637
+ export type ProductAdoptResponse = z.infer<typeof ProductAdoptResponseSchema>;
638
+ export type ProductDeleteResponse = z.infer<typeof ProductDeleteResponseSchema>;
709
639
 
710
640
  // ── Taxonomy (categories + tags) ──
711
641
 
@@ -939,3 +869,23 @@ export interface EmbedStatusResponse {
939
869
  };
940
870
  chunks: { total: number; embedded: number; unembedded: number };
941
871
  }
872
+
873
+ // ── Evaluate (URL recommendation) ──
874
+
875
+ /**
876
+ * Returned verbatim by GET /v1/evaluate. Lives in the public api-types package
877
+ * so external consumers (CLI, third-party clients) can import the shape
878
+ * without depending on worker-internal AI helpers.
879
+ */
880
+ export interface EvaluationResult {
881
+ recommendedMethod: "feed" | "github" | "markdown" | "scrape" | "crawl";
882
+ recommendedUrl: string;
883
+ feedUrl?: string;
884
+ feedType?: "rss" | "atom" | "jsonfeed";
885
+ githubRepo?: string;
886
+ pageStructure: "single-page" | "index" | "unknown";
887
+ alternatives: Array<{ url: string; method: string; note: string }>;
888
+ confidence: "high" | "medium" | "low";
889
+ provider?: string;
890
+ notes?: string;
891
+ }
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import { CategorySchema, ListResponseSchema, OverviewPageItemSchema } from "./shared.js";
3
+ import { SourceListItemSchema } from "./sources.js";
4
+ import { ProductListItemSchema } from "./products.js";
5
+
6
+ export const OrgListItemSchema = z.object({
7
+ id: z.string(),
8
+ slug: z.string(),
9
+ name: z.string(),
10
+ domain: z.string().nullable(),
11
+ description: z.string().nullable(),
12
+ category: CategorySchema.nullable(),
13
+ avatarUrl: z.string().nullable(),
14
+ sourceCount: z.number().int().min(0),
15
+ releaseCount: z.number().int().min(0),
16
+ recentReleaseCount: z.number().int().min(0),
17
+ lastActivity: z.string().nullable(),
18
+ topProducts: z.array(z.string()),
19
+ sparkline: z.array(z.number().int().min(0)).length(30),
20
+ });
21
+
22
+ export const OrgListResponseSchema = ListResponseSchema(OrgListItemSchema);
23
+
24
+ export const OrgAccountItemSchema = z.object({
25
+ platform: z.string(),
26
+ handle: z.string(),
27
+ });
28
+
29
+ export const OrgAccountsResponseSchema = ListResponseSchema(OrgAccountItemSchema);
30
+ export const OrgTagsResponseSchema = ListResponseSchema(z.string());
31
+
32
+ export const CreateOrgBodySchema = z.object({
33
+ name: z.string().min(1),
34
+ slug: z.string().optional(),
35
+ domain: z.string().optional(),
36
+ description: z.string().optional(),
37
+ category: CategorySchema.optional(),
38
+ tags: z.array(z.string()).optional(),
39
+ });
40
+
41
+ export const UpdateOrgBodySchema = z.object({
42
+ name: z.string().optional(),
43
+ slug: z.string().optional(),
44
+ domain: z.string().nullable().optional(),
45
+ description: z.string().nullable().optional(),
46
+ category: CategorySchema.nullable().optional(),
47
+ tags: z.array(z.string()).optional(),
48
+ aliases: z.array(z.string()).optional(),
49
+ });
50
+
51
+ // Org detail's products query selects a strict subset of `ProductListItem` —
52
+ // no category/orgId/createdAt because the parent already names the org and
53
+ // the org detail UI doesn't surface those columns.
54
+ const OrgDetailProductSchema = ProductListItemSchema.pick({
55
+ id: true,
56
+ slug: true,
57
+ name: true,
58
+ url: true,
59
+ description: true,
60
+ sourceCount: true,
61
+ });
62
+
63
+ const OrgDetailPlaybookSchema = z.object({
64
+ scope: z.literal("playbook"),
65
+ content: z.string(),
66
+ updatedAt: z.string(),
67
+ });
68
+
69
+ export const OrgDetailSchema = z.object({
70
+ id: z.string().optional(),
71
+ slug: z.string(),
72
+ name: z.string(),
73
+ domain: z.string().nullable(),
74
+ description: z.string().nullable().optional(),
75
+ category: CategorySchema.nullable().optional(),
76
+ avatarUrl: z.string().nullable(),
77
+ tags: z.array(z.string()).optional(),
78
+ sourceCount: z.number().int().min(0),
79
+ releaseCount: z.number().int().min(0),
80
+ releasesLast30Days: z.number().int().min(0),
81
+ avgReleasesPerWeek: z.number(),
82
+ lastFetchedAt: z.string().nullable(),
83
+ lastPolledAt: z.string().nullable(),
84
+ trackingSince: z.string(),
85
+ aliases: z.array(z.string()).optional(),
86
+ accounts: z.array(OrgAccountItemSchema),
87
+ products: z.array(OrgDetailProductSchema),
88
+ sources: z.array(SourceListItemSchema),
89
+ overview: OverviewPageItemSchema.nullable().optional(),
90
+ playbook: OrgDetailPlaybookSchema.nullable().optional(),
91
+ });
@@ -0,0 +1,132 @@
1
+ import { z } from "zod";
2
+ import { CategorySchema, ListResponseSchema } from "./shared.js";
3
+ import { SourceTypeSchema } from "./sources.js";
4
+
5
+ /**
6
+ * Raw `products` table row, returned by `POST /v1/products` and
7
+ * `PATCH /v1/products/:slug`. The OSS CLI types these responses against
8
+ * `Product` from `@buildinternet/releases-core/schema` (the drizzle row
9
+ * type), so `embeddedAt` and `deletedAt` stay on the wire even though
10
+ * they're internal columns. `deletedAt` is always `null` on these paths
11
+ * (live rows only); `embeddedAt` reflects when the product was last
12
+ * indexed for semantic search.
13
+ */
14
+ export const ProductRowSchema = z.object({
15
+ id: z.string(),
16
+ name: z.string(),
17
+ slug: z.string(),
18
+ orgId: z.string(),
19
+ url: z.string().nullable(),
20
+ description: z.string().nullable(),
21
+ category: CategorySchema.nullable(),
22
+ createdAt: z.string(),
23
+ embeddedAt: z.string().nullable(),
24
+ deletedAt: z.string().nullable(),
25
+ });
26
+
27
+ /**
28
+ * Per-product row returned by `GET /v1/products`. Adds `sourceCount` to
29
+ * the row shape but omits the internal `embeddedAt` / `deletedAt` columns
30
+ * (the list handler explicitly selects only the user-facing fields).
31
+ */
32
+ export const ProductListItemSchema = z.object({
33
+ id: z.string(),
34
+ name: z.string(),
35
+ slug: z.string(),
36
+ orgId: z.string(),
37
+ url: z.string().nullable(),
38
+ description: z.string().nullable(),
39
+ category: CategorySchema.nullable(),
40
+ createdAt: z.string(),
41
+ sourceCount: z.number().int().min(0),
42
+ });
43
+
44
+ export const ProductListResponseSchema = ListResponseSchema(ProductListItemSchema);
45
+
46
+ /**
47
+ * Embedded source row returned in `ProductDetail.sources`. The detail
48
+ * handler explicitly selects only this small subset; reuses the shared
49
+ * `SourceTypeSchema` enum so source types stay in one place.
50
+ */
51
+ export const ProductDetailSourceSchema = z.object({
52
+ id: z.string(),
53
+ slug: z.string(),
54
+ name: z.string(),
55
+ type: SourceTypeSchema,
56
+ url: z.string(),
57
+ });
58
+
59
+ /**
60
+ * Returned by `GET /v1/products/:identifier` (and the org-scoped twin).
61
+ * Spreads the raw product row and adds `sources`, `tags`, `aliases`.
62
+ */
63
+ export const ProductDetailSchema = ProductRowSchema.extend({
64
+ sources: z.array(ProductDetailSourceSchema),
65
+ tags: z.array(z.string()),
66
+ aliases: z.array(z.string()),
67
+ });
68
+
69
+ /** Body accepted by `POST /v1/products`. */
70
+ export const CreateProductBodySchema = z.object({
71
+ name: z.string().min(1),
72
+ orgId: z.string().optional(),
73
+ orgSlug: z.string().optional(),
74
+ slug: z.string().optional(),
75
+ url: z.string().optional(),
76
+ description: z.string().optional(),
77
+ category: CategorySchema.optional(),
78
+ tags: z.array(z.string()).optional(),
79
+ });
80
+
81
+ /** Body accepted by `PATCH /v1/products/:slug`. */
82
+ export const UpdateProductBodySchema = z.object({
83
+ name: z.string().optional(),
84
+ url: z.string().nullable().optional(),
85
+ description: z.string().nullable().optional(),
86
+ category: CategorySchema.nullable().optional(),
87
+ tags: z.array(z.string()).optional(),
88
+ aliases: z.array(z.string()).optional(),
89
+ });
90
+
91
+ /** Body accepted by `POST /v1/products/adopt`. */
92
+ export const AdoptProductBodySchema = z.object({
93
+ sourceOrgSlug: z.string().min(1),
94
+ targetOrgSlug: z.string().min(1),
95
+ slug: z.string().optional(),
96
+ url: z.string().optional(),
97
+ dryRun: z.boolean().optional(),
98
+ });
99
+
100
+ /** Live (non-dryRun) result from `POST /v1/products/adopt`. */
101
+ export const ProductAdoptResultSchema = z.object({
102
+ product: ProductRowSchema,
103
+ sourcesMoved: z.number().int().min(0),
104
+ accountsMoved: z.number().int().min(0),
105
+ sourceOrgDeleted: z.string(),
106
+ });
107
+
108
+ /** Dry-run preview from `POST /v1/products/adopt` with `dryRun: true`. */
109
+ export const ProductAdoptDryRunSchema = z.object({
110
+ dryRun: z.literal(true),
111
+ product: z.object({
112
+ name: z.string(),
113
+ slug: z.string(),
114
+ url: z.string().nullable(),
115
+ orgSlug: z.string(),
116
+ }),
117
+ sourcesToMove: z.array(z.string()),
118
+ sourceOrgToDelete: z.string(),
119
+ });
120
+
121
+ /** Response shape returned by `POST /v1/products/adopt` (union of live + dry-run). */
122
+ export const ProductAdoptResponseSchema = z.union([
123
+ ProductAdoptResultSchema,
124
+ ProductAdoptDryRunSchema,
125
+ ]);
126
+
127
+ /** Response shape returned by `DELETE /v1/products/:identifier`. */
128
+ export const ProductDeleteResponseSchema = z.object({
129
+ deleted: z.literal(true),
130
+ hard: z.literal(true).optional(),
131
+ deletedAt: z.string().optional(),
132
+ });
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { RELEASE_TYPES } from "@buildinternet/releases-core/schema";
3
+ import { CATEGORIES } from "@buildinternet/releases-core/categories";
4
+
5
+ export const CategorySchema = z.enum(CATEGORIES);
6
+
7
+ export const MediaItemSchema = z.object({
8
+ type: z.enum(["image", "video", "gif"]),
9
+ url: z.string(),
10
+ alt: z.string().optional(),
11
+ r2Url: z.string().optional(),
12
+ });
13
+
14
+ export const PaginationSchema = z.object({
15
+ page: z.number().int().min(1),
16
+ pageSize: z.number().int().min(1),
17
+ returned: z.number().int().min(0),
18
+ totalItems: z.number().int().min(0).optional(),
19
+ totalPages: z.number().int().min(0).optional(),
20
+ hasMore: z.boolean(),
21
+ });
22
+
23
+ export const ListResponseSchema = <T extends z.ZodTypeAny>(item: T) =>
24
+ z.object({
25
+ items: z.array(item),
26
+ pagination: PaginationSchema,
27
+ });
28
+
29
+ export const ErrorResponseSchema = z.object({
30
+ error: z.string(),
31
+ message: z.string(),
32
+ });
33
+
34
+ export const StatsSchema = z.object({
35
+ orgs: z.number().int().min(0),
36
+ sources: z.number().int().min(0),
37
+ releases: z.number().int().min(0),
38
+ products: z.number().int().min(0),
39
+ });
40
+
41
+ export const ReleaseTypeSchema = z.enum(RELEASE_TYPES);
42
+
43
+ export const ReleaseItemSchema = z.object({
44
+ id: z.string().optional(),
45
+ version: z.string().nullable(),
46
+ title: z.string(),
47
+ summary: z.string(),
48
+ content: z.string().optional(),
49
+ publishedAt: z.string().nullable(),
50
+ url: z.string().nullable(),
51
+ media: z.array(MediaItemSchema).optional(),
52
+ // `.optional()` — older API responses (mid-deploy or pinned old workers)
53
+ // may omit `type`; consumers should treat `undefined` as `"feature"`.
54
+ type: ReleaseTypeSchema.optional(),
55
+ // Pre-release flag (alpha/beta/rc/preview/nightly). Computed at ingest —
56
+ // GitHub uses the API's authoritative `prerelease` field; other adapters
57
+ // fall back to a SemVer-prerelease regex. `.optional()` for the same
58
+ // older-response degrade-gracefully reason as `type`.
59
+ prerelease: z.boolean().optional(),
60
+ });
61
+
62
+ export const ReleaseSummaryItemSchema = z.object({
63
+ year: z.number().int().nullable().optional(),
64
+ month: z.number().int().nullable().optional(),
65
+ windowDays: z.number().int().nullable().optional(),
66
+ summary: z.string(),
67
+ releaseCount: z.number().int().min(0),
68
+ generatedAt: z.string(),
69
+ });
70
+
71
+ export const OverviewPageItemSchema = z.object({
72
+ scope: z.enum(["org", "product"]),
73
+ orgSlug: z.string().nullable().optional(),
74
+ productSlug: z.string().nullable().optional(),
75
+ content: z.string(),
76
+ releaseCount: z.number().int().min(0),
77
+ lastContributingReleaseAt: z.string().nullable(),
78
+ generatedAt: z.string(),
79
+ updatedAt: z.string(),
80
+ });
@@ -0,0 +1,267 @@
1
+ import { z } from "zod";
2
+ import {
3
+ SOURCE_TYPES,
4
+ SOURCE_DISCOVERY,
5
+ SOURCE_FETCH_PRIORITIES,
6
+ } from "@buildinternet/releases-core/source-enums";
7
+ import {
8
+ ListResponseSchema,
9
+ PaginationSchema,
10
+ ReleaseItemSchema,
11
+ ReleaseSummaryItemSchema,
12
+ } from "./shared.js";
13
+
14
+ export const SourceTypeSchema = z.enum(SOURCE_TYPES);
15
+ export const SourceDiscoverySchema = z.enum(SOURCE_DISCOVERY);
16
+ export const SourceFetchPrioritySchema = z.enum(SOURCE_FETCH_PRIORITIES);
17
+
18
+ /**
19
+ * Per-source row used in list/embedded contexts (e.g. nested inside OrgDetail).
20
+ * Most fields are optional — embedded shapes (org detail) only carry the
21
+ * subset needed for that view.
22
+ */
23
+ export const SourceListItemSchema = z.object({
24
+ id: z.string().optional(),
25
+ slug: z.string(),
26
+ name: z.string(),
27
+ type: SourceTypeSchema,
28
+ url: z.string().optional(),
29
+ // `orgSlug` is populated on the standalone `/v1/sources` list path; the
30
+ // `OrgDetail.sources` embedded path omits it (the parent already names the org).
31
+ orgSlug: z.string().nullable().optional(),
32
+ releaseCount: z.number().int().min(0),
33
+ latestVersion: z.string().nullable(),
34
+ latestDate: z.string().nullable(),
35
+ latestAddedAt: z.string().nullable().optional(),
36
+ isPrimary: z.boolean().optional(),
37
+ isHidden: z.boolean().optional(),
38
+ /**
39
+ * How the row was created. Optional on the wire so older API responses
40
+ * (mid-deploy or pinned old workers) degrade gracefully — consumers that
41
+ * see `undefined` should treat it as `"curated"`.
42
+ */
43
+ discovery: SourceDiscoverySchema.optional(),
44
+ fetchPriority: SourceFetchPrioritySchema.nullable().optional(),
45
+ lastFetchedAt: z.string().nullable().optional(),
46
+ lastPolledAt: z.string().nullable().optional(),
47
+ changeDetectedAt: z.string().nullable().optional(),
48
+ consecutiveNoChange: z.number().int().nullable().optional(),
49
+ consecutiveErrors: z.number().int().nullable().optional(),
50
+ nextFetchAfter: z.string().nullable().optional(),
51
+ medianGapDays: z.number().nullable().optional(),
52
+ lastRetieredAt: z.string().nullable().optional(),
53
+ metadata: z.string().nullable().optional(),
54
+ productName: z.string().nullable().optional(),
55
+ productSlug: z.string().nullable().optional(),
56
+ });
57
+
58
+ /**
59
+ * Canonical row returned by `GET /v1/sources` (list). Superset of
60
+ * `SourceListItem` with required `id` / `orgName` / `orgSlug` fields used by
61
+ * the CLI `list` command.
62
+ */
63
+ export const SourceWithOrgSchema = z.object({
64
+ id: z.string(),
65
+ name: z.string(),
66
+ slug: z.string(),
67
+ type: SourceTypeSchema,
68
+ url: z.string(),
69
+ orgName: z.string().nullable(),
70
+ orgSlug: z.string().nullable(),
71
+ productName: z.string().nullable(),
72
+ productSlug: z.string().nullable(),
73
+ isPrimary: z.boolean(),
74
+ isHidden: z.boolean().nullable(),
75
+ discovery: SourceDiscoverySchema.optional(),
76
+ metadata: z.string().nullable(),
77
+ releaseCount: z.number().int().min(0),
78
+ latestVersion: z.string().nullable(),
79
+ latestDate: z.string().nullable(),
80
+ lastFetchedAt: z.string().nullable(),
81
+ lastPolledAt: z.string().nullable(),
82
+ fetchPriority: SourceFetchPrioritySchema.nullable(),
83
+ changeDetectedAt: z.string().nullable(),
84
+ consecutiveNoChange: z.number().int().nullable(),
85
+ consecutiveErrors: z.number().int().nullable(),
86
+ nextFetchAfter: z.string().nullable(),
87
+ medianGapDays: z.number().nullable(),
88
+ lastRetieredAt: z.string().nullable(),
89
+ });
90
+
91
+ export const SourceListResponseSchema = ListResponseSchema(SourceWithOrgSchema);
92
+
93
+ /**
94
+ * `GET /v1/sources` returns a bare array by default and an envelope when
95
+ * `?envelope=true` is passed. The OpenAPI spec advertises the union so
96
+ * either client expectation is documented.
97
+ */
98
+ export const SourceListResultSchema = z.union([
99
+ z.array(SourceWithOrgSchema),
100
+ SourceListResponseSchema,
101
+ ]);
102
+
103
+ const SourceDetailSummariesSchema = z.object({
104
+ rolling: ReleaseSummaryItemSchema.nullable(),
105
+ monthly: z.array(ReleaseSummaryItemSchema),
106
+ });
107
+
108
+ /**
109
+ * Resolved attribution block returned alongside a source row. `id` is
110
+ * additive vs the legacy `{ slug, name }` shape — older clients that ignored
111
+ * it still parse cleanly. `productId` / `productSlug` are sibling top-level
112
+ * fields on the source response (kept flat to mirror `orgId`), not nested
113
+ * inside this block.
114
+ */
115
+ export const SourceOrgRefSchema = z.object({
116
+ id: z.string(),
117
+ slug: z.string(),
118
+ name: z.string(),
119
+ });
120
+
121
+ export const SourceDetailSchema = z.object({
122
+ id: z.string(),
123
+ slug: z.string(),
124
+ name: z.string(),
125
+ type: SourceTypeSchema,
126
+ url: z.string(),
127
+ orgId: z.string().nullable(),
128
+ productId: z.string().nullable(),
129
+ productSlug: z.string().nullable(),
130
+ // Hidden sources are reachable by direct URL but excluded from listings,
131
+ // sitemap, and AI features. On-demand lookups and admin-suppressed rows
132
+ // set this true; canonical curated/agent sources leave it false.
133
+ isHidden: z.boolean(),
134
+ // Pairs with `isHidden` to distinguish admin-suppressed rows (`curated` /
135
+ // `agent` + hidden) from rows materialized by `/v1/lookups` (`on_demand`).
136
+ // Optional for graceful degradation against older API responses.
137
+ discovery: SourceDiscoverySchema.optional(),
138
+ isPrimary: z.boolean(),
139
+ metadata: z.string(),
140
+ changelogUrl: z.string().nullable().optional(),
141
+ hasChangelogFile: z.boolean().optional(),
142
+ org: SourceOrgRefSchema.nullable(),
143
+ releaseCount: z.number().int().min(0),
144
+ releasesLast30Days: z.number().int().min(0),
145
+ avgReleasesPerWeek: z.number(),
146
+ latestVersion: z.string().nullable(),
147
+ latestDate: z.string().nullable(),
148
+ lastFetchedAt: z.string().nullable(),
149
+ lastPolledAt: z.string().nullable(),
150
+ trackingSince: z.string(),
151
+ releases: z.array(ReleaseItemSchema),
152
+ pagination: PaginationSchema,
153
+ summaries: SourceDetailSummariesSchema,
154
+ });
155
+
156
+ /**
157
+ * Response returned by `POST /v1/sources` and `PATCH /v1/sources/:slug`.
158
+ *
159
+ * Pre-#794 these endpoints returned the bare drizzle row, leaving callers to
160
+ * round-trip a separate `GET` to confirm the resolved org/product attribution.
161
+ * Now they return the same source row plus a resolved `org { id, slug, name }`
162
+ * block and `productSlug`, so an agent can answer "did the write take?" from
163
+ * the response alone.
164
+ *
165
+ * Wire fields are loosely-typed (`z.unknown()`) for the timestamps/counters
166
+ * that aren't load-bearing for callers — the row is large and we don't want
167
+ * to hand-mirror every column. Stable, agent-relevant fields are typed.
168
+ */
169
+ export const SourceMutationResponseSchema = z.looseObject({
170
+ id: z.string(),
171
+ slug: z.string(),
172
+ name: z.string(),
173
+ type: SourceTypeSchema,
174
+ url: z.string(),
175
+ orgId: z.string().nullable(),
176
+ productId: z.string().nullable(),
177
+ productSlug: z.string().nullable(),
178
+ org: SourceOrgRefSchema.nullable(),
179
+ metadata: z.string().nullable(),
180
+ isPrimary: z.boolean().nullable().optional(),
181
+ isHidden: z.boolean().nullable().optional(),
182
+ discovery: SourceDiscoverySchema.optional(),
183
+ createdAt: z.string().optional(),
184
+ });
185
+
186
+ /** Body accepted by `PATCH /v1/sources/:slug`. */
187
+ export const SourcePatchInputSchema = z.object({
188
+ name: z.string().optional(),
189
+ url: z.string().optional(),
190
+ type: SourceTypeSchema.optional(),
191
+ slug: z.string().optional(),
192
+ metadata: z.string().optional(),
193
+ orgId: z.string().nullable().optional(),
194
+ productId: z.string().nullable().optional(),
195
+ lastFetchedAt: z.string().nullable().optional(),
196
+ lastContentHash: z.string().nullable().optional(),
197
+ fetchPriority: SourceFetchPrioritySchema.optional(),
198
+ consecutiveNoChange: z.number().int().optional(),
199
+ consecutiveErrors: z.number().int().optional(),
200
+ nextFetchAfter: z.string().nullable().optional(),
201
+ isPrimary: z.boolean().optional(),
202
+ isHidden: z.boolean().optional(),
203
+ changeDetectedAt: z.string().nullable().optional(),
204
+ lastPolledAt: z.string().nullable().optional(),
205
+ });
206
+
207
+ /** Body accepted by `POST /v1/sources`. */
208
+ export const CreateSourceBodySchema = z.object({
209
+ name: z.string().min(1),
210
+ url: z.string().min(1),
211
+ type: SourceTypeSchema.optional(),
212
+ slug: z.string().optional(),
213
+ orgId: z.string().optional(),
214
+ orgSlug: z.string().optional(),
215
+ /**
216
+ * Either accepted; `productId` wins when both are supplied. `productSlug`
217
+ * resolves within the requested org (slug uniqueness is per-org). The
218
+ * legacy `productId` ingestion path is preserved for callers that already
219
+ * resolve products client-side; new agent flows should pass `productSlug`.
220
+ */
221
+ productId: z.string().optional(),
222
+ productSlug: z.string().optional(),
223
+ metadata: z.string().optional(),
224
+ isPrimary: z.boolean().optional(),
225
+ });
226
+
227
+ export const ChangelogFileSummarySchema = z.object({
228
+ path: z.string(),
229
+ filename: z.string(),
230
+ url: z.string(),
231
+ bytes: z.number().int().min(0),
232
+ fetchedAt: z.string(),
233
+ });
234
+
235
+ export const SourceChangelogResponseSchema = z.object({
236
+ path: z.string(),
237
+ filename: z.string(),
238
+ url: z.string(),
239
+ rawUrl: z.string(),
240
+ content: z.string(),
241
+ bytes: z.number().int().min(0),
242
+ fetchedAt: z.string(),
243
+ /** Character offset of the first character in `content` within the full file. */
244
+ offset: z.number().int().min(0),
245
+ /** The limit (in chars) that was applied to produce this slice. */
246
+ limit: z.number().int().min(0),
247
+ /** Next offset to request for the next slice, or null if `content` is the tail. */
248
+ nextOffset: z.number().int().nullable(),
249
+ /** Total length of the full file in characters. */
250
+ totalChars: z.number().int().min(0),
251
+ /** Requested token budget when in token mode (cl100k_base). */
252
+ tokens: z.number().int().optional(),
253
+ /** Encoded token count of the returned `content`. Set in token mode. */
254
+ sliceTokens: z.number().int().optional(),
255
+ /** Full-file token count (cl100k_base). Always populated. */
256
+ totalTokens: z.number().int().min(0),
257
+ /** True when the upstream file exceeded the 1MB cap and content was sliced. */
258
+ truncated: z.boolean(),
259
+ /** Byte offset where the file was truncated, or null when not truncated. */
260
+ truncatedAt: z.number().int().nullable(),
261
+ /**
262
+ * Index of every changelog file tracked for this source (root plus any
263
+ * discovered per-package files). Always present even for single-file
264
+ * sources so clients can lazily render a file picker.
265
+ */
266
+ files: z.array(ChangelogFileSummarySchema),
267
+ });