@buildinternet/releases-api-types 0.59.0 → 0.60.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.
Files changed (126) hide show
  1. package/README.md +14 -6
  2. package/dist/api-types.d.ts +1168 -0
  3. package/dist/api-types.d.ts.map +1 -0
  4. package/dist/api-types.js +134 -0
  5. package/dist/schemas/account-profile.d.ts +29 -0
  6. package/dist/schemas/account-profile.d.ts.map +1 -0
  7. package/dist/schemas/account-profile.js +24 -0
  8. package/dist/schemas/ai-models.d.ts +67 -0
  9. package/dist/schemas/ai-models.d.ts.map +1 -0
  10. package/{src/schemas/ai-models.ts → dist/schemas/ai-models.js} +25 -31
  11. package/dist/schemas/api-keys.d.ts +71 -0
  12. package/dist/schemas/api-keys.d.ts.map +1 -0
  13. package/dist/schemas/api-keys.js +32 -0
  14. package/dist/schemas/classifications.d.ts +134 -0
  15. package/dist/schemas/classifications.d.ts.map +1 -0
  16. package/dist/schemas/classifications.js +96 -0
  17. package/dist/schemas/collections.d.ts +605 -0
  18. package/dist/schemas/collections.d.ts.map +1 -0
  19. package/dist/schemas/collections.js +320 -0
  20. package/dist/schemas/datasets.d.ts +34 -0
  21. package/dist/schemas/datasets.d.ts.map +1 -0
  22. package/dist/schemas/datasets.js +11 -0
  23. package/dist/schemas/errors.d.ts +43 -0
  24. package/dist/schemas/errors.d.ts.map +1 -0
  25. package/dist/schemas/errors.js +61 -0
  26. package/dist/schemas/feedback.d.ts +134 -0
  27. package/dist/schemas/feedback.d.ts.map +1 -0
  28. package/dist/schemas/feedback.js +76 -0
  29. package/dist/schemas/follows.d.ts +154 -0
  30. package/dist/schemas/follows.d.ts.map +1 -0
  31. package/dist/schemas/follows.js +42 -0
  32. package/dist/schemas/listing.d.ts +374 -0
  33. package/dist/schemas/listing.d.ts.map +1 -0
  34. package/dist/schemas/listing.js +175 -0
  35. package/dist/schemas/lookups.d.ts +285 -0
  36. package/dist/schemas/lookups.d.ts.map +1 -0
  37. package/{src/schemas/lookups.ts → dist/schemas/lookups.js} +67 -80
  38. package/dist/schemas/marketing-classifier.d.ts +32 -0
  39. package/dist/schemas/marketing-classifier.d.ts.map +1 -0
  40. package/{src/schemas/marketing-classifier.ts → dist/schemas/marketing-classifier.js} +14 -18
  41. package/dist/schemas/orgs.d.ts +950 -0
  42. package/dist/schemas/orgs.d.ts.map +1 -0
  43. package/dist/schemas/orgs.js +473 -0
  44. package/dist/schemas/overviews.d.ts +205 -0
  45. package/dist/schemas/overviews.d.ts.map +1 -0
  46. package/{src/schemas/overviews.ts → dist/schemas/overviews.js} +41 -54
  47. package/dist/schemas/products.d.ts +595 -0
  48. package/dist/schemas/products.d.ts.map +1 -0
  49. package/{src/schemas/products.ts → dist/schemas/products.js} +113 -136
  50. package/dist/schemas/publish-tokens.d.ts +55 -0
  51. package/dist/schemas/publish-tokens.d.ts.map +1 -0
  52. package/{src/schemas/publish-tokens.ts → dist/schemas/publish-tokens.js} +18 -29
  53. package/dist/schemas/recommendations.d.ts +52 -0
  54. package/dist/schemas/recommendations.d.ts.map +1 -0
  55. package/dist/schemas/recommendations.js +58 -0
  56. package/dist/schemas/related.d.ts +331 -0
  57. package/dist/schemas/related.d.ts.map +1 -0
  58. package/dist/schemas/related.js +115 -0
  59. package/dist/schemas/releases.d.ts +623 -0
  60. package/dist/schemas/releases.d.ts.map +1 -0
  61. package/dist/schemas/releases.js +414 -0
  62. package/dist/schemas/resolve.d.ts +259 -0
  63. package/dist/schemas/resolve.d.ts.map +1 -0
  64. package/{src/schemas/resolve.ts → dist/schemas/resolve.js} +2 -5
  65. package/dist/schemas/search.d.ts +475 -0
  66. package/dist/schemas/search.d.ts.map +1 -0
  67. package/dist/schemas/search.js +258 -0
  68. package/dist/schemas/semantic-alert-summary.d.ts +54 -0
  69. package/dist/schemas/semantic-alert-summary.d.ts.map +1 -0
  70. package/dist/schemas/semantic-alert-summary.js +57 -0
  71. package/dist/schemas/semantic-alerts.d.ts +41 -0
  72. package/dist/schemas/semantic-alerts.d.ts.map +1 -0
  73. package/dist/schemas/semantic-alerts.js +42 -0
  74. package/dist/schemas/shared.d.ts +247 -0
  75. package/dist/schemas/shared.d.ts.map +1 -0
  76. package/dist/schemas/shared.js +248 -0
  77. package/dist/schemas/site-notice.d.ts +35 -0
  78. package/dist/schemas/site-notice.d.ts.map +1 -0
  79. package/{src/schemas/site-notice.ts → dist/schemas/site-notice.js} +14 -18
  80. package/dist/schemas/sitemap.d.ts +97 -0
  81. package/dist/schemas/sitemap.d.ts.map +1 -0
  82. package/dist/schemas/sitemap.js +107 -0
  83. package/dist/schemas/sources.d.ts +1145 -0
  84. package/dist/schemas/sources.d.ts.map +1 -0
  85. package/dist/schemas/sources.js +645 -0
  86. package/dist/schemas/stats.d.ts +82 -0
  87. package/dist/schemas/stats.d.ts.map +1 -0
  88. package/dist/schemas/stats.js +59 -0
  89. package/dist/schemas/taxonomy.d.ts +344 -0
  90. package/dist/schemas/taxonomy.d.ts.map +1 -0
  91. package/{src/schemas/taxonomy.ts → dist/schemas/taxonomy.js} +47 -58
  92. package/dist/schemas/webhooks.d.ts +618 -0
  93. package/dist/schemas/webhooks.d.ts.map +1 -0
  94. package/dist/schemas/webhooks.js +191 -0
  95. package/dist/schemas/well-known.d.ts +302 -0
  96. package/dist/schemas/well-known.d.ts.map +1 -0
  97. package/dist/schemas/well-known.js +192 -0
  98. package/dist/schemas/workspace-integrations.d.ts +27 -0
  99. package/dist/schemas/workspace-integrations.d.ts.map +1 -0
  100. package/dist/schemas/workspace-integrations.js +22 -0
  101. package/package.json +16 -5
  102. package/src/api-types.ts +0 -2229
  103. package/src/schemas/account-profile.ts +0 -28
  104. package/src/schemas/ai-models.test.ts +0 -29
  105. package/src/schemas/classifications.ts +0 -109
  106. package/src/schemas/collections.ts +0 -346
  107. package/src/schemas/datasets.ts +0 -31
  108. package/src/schemas/errors.test.ts +0 -55
  109. package/src/schemas/errors.ts +0 -81
  110. package/src/schemas/feedback.ts +0 -56
  111. package/src/schemas/listing.ts +0 -226
  112. package/src/schemas/orgs.ts +0 -533
  113. package/src/schemas/recommendations.ts +0 -39
  114. package/src/schemas/related.ts +0 -126
  115. package/src/schemas/releases.ts +0 -449
  116. package/src/schemas/search.ts +0 -274
  117. package/src/schemas/semantic-alert-summary.ts +0 -65
  118. package/src/schemas/shared.notice.test.ts +0 -36
  119. package/src/schemas/shared.ts +0 -267
  120. package/src/schemas/site-notice.test.ts +0 -67
  121. package/src/schemas/sitemap.ts +0 -117
  122. package/src/schemas/sources.ts +0 -674
  123. package/src/schemas/stats.ts +0 -62
  124. package/src/schemas/well-known.test.ts +0 -264
  125. package/src/schemas/well-known.ts +0 -252
  126. package/src/schemas/workspace-integrations.ts +0 -26
@@ -1,274 +0,0 @@
1
- import { z } from "zod";
2
- import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
3
- import {
4
- AppStoreSourceInfoSchema,
5
- ImportanceScoreSchema,
6
- MediaItemSchema,
7
- OrgStatusSchema,
8
- ReleaseThumbnailSchema,
9
- ReleaseTypeSchema,
10
- VideoSourceInfoSchema,
11
- } from "./shared.js";
12
- import { SourceTypeSchema } from "./sources.js";
13
- import { LookupStatusSchema } from "./lookups.js";
14
- import { CollectionMemberSchema } from "./collections.js";
15
-
16
- /**
17
- * Org hit on the unified `/v1/search` response. The list is built either
18
- * from `searchOrgs` LIKE matches or, when `?domain=` resolves, from a
19
- * single-row "scoped org" projection — both produce the same wire shape.
20
- *
21
- * `category` is loose (`z.string().nullable()`) rather than the canonical
22
- * `CategorySchema` enum because the scoped-org projection passes the raw
23
- * DB column through without going through `resolveCategoryInput`; legacy
24
- * rows may carry deprecated slugs that haven't been migrated yet. Tighten
25
- * once #689 (category overlay backfill) lands across the prod data set.
26
- */
27
- export const SearchOrgHitSchema = z.object({
28
- slug: z.string(),
29
- name: z.string(),
30
- domain: z.string().nullable(),
31
- avatarUrl: z.string().nullable(),
32
- category: z.string().nullable(),
33
- /**
34
- * Org tier (#1947/#2034). Optional for mid-deploy tolerance; absent ⇒
35
- * `"tracked"`. Lets search render the icon-only stub marker (#2031 parity).
36
- */
37
- status: OrgStatusSchema.optional(),
38
- /**
39
- * Org-level alternate domains (product-scoped aliases excluded, sorted),
40
- * for the `+N` hover on the origin domain — the anti-impersonation anchor
41
- * mirrored from the catalog row (#2031/#2034). Optional; absent ⇒ none.
42
- */
43
- aliasDomains: z.array(z.string()).optional(),
44
- });
45
-
46
- /**
47
- * Unified catalog entry — either a product row or a standalone source
48
- * presented as product-shaped. `entryType` discriminates between the two
49
- * row shapes (`"product"` vs `"source"`). `kind` carries the entity
50
- * taxonomy (platform/sdk/mobile/…). `entryType` (not `type`) because
51
- * source rows already carry `type: github|scrape|feed|agent` on the wire.
52
- */
53
- export const SearchCatalogHitSchema = z.object({
54
- slug: z.string(),
55
- name: z.string(),
56
- orgSlug: z.string().nullable(),
57
- orgName: z.string().nullable(),
58
- /** Owning org's avatar URL, for a small inline byline avatar. Products have
59
- * no avatar of their own, so the web row anchors on the org's instead. */
60
- orgAvatarUrl: z.string().nullable().optional(),
61
- // Loose `z.string().nullable()` rather than the canonical `CategorySchema`
62
- // enum because `foldSourcesIntoCatalog` and the catalog-search handlers
63
- // pass `s.productCategory` / `o.category` through without validating
64
- // against `resolveCategoryInput`. Legacy rows may carry deprecated slugs
65
- // that haven't been migrated yet. Tighten once #689 (category overlay
66
- // backfill) lands across the prod data set.
67
- category: z.string().nullable(),
68
- entryType: z.enum(["product", "source"]),
69
- kind: z.enum(KIND_VALUES).nullable().optional(),
70
- sourceSlug: z.string().optional(),
71
- sourceType: SourceTypeSchema.optional(),
72
- });
73
-
74
- export const SearchSourceHitSchema = z.object({
75
- slug: z.string(),
76
- name: z.string(),
77
- type: SourceTypeSchema,
78
- orgSlug: z.string().nullable(),
79
- orgName: z.string().nullable(),
80
- productSlug: z.string().nullable(),
81
- stars: z.number().int().min(0).nullable().optional(),
82
- });
83
-
84
- export const SearchReleaseHitSchema = z.object({
85
- id: z.string(),
86
- sourceSlug: z.string(),
87
- sourceName: z.string(),
88
- // Loose `z.string()` rather than `SourceTypeSchema` because the
89
- // release-hit hydration path reads `sources.type` straight from the DB
90
- // without re-validating; legacy rows occasionally drift from the
91
- // canonical four-value set. Used by the web byline icon.
92
- sourceType: z.string().optional(),
93
- /**
94
- * App Store platform + icon for `type: "appstore"` sources, null/absent
95
- * otherwise. Powers the compact app-update treatment on the search card. #1206
96
- */
97
- appStore: AppStoreSourceInfoSchema.nullable().optional(),
98
- /**
99
- * Video provider tag for `type: "video"` sources, absent otherwise. Powers
100
- * the thumbnail-forward video row on search cards. #video
101
- */
102
- video: VideoSourceInfoSchema.nullable().optional(),
103
- orgSlug: z.string().nullable(),
104
- orgName: z.string().nullable().optional(),
105
- /** Owning product slug — present when the source belongs to a product. Lets
106
- * the web byline link to the product page instead of the source. */
107
- productSlug: z.string().nullable().optional(),
108
- version: z.string().nullable(),
109
- title: z.string(),
110
- summary: z.string(),
111
- titleGenerated: z.string().nullable().optional(),
112
- titleShort: z.string().nullable().optional(),
113
- /**
114
- * The release's upstream source URL, when it has one. Lets the web link
115
- * search/lookup hits straight to the original page instead of the
116
- * noindexed `/release/<id>` stub — see `releaseLinkProps()` in
117
- * `apps/web/src/lib/release-link.ts`. Optional for older servers / mid-deploy
118
- * pin tolerance.
119
- */
120
- url: z.string().nullable().optional(),
121
- /**
122
- * AI-scored importance 1–5 (5=landmark, 1=housekeeping). Null when
123
- * unscored; optional for older servers / mid-deploy pin tolerance.
124
- */
125
- importance: ImportanceScoreSchema,
126
- /**
127
- * Full markdown body. Omitted on list hits by default (summary + media
128
- * cover the card/snippet surfaces). Opt in with `?include_content=true`
129
- * on `/v1/search`. Fetch full body via `GET /v1/releases/:id` otherwise.
130
- */
131
- content: z.string().optional(),
132
- media: z.array(MediaItemSchema).optional(),
133
- publishedAt: z.string().nullable(),
134
- // Hybrid fusion score; absent on pure-lexical results.
135
- score: z.number().optional(),
136
- type: ReleaseTypeSchema.optional(),
137
- coverageCount: z.number().int().min(0).optional(),
138
- });
139
-
140
- /**
141
- * Collection hit on the unified `/v1/search` response. Two origins are folded
142
- * into the same wire shape via the `via` discriminator:
143
- *
144
- * - `direct` — the collection itself matched (name/description via LIKE in
145
- * every mode, plus vector match in hybrid/semantic mode).
146
- * - `member` — the collection was surfaced because one of the orgs that
147
- * *did* match the query is a member of it; `matchedOrgSlugs`
148
- * carries the subset of result-set org slugs that triggered
149
- * the rollup so the UI can render an "includes X, Y" hint.
150
- *
151
- * `direct` rows always sort ahead of `member` rows; ties break on
152
- * `score` then `memberCount`. Slugs are unique across the array — a
153
- * direct hit that's also a member rollup keeps only the `direct` row
154
- * with `matchedOrgSlugs` carried over.
155
- */
156
- export const SearchCollectionHitSchema = z.object({
157
- slug: z.string(),
158
- name: z.string(),
159
- description: z.string().nullable(),
160
- memberCount: z.number().int().min(0),
161
- via: z.enum(["direct", "member"]),
162
- /** Hybrid/semantic fusion score; absent on pure-lexical hits. */
163
- score: z.number().optional(),
164
- /** Result-set org slugs that triggered this rollup. Always present on `via=member`. */
165
- matchedOrgSlugs: z.array(z.string()).optional(),
166
- /**
167
- * A small org preview (capped at 3) for an inline avatar facepile on the
168
- * search card, mirroring the collections list page. Org-kind only — search's
169
- * `memberCount` counts orgs, so the facepile and its "+N more" stay
170
- * consistent. Attached after hit merge; optional on the wire so older workers
171
- * mid-rollout don't trip the schema.
172
- */
173
- previewMembers: z.array(CollectionMemberSchema).optional(),
174
- });
175
-
176
- /**
177
- * Heading-aware CHANGELOG.md slice returned by hybrid / semantic search.
178
- * Clients can deep-link to
179
- * `/<orgSlug>/<sourceSlug>/changelog?offset=<offset>#chunk` to read the
180
- * surrounding file content (the web `chunkDeepLink` builder; falls back to
181
- * the `/source/<sourceSlug>` redirect shim when `orgSlug` is null).
182
- */
183
- export const SearchChunkHitSchema = z.object({
184
- sourceSlug: z.string(),
185
- sourceName: z.string(),
186
- orgSlug: z.string().nullable(),
187
- orgName: z.string().nullable().optional(),
188
- filePath: z.string(),
189
- offset: z.number().int().min(0),
190
- length: z.number().int().min(0),
191
- heading: z.string().nullable(),
192
- snippet: z.string(),
193
- score: z.number(),
194
- });
195
-
196
- /**
197
- * Slim wire payload embedded in a search response when the query is a
198
- * GitHub `org/repo` coordinate and no existing entity matched. Reuses
199
- * `LookupStatusSchema` from `./lookups.js` rather than redefining — the
200
- * thick `POST /v1/lookups` response and the slim search-embedded payload
201
- * share the same status set.
202
- */
203
- export const LookupResultPayloadSchema = z.object({
204
- status: LookupStatusSchema,
205
- source: z
206
- .object({
207
- id: z.string(),
208
- slug: z.string(),
209
- name: z.string(),
210
- url: z.string(),
211
- discovery: z.enum(["curated", "agent", "on_demand"]),
212
- stars: z.number().int().min(0).nullable().optional(),
213
- })
214
- .optional(),
215
- releases: z
216
- .array(
217
- z.object({
218
- id: z.string(),
219
- version: z.string().nullable(),
220
- title: z.string(),
221
- publishedAt: z.string().nullable(),
222
- /** First image/gif thumbnail; null when the release has none. */
223
- thumbnail: ReleaseThumbnailSchema.nullable().optional(),
224
- /** The release's upstream source URL, when it has one. See
225
- * `SearchReleaseHitSchema.url`. */
226
- url: z.string().nullable().optional(),
227
- }),
228
- )
229
- .optional(),
230
- // Unambiguous "did you mean" rail; null when the org segment matches
231
- // multiple curated orgs or none.
232
- relatedOrg: z
233
- .object({
234
- org: z.object({ id: z.string(), slug: z.string(), name: z.string() }),
235
- sources: z.array(
236
- z.object({ id: z.string(), slug: z.string(), name: z.string(), url: z.string() }),
237
- ),
238
- })
239
- .nullable(),
240
- });
241
-
242
- export const UnifiedSearchResponseSchema = z.object({
243
- query: z.string(),
244
- // Normalized `?domain=` echo + resolution outcome. Both absent when no
245
- // domain filter was applied.
246
- domain: z.string().optional(),
247
- domainStatus: z.enum(["matched", "not_found"]).optional(),
248
- // Normalized `?category=` echo. Present only when a category filter was
249
- // applied; the resolved canonical slug (aliases are folded to canonical).
250
- // `categoryStatus` is always `matched` because an unknown category is a 400,
251
- // not an empty envelope — categories are a fixed enum, unlike collections.
252
- category: z.string().optional(),
253
- categoryStatus: z.enum(["matched"]).optional(),
254
- // Normalized `?collection=` echo + resolution outcome. `not_found` when the
255
- // slug matched no collection (empty envelope, mirroring `domainStatus`).
256
- collection: z.string().optional(),
257
- collectionStatus: z.enum(["matched", "not_found"]).optional(),
258
- orgs: z.array(SearchOrgHitSchema),
259
- catalog: z.array(SearchCatalogHitSchema),
260
- sources: z.array(SearchSourceHitSchema),
261
- releases: z.array(SearchReleaseHitSchema),
262
- /**
263
- * Curated collections matching the query, either directly (name/description
264
- * match or vector hit) or via member rollup (one of the result orgs is in
265
- * the collection). Optional on the wire so older workers mid-rollout don't
266
- * trip the schema check; clients should treat missing and `[]` identically.
267
- */
268
- collections: z.array(SearchCollectionHitSchema).optional(),
269
- chunks: z.array(SearchChunkHitSchema).optional(),
270
- mode: z.enum(["lexical", "semantic", "hybrid"]).optional(),
271
- degraded: z.boolean().optional(),
272
- degradedReason: z.string().optional(),
273
- lookup: LookupResultPayloadSchema.nullable().optional(),
274
- });
@@ -1,65 +0,0 @@
1
- import { z } from "zod";
2
-
3
- /**
4
- * Admin interest-alert quality (`GET /v1/admin/semantic-alerts/summary`).
5
- * Decisions live in the classifications Analytics Engine dataset with
6
- * `blob4 = semantic-alert`. This is the hydrated read model. Alert query
7
- * text is not a field — it is not stored on the points.
8
- */
9
-
10
- const ProbabilityBinSchema = z.object({
11
- /** Inclusive. Width is 0.1. `0.8` is the start of the default-threshold bin. */
12
- start: z.number(),
13
- /** Exclusive, except the last bin (`0.9`–`1`) which includes `1`. */
14
- end: z.number(),
15
- count: z.number(),
16
- });
17
-
18
- const SemanticAlertDispositionCountsSchema = z.object({
19
- matched: z.number(),
20
- belowThreshold: z.number(),
21
- failed: z.number(),
22
- });
23
-
24
- /**
25
- * `GET /v1/admin/semantic-alerts/summary`.
26
- *
27
- * `scored` is matched + below threshold (a probability came back).
28
- * `matchRate` is matched / scored, or null when nothing was scored.
29
- * `failed` is fail-closed and stays out of the rate.
30
- * Cost is not on these points; `meta.costLane` names the `ai_usage` lane.
31
- */
32
- export const SemanticAlertSummarySchema = z.object({
33
- after: z.string(),
34
- before: z.string(),
35
- bucket: z.enum(["hour", "day"]),
36
- totals: SemanticAlertDispositionCountsSchema.extend({
37
- scored: z.number(),
38
- matchRate: z.number().nullable(),
39
- }),
40
- series: z.array(
41
- SemanticAlertDispositionCountsSchema.extend({
42
- t: z.string(),
43
- }),
44
- ),
45
- failures: z.array(
46
- z.object({
47
- category: z.enum(["provider_error", "invalid_probability", "unknown"]),
48
- count: z.number(),
49
- }),
50
- ),
51
- probability: z.object({
52
- /** Default alert threshold. Each point stores its own threshold separately. */
53
- defaultThreshold: z.literal(0.8),
54
- bins: z.array(ProbabilityBinSchema),
55
- /** Sampled rows whose probability double is `< 0` (the absent sentinel). */
56
- missing: z.number(),
57
- }),
58
- meta: z.object({
59
- dataset: z.string(),
60
- retentionDays: z.literal(90),
61
- sampled: z.literal(true),
62
- /** `ai_usage` lane that carries JEV cost. Not summed from these points. */
63
- costLane: z.literal("semantic-alert-match"),
64
- }),
65
- });
@@ -1,36 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
- import { NoticeSchema } from "./shared";
3
-
4
- describe("NoticeSchema", () => {
5
- it("accepts a message-only notice", () => {
6
- expect(NoticeSchema.safeParse({ message: "Heads up" }).success).toBe(true);
7
- });
8
- it("accepts a message + internal coordinate", () => {
9
- expect(
10
- NoticeSchema.safeParse({
11
- message: "Moved",
12
- coordinate: "cognition/devin",
13
- linkText: "View Devin",
14
- }).success,
15
- ).toBe(true);
16
- });
17
- it("accepts a message + external href", () => {
18
- expect(NoticeSchema.safeParse({ message: "Moved", href: "https://devin.ai" }).success).toBe(
19
- true,
20
- );
21
- });
22
- it("rejects both coordinate and href", () => {
23
- expect(
24
- NoticeSchema.safeParse({ message: "x", coordinate: "a/b", href: "https://x.com" }).success,
25
- ).toBe(false);
26
- });
27
- it("rejects an empty message and an over-long message", () => {
28
- expect(NoticeSchema.safeParse({ message: "" }).success).toBe(false);
29
- expect(NoticeSchema.safeParse({ message: "x".repeat(281) }).success).toBe(false);
30
- });
31
- it("rejects a malformed coordinate", () => {
32
- expect(NoticeSchema.safeParse({ message: "x", coordinate: "/leading" }).success).toBe(false);
33
- expect(NoticeSchema.safeParse({ message: "x", coordinate: "a/b/c" }).success).toBe(false);
34
- expect(NoticeSchema.safeParse({ message: "x", coordinate: "" }).success).toBe(false);
35
- });
36
- });
@@ -1,267 +0,0 @@
1
- import { z } from "zod";
2
- import { RELEASE_TYPES } from "@buildinternet/releases-core/schema";
3
- import { BREAKING_LEVELS } from "@buildinternet/releases-core/breaking";
4
- import { CATEGORIES } from "@buildinternet/releases-core/categories";
5
- import { isValidNoticeCoordinate } from "@buildinternet/releases-core/notice";
6
- import { IMPORTANCE_MAX, IMPORTANCE_MIN } from "@buildinternet/releases-core/importance";
7
-
8
- export const CategorySchema = z.enum(CATEGORIES);
9
-
10
- /**
11
- * AI-scored release importance, 1 (housekeeping) to 5 (landmark). Scored at
12
- * ingest; `.nullable().optional()` — null when unscored, absent on older
13
- * servers/pinned workers.
14
- */
15
- export const ImportanceScoreSchema = z
16
- .number()
17
- .int()
18
- .min(IMPORTANCE_MIN)
19
- .max(IMPORTANCE_MAX)
20
- .nullable()
21
- .optional();
22
-
23
- export const MediaItemSchema = z.object({
24
- type: z.enum(["image", "video", "gif"]),
25
- url: z.string(),
26
- alt: z.string().optional(),
27
- // R2 object key (`releases/<hash>.<ext>`) for media mirrored into the
28
- // `released-media` bucket; read paths resolve it to a same-origin `r2Url`
29
- // (`resolveR2Url`). Stamped by `processMediaForR2` at ingest / manual edit and
30
- // part of the stored `media[]` JSON — surfaced on the wire so a curator round-
31
- // tripping `media` through `PATCH /v1/releases/:id` can preserve an already-
32
- // mirrored item without forcing a re-fetch.
33
- r2Key: z.string().optional(),
34
- r2Url: z.string().optional(),
35
- // For `type: "video"` items promoted from a hosted-video link found inline in
36
- // a release body (Wistia/Loom/Vimeo/YouTube), `url` holds the poster/thumbnail
37
- // (mirrored to R2 like any image) and `linkUrl` holds the human watch URL the
38
- // play-card links out to. Absent for ordinary image/gif media. See
39
- // `detectInlineVideos` in `@releases/rendering/video-embed` and web's
40
- // release-content video card.
41
- linkUrl: z.string().optional(),
42
- });
43
-
44
- /**
45
- * App Store platform + icon for a `type: "appstore"` source. Threaded onto
46
- * read surfaces so the UI can render the compact app-update row (app icon +
47
- * "Available for iOS/macOS") instead of the standard version/notes/thumbnail
48
- * layout. Sourced from `source.metadata.appStore` server-side. #mobile-appstore-feed-row
49
- */
50
- export const AppStoreSourceInfoSchema = z.object({
51
- platform: z.enum(["ios", "macos"]),
52
- iconUrl: z.string().nullable(),
53
- });
54
-
55
- /** Video provider tag, present only when `sourceType === "video"`. Thumbnail
56
- * and watch URL reuse the release's existing `media[]` / `url`. */
57
- export const VideoSourceInfoSchema = z.object({
58
- provider: z.enum(["youtube", "vimeo", "wistia"]),
59
- });
60
-
61
- /**
62
- * Org tier surfaced on read paths (#1947). `stub` = known only by declared
63
- * release locations (no processed sources yet); `tracked` = a normal org whose
64
- * sources fetch. The column is `organizations.tier`; the wire name is `status`.
65
- * Optional on the wire so a pinned client mid-deploy tolerates its absence —
66
- * treat a missing value as `"tracked"`.
67
- */
68
- export const OrgStatusSchema = z.enum(["stub", "tracked"]);
69
-
70
- /**
71
- * A declared release location (#1947) — one `release_locations` row on the
72
- * wire, the same shape a releases.json `releases[]` entry carries plus its
73
- * provenance `basis` and, once promoted, the `sourceId` it materialized into.
74
- * Surfaced on stub orgs so an agent gets "release info is published at these
75
- * locations" instead of an empty sources list.
76
- */
77
- export const ReleaseLocationItemSchema = z.object({
78
- url: z.string().optional(),
79
- feed: z.string().optional(),
80
- github: z.string().optional(),
81
- appstore: z.string().optional(),
82
- file: z.string().optional(),
83
- title: z.string().nullable().optional(),
84
- canonical: z.boolean(),
85
- basis: z.enum(["curator", "declared", "detected", "generated"]),
86
- /** Owning product id when the locator is product-scoped, else null. */
87
- productId: z.string().nullable(),
88
- /** Set once the locator has been promoted into a source, else null. */
89
- sourceId: z.string().nullable(),
90
- /** `"push"` when the owner publishes this changelog instead of us polling it. */
91
- publish: z.literal("push").optional(),
92
- /** Changelog file or glob. Present with `publish: "push"`. */
93
- path: z.string().optional(),
94
- });
95
-
96
- export const PaginationSchema = z.object({
97
- page: z.number().int().min(1),
98
- pageSize: z.number().int().min(1),
99
- returned: z.number().int().min(0),
100
- totalItems: z.number().int().min(0).optional(),
101
- totalPages: z.number().int().min(0).optional(),
102
- hasMore: z.boolean(),
103
- });
104
-
105
- export const ListResponseSchema = <T extends z.ZodTypeAny>(item: T) =>
106
- z.object({
107
- items: z.array(item),
108
- pagination: PaginationSchema,
109
- });
110
-
111
- export const StatsSchema = z.object({
112
- orgs: z.number().int().min(0),
113
- sources: z.number().int().min(0),
114
- releases: z.number().int().min(0),
115
- products: z.number().int().min(0),
116
- });
117
-
118
- export const ReleaseTypeSchema = z.enum(RELEASE_TYPES);
119
-
120
- /**
121
- * Per-category item counts produced by the AI release-content pass and
122
- * persisted on `releases.metadata.composition`. Surfaces as a chip on the
123
- * release detail UI ("12 fixes · 3 features"); `null` when the row hasn't
124
- * been through the AI pass yet, the body was empty/boilerplate, or all
125
- * three counts came back zero (we filter that case out on the parse side).
126
- */
127
- export const ReleaseCompositionSchema = z.object({
128
- bugs: z.number().int().min(0),
129
- features: z.number().int().min(0),
130
- enhancements: z.number().int().min(0),
131
- });
132
-
133
- export const ReleaseItemSchema = z.object({
134
- id: z.string().optional(),
135
- version: z.string().nullable(),
136
- title: z.string(),
137
- summary: z.string(),
138
- content: z.string().optional(),
139
- publishedAt: z.string().nullable(),
140
- // ISO timestamp of when the row was fetched/inserted — used by the feed
141
- // cursor's tie-break (publishedAt|fetchedAt|id) so that same-`publishedAt`
142
- // rows stay stably ordered across paginated requests. `.optional()` because
143
- // not every endpoint that returns a ReleaseItem populates it (notably
144
- // single-release reads).
145
- fetchedAt: z.string().optional(),
146
- url: z.string().nullable(),
147
- media: z.array(MediaItemSchema).optional(),
148
- // `.optional()` — older API responses (mid-deploy or pinned old workers)
149
- // may omit `type`; consumers should treat `undefined` as `"feature"`.
150
- type: ReleaseTypeSchema.optional(),
151
- // Pre-release flag (alpha/beta/rc/preview/nightly). Computed at ingest —
152
- // GitHub uses the API's authoritative `prerelease` field; other adapters
153
- // fall back to a SemVer-prerelease regex. `.optional()` for the same
154
- // older-response degrade-gracefully reason as `type`.
155
- prerelease: z.boolean().optional(),
156
- // AI-generated headline forms paired with `summary` (#852, renamed in #860).
157
- // Both nullable — most rows in the DB have neither populated, and we don't
158
- // plan to backfill, so consumers must fall back to `title` for display.
159
- // `.optional()` for the same mid-deploy / pinned-worker reason as `type`.
160
- titleGenerated: z.string().nullable().optional(),
161
- titleShort: z.string().nullable().optional(),
162
- // Machine-readable breaking-change level (#1696, surfaced on read paths in
163
- // #1710). `.optional()` for mid-deploy / pinned-worker tolerance and because
164
- // rows predating the column omit it; absent or `"unknown"` means not
165
- // classified (the fail-open default). List paths carry `breaking` only —
166
- // `migrationNotes` stays detail-route-only to keep list payloads slim.
167
- breaking: z.enum(BREAKING_LEVELS).optional(),
168
- importance: ImportanceScoreSchema,
169
- // Count of demoted siblings rolling up into this row via `release_coverage`
170
- // (0 when standalone). Lets list views show a cluster indicator without a
171
- // detail-page round trip. `.optional()` — older API responses and pinned
172
- // workers omit the field; treat undefined as 0.
173
- coverageCount: z.number().int().min(0).optional(),
174
- // Cached `LENGTH(content)` and `countTokensSafe(content)` for the release
175
- // body — lets feed surfaces ("releases get <org>", `/v1/releases/latest`,
176
- // MCP `get_latest_releases`) advertise "this release is ~1.5K tokens" so
177
- // agents can decide whether to pull the full body. `.optional()` and
178
- // nullable because pre-existing rows landed without the columns; the
179
- // backfill script populates them and renderers degrade to "size unknown"
180
- // when null. See #958.
181
- contentChars: z.number().int().min(0).nullable().optional(),
182
- contentTokens: z.number().int().min(0).nullable().optional(),
183
- // Per-category item counts produced by the AI release-content pass and
184
- // stored on `releases.metadata.composition`. `null`/`undefined` when the
185
- // row hasn't been through the AI pass yet or the body was boilerplate.
186
- composition: ReleaseCompositionSchema.nullable().optional(),
187
- });
188
-
189
- export const ReleaseSummaryItemSchema = z.object({
190
- year: z.number().int().nullable().optional(),
191
- month: z.number().int().nullable().optional(),
192
- windowDays: z.number().int().nullable().optional(),
193
- summary: z.string(),
194
- releaseCount: z.number().int().min(0),
195
- generatedAt: z.string(),
196
- });
197
-
198
- /**
199
- * A source attached to an overview page (#846, reshaped #1934). Each row names
200
- * a source the overview drew on — `sourceUrl` plus, when it resolved to an
201
- * on-registry release, `releaseId` + a canonical `releaseWebUrl`. This is a
202
- * source *list*, not span-anchored provenance: the old body char offsets +
203
- * verbatim quote are removed. Optional everywhere it appears — consumers should
204
- * degrade gracefully.
205
- */
206
- export const OverviewCitationSchema = z.object({
207
- sourceUrl: z.string(),
208
- title: z.string().nullable().optional(),
209
- /** Best-effort link to the on-registry release this source maps to (resolved server-side). */
210
- releaseId: z.string().nullable().optional(),
211
- /**
212
- * Canonical web URL of the on-registry release page for this source, when
213
- * `releaseId` resolved (#1934). Kept for machine consumers; the web Sources
214
- * footer links `sourceUrl` (the #2218 link policy — `/release/*` pages are
215
- * noindexed).
216
- */
217
- releaseWebUrl: z.string().nullable().optional(),
218
- /**
219
- * @deprecated Body char offsets / verbatim quote — removed with span-anchored
220
- * citations (#1934). Kept optional for wire back-compat; no producer emits
221
- * them and no consumer should read them.
222
- */
223
- startIndex: z.number().int().min(0).optional(),
224
- endIndex: z.number().int().min(0).optional(),
225
- citedText: z.string().optional(),
226
- });
227
-
228
- /**
229
- * Entity notice — a small curator-set note on an org / product / source. Stored
230
- * under the `notice` key of the entity's metadata JSON; mirrors the `Notice`
231
- * type in @buildinternet/releases-core/notice. `coordinate` is an internal
232
- * registry path ("org" / "org/slug"); `href` is an external URL; at most one.
233
- */
234
- export const NoticeSchema = z
235
- .object({
236
- message: z.string().min(1).max(280),
237
- linkText: z.string().min(1).max(60).optional(),
238
- coordinate: z.string().min(1).max(200).optional(),
239
- href: z.url().max(500).optional(),
240
- })
241
- .refine((n) => !(n.coordinate && n.href), {
242
- message: "notice may set coordinate or href, not both",
243
- })
244
- .refine((n) => !n.coordinate || isValidNoticeCoordinate(n.coordinate), {
245
- message: "notice.coordinate must be 'org' or 'org/slug'",
246
- });
247
-
248
- /**
249
- * Display thumbnail derived server-side from a release's first image/gif media
250
- * entry. Shared by the coverage, lookup, and related-release payloads.
251
- */
252
- export const ReleaseThumbnailSchema = z.object({
253
- url: z.string(),
254
- alt: z.string().optional(),
255
- });
256
-
257
- export const OverviewPageItemSchema = z.object({
258
- scope: z.enum(["org", "product"]),
259
- orgSlug: z.string().nullable().optional(),
260
- productSlug: z.string().nullable().optional(),
261
- content: z.string(),
262
- releaseCount: z.number().int().min(0),
263
- lastContributingReleaseAt: z.string().nullable(),
264
- generatedAt: z.string(),
265
- updatedAt: z.string(),
266
- citations: z.array(OverviewCitationSchema).optional(),
267
- });