@grove-dev/core 0.1.1

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 (70) hide show
  1. package/README.md +89 -0
  2. package/dist/build-data.d.ts +55 -0
  3. package/dist/build-data.d.ts.map +1 -0
  4. package/dist/build-data.js +115 -0
  5. package/dist/build-data.js.map +1 -0
  6. package/dist/config.d.ts +5 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +14 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/decisions.d.ts +28 -0
  11. package/dist/decisions.d.ts.map +1 -0
  12. package/dist/decisions.js +57 -0
  13. package/dist/decisions.js.map +1 -0
  14. package/dist/enrich.d.ts +26 -0
  15. package/dist/enrich.d.ts.map +1 -0
  16. package/dist/enrich.js +121 -0
  17. package/dist/enrich.js.map +1 -0
  18. package/dist/github-client.d.ts +28 -0
  19. package/dist/github-client.d.ts.map +1 -0
  20. package/dist/github-client.js +91 -0
  21. package/dist/github-client.js.map +1 -0
  22. package/dist/github.d.ts +8 -0
  23. package/dist/github.d.ts.map +1 -0
  24. package/dist/github.js +62 -0
  25. package/dist/github.js.map +1 -0
  26. package/dist/health.d.ts +3 -0
  27. package/dist/health.d.ts.map +1 -0
  28. package/dist/health.js +103 -0
  29. package/dist/health.js.map +1 -0
  30. package/dist/importer.d.ts +3 -0
  31. package/dist/importer.d.ts.map +1 -0
  32. package/dist/importer.js +24 -0
  33. package/dist/importer.js.map +1 -0
  34. package/dist/index.d.ts +17 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +17 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/io.d.ts +4 -0
  39. package/dist/io.d.ts.map +1 -0
  40. package/dist/io.js +21 -0
  41. package/dist/io.js.map +1 -0
  42. package/dist/llms.d.ts +34 -0
  43. package/dist/llms.d.ts.map +1 -0
  44. package/dist/llms.js +99 -0
  45. package/dist/llms.js.map +1 -0
  46. package/dist/markdown.d.ts +18 -0
  47. package/dist/markdown.d.ts.map +1 -0
  48. package/dist/markdown.js +182 -0
  49. package/dist/markdown.js.map +1 -0
  50. package/dist/parseReadme.d.ts +25 -0
  51. package/dist/parseReadme.d.ts.map +1 -0
  52. package/dist/parseReadme.js +100 -0
  53. package/dist/parseReadme.js.map +1 -0
  54. package/dist/schema.d.ts +988 -0
  55. package/dist/schema.d.ts.map +1 -0
  56. package/dist/schema.js +729 -0
  57. package/dist/schema.js.map +1 -0
  58. package/dist/sitemap.d.ts +38 -0
  59. package/dist/sitemap.d.ts.map +1 -0
  60. package/dist/sitemap.js +73 -0
  61. package/dist/sitemap.js.map +1 -0
  62. package/dist/slug.d.ts +3 -0
  63. package/dist/slug.d.ts.map +1 -0
  64. package/dist/slug.js +15 -0
  65. package/dist/slug.js.map +1 -0
  66. package/dist/validate.d.ts +11 -0
  67. package/dist/validate.d.ts.map +1 -0
  68. package/dist/validate.js +58 -0
  69. package/dist/validate.js.map +1 -0
  70. package/package.json +50 -0
package/dist/schema.js ADDED
@@ -0,0 +1,729 @@
1
+ import { z } from "zod";
2
+ import { parse, stringify } from "yaml";
3
+ // ──────────────────────────────────────────────────────────────────────
4
+ // Health & decision labels
5
+ // ──────────────────────────────────────────────────────────────────────
6
+ export const healthStatusSchema = z.enum([
7
+ "active",
8
+ "mature",
9
+ "stale",
10
+ "inactive",
11
+ "archived",
12
+ "unknown",
13
+ "historical",
14
+ "needs_review",
15
+ "quiet",
16
+ "unavailable",
17
+ ]);
18
+ export const decisionVisibilitySchema = z.enum([
19
+ "highlight",
20
+ "keep",
21
+ "needs_review",
22
+ "hide",
23
+ "remove",
24
+ "historical",
25
+ ]);
26
+ export const healthTierSchema = z.enum([
27
+ "curated",
28
+ "listed",
29
+ "experimental",
30
+ "hidden",
31
+ ]);
32
+ // ──────────────────────────────────────────────────────────────────────
33
+ // Curation: project shape
34
+ // ──────────────────────────────────────────────────────────────────────
35
+ export const projectTypeSchema = z.enum([
36
+ "real-app",
37
+ "production",
38
+ "reference",
39
+ "library",
40
+ "tool",
41
+ "demo",
42
+ "template",
43
+ "historical",
44
+ ]);
45
+ export const difficultySchema = z.enum(["beginner", "intermediate", "advanced"]);
46
+ export const codebaseSizeSchema = z.enum(["small", "medium", "large", "huge"]);
47
+ export const appLabelSchema = z.enum(["new", "hot", "mature", "featured"]);
48
+ export const scoreSchema = z.object({
49
+ activity: z.number().min(0).max(100).optional(),
50
+ maturity: z.number().min(0).max(100).optional(),
51
+ learning: z.number().min(0).max(100).optional(),
52
+ contribution: z.number().min(0).max(100).optional(),
53
+ docs: z.number().min(0).max(100).optional(),
54
+ overall: z.number().min(0).max(100).optional(),
55
+ });
56
+ // ──────────────────────────────────────────────────────────────────────
57
+ // Distribution channels
58
+ // ──────────────────────────────────────────────────────────────────────
59
+ export const distributionChannelTypeSchema = z.enum([
60
+ "app-store",
61
+ "play-store",
62
+ "fdroid",
63
+ "github-releases",
64
+ "apk",
65
+ "testflight",
66
+ "website",
67
+ "snapcraft",
68
+ "flathub",
69
+ "microsoft-store",
70
+ "package-registry",
71
+ "docs",
72
+ "demo",
73
+ "other",
74
+ ]);
75
+ export const distributionChannelSchema = z.object({
76
+ type: distributionChannelTypeSchema,
77
+ platform: z.string().optional(),
78
+ label: z.string().optional(),
79
+ url: z.string().url(),
80
+ verified: z.boolean().optional(),
81
+ notes: z.string().optional(),
82
+ });
83
+ // ──────────────────────────────────────────────────────────────────────
84
+ // GitHub-shaped metadata
85
+ // ──────────────────────────────────────────────────────────────────────
86
+ export const githubLicenseSchema = z
87
+ .object({
88
+ key: z.string().nullable().optional(),
89
+ name: z.string().nullable().optional(),
90
+ spdx_id: z.string().nullable().optional(),
91
+ url: z.string().nullable().optional(),
92
+ })
93
+ .passthrough()
94
+ .nullable()
95
+ .optional();
96
+ export const githubRepositorySchema = z
97
+ .object({
98
+ id: z.number().optional(),
99
+ node_id: z.string().optional(),
100
+ name: z.string().optional(),
101
+ full_name: z.string().optional(),
102
+ html_url: z.string().url().optional(),
103
+ clone_url: z.string().optional(),
104
+ ssh_url: z.string().optional(),
105
+ homepage: z.string().nullable().optional(),
106
+ description: z.string().nullable().optional(),
107
+ fork: z.boolean().optional(),
108
+ archived: z.boolean().optional(),
109
+ disabled: z.boolean().optional(),
110
+ private: z.boolean().optional(),
111
+ is_template: z.boolean().optional(),
112
+ visibility: z.string().optional(),
113
+ default_branch: z.string().optional(),
114
+ language: z.string().nullable().optional(),
115
+ topics: z.array(z.string()).optional(),
116
+ license: githubLicenseSchema,
117
+ stargazers_count: z.number().optional(),
118
+ watchers_count: z.number().optional(),
119
+ forks_count: z.number().optional(),
120
+ open_issues_count: z.number().optional(),
121
+ subscribers_count: z.number().optional(),
122
+ size: z.number().optional(),
123
+ created_at: z.string().nullable().optional(),
124
+ updated_at: z.string().nullable().optional(),
125
+ pushed_at: z.string().nullable().optional(),
126
+ })
127
+ .passthrough();
128
+ // ──────────────────────────────────────────────────────────────────────
129
+ // Stack (registry-backed taxonomy)
130
+ // ──────────────────────────────────────────────────────────────────────
131
+ export const stackTechnologySchema = z
132
+ .object({
133
+ id: z.string().min(1),
134
+ role: z.string().optional(),
135
+ })
136
+ .passthrough();
137
+ export const stackSchema = z
138
+ .object({
139
+ primary: z.string().min(1),
140
+ families: z.array(z.string()).optional(),
141
+ technologies: z.array(stackTechnologySchema).optional(),
142
+ })
143
+ .passthrough();
144
+ export const stackRefSchema = z.object({
145
+ id: z.string().min(1),
146
+ name: z.string().min(1),
147
+ family: z.string().optional(),
148
+ languages: z.array(z.string()).optional(),
149
+ platforms: z.array(z.string()).optional(),
150
+ icon: z.string().optional(),
151
+ status: z.enum(["live", "expanding", "planned"]).optional(),
152
+ blurb: z.string().optional(),
153
+ });
154
+ export const platformRefSchema = z.object({
155
+ id: z.string().min(1),
156
+ name: z.string().min(1),
157
+ icon: z.string().optional(),
158
+ });
159
+ export const categoryRefSchema = z.object({
160
+ id: z.string().min(1),
161
+ name: z.string().min(1),
162
+ blurb: z.string().optional(),
163
+ });
164
+ export const distributionChannelRefSchema = z.object({
165
+ id: distributionChannelTypeSchema,
166
+ name: z.string().min(1),
167
+ platforms: z.array(z.string()).optional(),
168
+ });
169
+ // ──────────────────────────────────────────────────────────────────────
170
+ // Item — generic open-source project record
171
+ // ──────────────────────────────────────────────────────────────────────
172
+ export const itemSchema = z.object({
173
+ id: z.string().min(1),
174
+ name: z.string().min(1),
175
+ description: z.string().default(""),
176
+ links: z
177
+ .object({
178
+ github: z.string().url().optional(),
179
+ website: z.string().url().optional(),
180
+ docs: z.string().url().optional(),
181
+ source: z.string().url().optional(),
182
+ })
183
+ .catchall(z.string().url())
184
+ .default({}),
185
+ source: z
186
+ .object({
187
+ type: z.enum(["markdown", "manual", "github-topic", "awesome-list", "submit"]).default("manual"),
188
+ file: z.string().optional(),
189
+ line: z.number().int().positive().optional(),
190
+ url: z.string().optional(),
191
+ provider: z.string().optional(),
192
+ owner: z.string().optional(),
193
+ repo: z.string().optional(),
194
+ })
195
+ .default({ type: "manual" }),
196
+ taxonomy: z
197
+ .object({
198
+ category: z.string().min(1).default("uncategorized"),
199
+ tags: z.array(z.string()).default([]),
200
+ language: z.string().optional(),
201
+ stack: z.string().optional(),
202
+ stacks: z.array(z.string()).default([]),
203
+ platforms: z.array(z.string()).default([]),
204
+ })
205
+ .default({
206
+ category: "uncategorized",
207
+ tags: [],
208
+ stacks: [],
209
+ platforms: [],
210
+ }),
211
+ labels: z.array(appLabelSchema).default([]),
212
+ lenses: z.array(z.string()).default([]),
213
+ distribution: z
214
+ .object({
215
+ channels: z.array(distributionChannelSchema).default([]),
216
+ })
217
+ .default({ channels: [] }),
218
+ curation: z
219
+ .object({
220
+ projectType: projectTypeSchema.optional(),
221
+ difficulty: difficultySchema.optional(),
222
+ codebaseSize: codebaseSizeSchema.optional(),
223
+ stateManagement: z.string().optional(),
224
+ backend: z.string().optional(),
225
+ architecture: z.string().optional(),
226
+ bestFor: z.array(z.string()).default([]),
227
+ whyListed: z.array(z.string()).default([]),
228
+ caveats: z.array(z.string()).default([]),
229
+ goodFirstIssues: z.union([z.boolean(), z.string().url()]).optional(),
230
+ contributionGuide: z.union([z.boolean(), z.string().url()]).optional(),
231
+ launchedBy: z.string().optional(),
232
+ launchAsk: z.array(z.string()).default([]),
233
+ reviewed: z.boolean().optional(),
234
+ reviewedBy: z.string().optional(),
235
+ reviewedAt: z.string().optional(),
236
+ notes: z.string().optional(),
237
+ scores: scoreSchema.default({}),
238
+ })
239
+ .default({
240
+ bestFor: [],
241
+ whyListed: [],
242
+ caveats: [],
243
+ launchAsk: [],
244
+ scores: {},
245
+ }),
246
+ });
247
+ export const itemsFileSchema = z.union([
248
+ z.array(itemSchema),
249
+ z.object({ items: z.array(itemSchema) }),
250
+ ]);
251
+ // ──────────────────────────────────────────────────────────────────────
252
+ // GitHub-shaped app record (schema v1)
253
+ // ──────────────────────────────────────────────────────────────────────
254
+ const isoLike = z.string().min(4).nullable().optional();
255
+ const finalAppSchema = z
256
+ .object({
257
+ schemaVersion: z.number().optional(),
258
+ id: z.string().optional(),
259
+ slug: z.string().min(1),
260
+ source: z
261
+ .object({
262
+ provider: z.literal("github"),
263
+ owner: z.string().min(1),
264
+ repo: z.string().min(1),
265
+ url: z.string().url().optional(),
266
+ })
267
+ .passthrough(),
268
+ app: z
269
+ .object({
270
+ name: z.string().min(1),
271
+ description: z.string().min(1),
272
+ category: z.string().min(1),
273
+ projectType: z.string().optional(),
274
+ platforms: z.array(z.string()).min(1),
275
+ tags: z.array(z.string()).optional(),
276
+ distribution: z
277
+ .object({
278
+ channels: z
279
+ .array(z
280
+ .object({
281
+ type: z.string().min(1),
282
+ platform: z.string().optional(),
283
+ label: z.string().optional(),
284
+ url: z.string().url(),
285
+ verified: z.boolean().optional(),
286
+ notes: z.string().optional(),
287
+ })
288
+ .passthrough())
289
+ .optional(),
290
+ })
291
+ .passthrough()
292
+ .optional(),
293
+ })
294
+ .passthrough(),
295
+ stack: stackSchema,
296
+ github: z
297
+ .object({
298
+ repository: githubRepositorySchema.optional(),
299
+ languages: z.record(z.string(), z.number()).optional(),
300
+ latestRelease: z.record(z.string(), z.unknown()).nullable().optional(),
301
+ activity: z.record(z.string(), z.unknown()).optional(),
302
+ files: z.record(z.string(), z.boolean()).optional(),
303
+ labels: z.array(z.record(z.string(), z.unknown())).optional(),
304
+ sync: z.record(z.string(), z.unknown()).optional(),
305
+ })
306
+ .passthrough()
307
+ .optional(),
308
+ health: z.record(z.string(), z.unknown()).optional(),
309
+ curation: z.record(z.string(), z.unknown()).optional(),
310
+ })
311
+ .passthrough();
312
+ const legacyAppSchema = z
313
+ .object({
314
+ slug: z.string().min(1),
315
+ name: z.string().min(1),
316
+ repoUrl: z.string().url(),
317
+ description: z.string().min(1),
318
+ stack: z.union([z.string(), z.record(z.string(), z.unknown())]),
319
+ stacks: z.array(z.string()).optional(),
320
+ platforms: z.array(z.string()).default([]),
321
+ category: z.string().min(1),
322
+ })
323
+ .passthrough();
324
+ // ──────────────────────────────────────────────────────────────────────
325
+ // Health metadata
326
+ // ──────────────────────────────────────────────────────────────────────
327
+ export const githubMetadataSchema = z.object({
328
+ fullName: z.string().optional(),
329
+ stars: z.number().int().nonnegative().default(0),
330
+ forks: z.number().int().nonnegative().default(0),
331
+ openIssues: z.number().int().nonnegative().optional(),
332
+ watchers: z.number().int().nonnegative().optional(),
333
+ archived: z.boolean().default(false),
334
+ disabled: z.boolean().optional(),
335
+ private: z.boolean().optional(),
336
+ visibility: z.string().optional(),
337
+ pushedAt: z.string().nullable().optional(),
338
+ updatedAt: z.string().nullable().optional(),
339
+ createdAt: z.string().nullable().optional(),
340
+ latestReleaseAt: z.string().nullable().optional(),
341
+ license: z.string().nullable().optional(),
342
+ topics: z.array(z.string()).default([]),
343
+ language: z.string().nullable().optional(),
344
+ defaultBranch: z.string().optional(),
345
+ languages: z.record(z.string(), z.number()).optional(),
346
+ contributorsKnown: z.number().int().nonnegative().optional(),
347
+ totalCommitsKnown: z.number().int().nonnegative().optional(),
348
+ openPullRequests: z.number().int().nonnegative().optional(),
349
+ description: z.string().nullable().optional(),
350
+ homepage: z.string().nullable().optional(),
351
+ files: z.record(z.string(), z.boolean()).optional(),
352
+ monthlyCommits: z
353
+ .array(z.union([
354
+ z.number(),
355
+ z.object({ month: z.string(), commits: z.number() }),
356
+ ]))
357
+ .optional(),
358
+ });
359
+ export const healthEntrySchema = z.object({
360
+ id: z.string().min(1),
361
+ github: githubMetadataSchema.optional(),
362
+ health: z.object({
363
+ status: healthStatusSchema,
364
+ maturity: z.enum(["experimental", "useful", "mature", "unknown"]).default("unknown"),
365
+ tier: healthTierSchema.default("experimental"),
366
+ visibility: decisionVisibilitySchema.default("keep"),
367
+ cleanupCandidate: z.boolean().default(false),
368
+ staleReason: z.string().nullable().optional(),
369
+ confidence: z.enum(["low", "medium", "high"]).default("medium"),
370
+ reasons: z.array(z.string()).default([]),
371
+ }),
372
+ });
373
+ export const healthFileSchema = z.union([
374
+ z.array(healthEntrySchema),
375
+ z.object({ health: z.array(healthEntrySchema) }),
376
+ ]);
377
+ // ──────────────────────────────────────────────────────────────────────
378
+ // Decisions (human curation layer)
379
+ // ──────────────────────────────────────────────────────────────────────
380
+ export const decisionSchema = z.object({
381
+ id: z.string().min(1),
382
+ decision: z.object({
383
+ visibility: decisionVisibilitySchema,
384
+ reason: z.string().min(1),
385
+ reviewedBy: z.string().optional(),
386
+ reviewedAt: z.string().optional(),
387
+ }),
388
+ });
389
+ export const decisionsFileSchema = z.union([
390
+ z.array(decisionSchema),
391
+ z.object({ decisions: z.array(decisionSchema) }),
392
+ ]);
393
+ // ──────────────────────────────────────────────────────────────────────
394
+ // Overrides (manual corrections to imported items)
395
+ // ──────────────────────────────────────────────────────────────────────
396
+ export const overrideSchema = z.object({
397
+ id: z.string().min(1),
398
+ patch: z.record(z.string(), z.unknown()),
399
+ });
400
+ export const overridesFileSchema = z.union([
401
+ z.array(overrideSchema),
402
+ z.object({ overrides: z.array(overrideSchema) }),
403
+ ]);
404
+ // ──────────────────────────────────────────────────────────────────────
405
+ // Curated config
406
+ // ──────────────────────────────────────────────────────────────────────
407
+ export const curatedConfigSchema = z.object({
408
+ name: z.string().min(1),
409
+ tagline: z.string().default("A living, health-aware developer directory."),
410
+ description: z.string().optional(),
411
+ siteUrl: z.string().url().optional(),
412
+ repoUrl: z.string().url().optional(),
413
+ itemLabel: z.string().default("project"),
414
+ paths: z
415
+ .object({
416
+ sourcesDir: z.string().default("sources"),
417
+ dataDir: z.string().default("data"),
418
+ contentDir: z.string().default("content"),
419
+ appsDir: z.string().default("data/apps"),
420
+ taxonomyDir: z.string().default("data/taxonomy"),
421
+ generatedDir: z.string().default("data/generated"),
422
+ items: z.string().default("data/items.yml"),
423
+ health: z.string().default("data/health.yml"),
424
+ decisions: z.string().default("data/decisions.yml"),
425
+ overrides: z.string().default("data/overrides.yml"),
426
+ })
427
+ .default({
428
+ sourcesDir: "sources",
429
+ dataDir: "data",
430
+ contentDir: "content",
431
+ appsDir: "data/apps",
432
+ taxonomyDir: "data/taxonomy",
433
+ generatedDir: "data/generated",
434
+ items: "data/items.yml",
435
+ health: "data/health.yml",
436
+ decisions: "data/decisions.yml",
437
+ overrides: "data/overrides.yml",
438
+ }),
439
+ });
440
+ // ──────────────────────────────────────────────────────────────────────
441
+ // Helpers
442
+ // ──────────────────────────────────────────────────────────────────────
443
+ export function unwrapItems(value) {
444
+ return Array.isArray(value) ? value : value.items;
445
+ }
446
+ export function unwrapHealth(value) {
447
+ return Array.isArray(value) ? value : value.health;
448
+ }
449
+ export function unwrapDecisions(value) {
450
+ return Array.isArray(value) ? value : value.decisions;
451
+ }
452
+ export function unwrapOverrides(value) {
453
+ return Array.isArray(value) ? value : value.overrides;
454
+ }
455
+ function compactArray(values) {
456
+ return [...new Set((values ?? []).filter(Boolean))];
457
+ }
458
+ function formatDateOnly(value) {
459
+ if (!value)
460
+ return undefined;
461
+ const d = new Date(value);
462
+ if (Number.isNaN(d.valueOf()))
463
+ return String(value);
464
+ return d.toISOString().slice(0, 10);
465
+ }
466
+ function daysSince(value) {
467
+ if (!value)
468
+ return Infinity;
469
+ const d = new Date(value);
470
+ if (Number.isNaN(d.valueOf()))
471
+ return Infinity;
472
+ return (Date.now() - d.valueOf()) / 86_400_000;
473
+ }
474
+ export function healthFromSignals(params) {
475
+ const { repo, monthlyCommits, legacyStatus } = params;
476
+ const stars = repo?.stargazers_count ?? 0;
477
+ const lastCommitAt = repo?.pushed_at;
478
+ const archived = Boolean(repo?.archived);
479
+ const disabled = Boolean(repo?.disabled);
480
+ const staleDays = daysSince(lastCommitAt);
481
+ const activeMonths = Array.isArray(monthlyCommits)
482
+ ? monthlyCommits.filter((m) => {
483
+ const commits = typeof m === "number" ? m : m?.commits;
484
+ return Number(commits) > 0;
485
+ }).length
486
+ : 0;
487
+ const status = archived
488
+ ? "archived"
489
+ : disabled
490
+ ? "unavailable"
491
+ : staleDays <= 180
492
+ ? "active"
493
+ : staleDays <= 365
494
+ ? "quiet"
495
+ : staleDays <= 730
496
+ ? "stale"
497
+ : "inactive";
498
+ const tier = status === "archived" || status === "unavailable"
499
+ ? "hidden"
500
+ : stars >= 500 || activeMonths >= 4
501
+ ? "curated"
502
+ : stars >= 50
503
+ ? "listed"
504
+ : "experimental";
505
+ return {
506
+ status: legacyStatus ?? status,
507
+ tier,
508
+ visibility: tier === "hidden" ? "hide" : "keep",
509
+ cleanupCandidate: status === "stale" || status === "archived" || status === "inactive" || status === "unavailable",
510
+ staleReason: status === "inactive"
511
+ ? "no_commits_24_months"
512
+ : status === "stale"
513
+ ? "no_commits_365_days"
514
+ : status === "archived"
515
+ ? "github_archived"
516
+ : status === "unavailable"
517
+ ? "github_unavailable"
518
+ : null,
519
+ };
520
+ }
521
+ export function parseAppYaml(text, fileSlug) {
522
+ const raw = parse(text) ?? {};
523
+ if (typeof raw !== "object" || Array.isArray(raw)) {
524
+ throw new Error(`${fileSlug}: app file must contain a YAML mapping`);
525
+ }
526
+ return raw;
527
+ }
528
+ export function stringifyAppYaml(app) {
529
+ return stringify(app, {
530
+ lineWidth: 100,
531
+ singleQuote: false,
532
+ defaultStringType: "PLAIN",
533
+ });
534
+ }
535
+ export function getOwnerRepoFromUrl(repoUrl) {
536
+ if (!repoUrl)
537
+ return null;
538
+ const m = String(repoUrl).match(/github\.com\/([^/]+)\/([^/?#]+)/);
539
+ if (!m)
540
+ return null;
541
+ return { owner: m[1], repo: m[2].replace(/\.git$/, "") };
542
+ }
543
+ export function normalizeAppRecord(raw, fileSlug) {
544
+ const hasFinalShape = typeof raw.app === "object" &&
545
+ raw.app !== null &&
546
+ typeof raw.source === "object" &&
547
+ raw.source !== null &&
548
+ raw.github !== undefined;
549
+ if (hasFinalShape) {
550
+ return normalizeFinal(raw, fileSlug);
551
+ }
552
+ return normalizeLegacy(raw, fileSlug);
553
+ }
554
+ function normalizeFinal(raw, fileSlug) {
555
+ const parsed = finalAppSchema.parse(raw);
556
+ const repo = parsed.github?.repository;
557
+ const repoUrl = parsed.source.url ?? repo?.html_url ?? `https://github.com/${parsed.source.owner}/${parsed.source.repo}`;
558
+ const activity = parsed.github?.activity ?? {};
559
+ const monthlyCommits = activity.monthlyCommits;
560
+ const health = {
561
+ ...healthFromSignals({ repo, monthlyCommits }),
562
+ ...parsed.health,
563
+ };
564
+ const technologies = compactArray((parsed.stack.technologies ?? []).map((t) => t.id));
565
+ const secondaryStacks = technologies.filter((id) => id !== parsed.stack.primary);
566
+ const curation = parsed.curation ?? {};
567
+ return {
568
+ ...parsed,
569
+ schemaVersion: 1,
570
+ slug: parsed.slug || fileSlug,
571
+ name: parsed.app.name,
572
+ description: parsed.app.description,
573
+ repoUrl,
574
+ homepageUrl: repo?.homepage || raw.homepageUrl || undefined,
575
+ stack: parsed.stack.primary,
576
+ stacks: secondaryStacks,
577
+ platforms: parsed.app.platforms,
578
+ distribution: parsed.app.distribution ?? { channels: [] },
579
+ category: parsed.app.category,
580
+ tags: compactArray([...(parsed.app.tags ?? []), ...(repo?.topics ?? [])]),
581
+ license: repo?.license?.spdx_id || undefined,
582
+ status: health.status,
583
+ stars: repo?.stargazers_count,
584
+ forks: repo?.forks_count,
585
+ openIssues: repo?.open_issues_count,
586
+ lastCommitAt: formatDateOnly(repo?.pushed_at) ?? null,
587
+ labels: curation.labels ?? [],
588
+ lenses: curation.lenses ?? [],
589
+ projectType: parsed.app.projectType,
590
+ difficulty: curation.difficulty,
591
+ codebaseSize: curation.codebaseSize,
592
+ bestFor: curation.bestFor ?? [],
593
+ whyListed: curation.whyListed ?? [],
594
+ caveats: curation.caveats ?? [],
595
+ scores: curation.scores ?? {},
596
+ curation: {
597
+ reviewed: Boolean(curation.reviewed),
598
+ reviewedBy: curation.reviewedBy,
599
+ reviewedAt: curation.reviewedAt,
600
+ notes: curation.notes,
601
+ },
602
+ github: parsed.github,
603
+ health,
604
+ tier: health.tier,
605
+ };
606
+ }
607
+ function normalizeLegacy(raw, fileSlug) {
608
+ const parsed = legacyAppSchema.parse({ ...raw, slug: raw.slug ?? fileSlug });
609
+ const ownerRepo = getOwnerRepoFromUrl(parsed.repoUrl);
610
+ const activity = parsed.activity ?? {};
611
+ const repo = {
612
+ full_name: ownerRepo ? `${ownerRepo.owner}/${ownerRepo.repo}` : undefined,
613
+ name: ownerRepo?.repo,
614
+ html_url: parsed.repoUrl,
615
+ homepage: parsed.homepageUrl,
616
+ description: parsed.description,
617
+ fork: false,
618
+ archived: parsed.status === "archived",
619
+ disabled: false,
620
+ private: false,
621
+ visibility: "public",
622
+ language: typeof parsed.stack === "string" ? parsed.stack : undefined,
623
+ license: parsed.license ? { spdx_id: parsed.license } : undefined,
624
+ stargazers_count: activity.stars ?? parsed.stars,
625
+ watchers_count: activity.watchers,
626
+ forks_count: activity.forks ?? parsed.forks,
627
+ open_issues_count: activity.openIssues,
628
+ subscribers_count: activity.contributors,
629
+ pushed_at: activity.lastCommitAt,
630
+ updated_at: activity.updatedAt,
631
+ };
632
+ const monthlyCommits = activity.monthlyCommits ?? [];
633
+ const primaryStack = typeof parsed.stack === "string" ? parsed.stack : parsed.stack.primary;
634
+ const health = healthFromSignals({ repo, monthlyCommits, legacyStatus: parsed.status });
635
+ return {
636
+ schemaVersion: 0,
637
+ slug: parsed.slug,
638
+ name: parsed.name,
639
+ description: parsed.description,
640
+ repoUrl: parsed.repoUrl,
641
+ visibility: health.visibility,
642
+ cleanupCandidate: health.cleanupCandidate,
643
+ staleReason: health.staleReason,
644
+ homepageUrl: parsed.homepageUrl,
645
+ stack: primaryStack,
646
+ stacks: compactArray([...(parsed.stacks ?? [])]),
647
+ platforms: parsed.platforms,
648
+ distribution: parsed.distribution ?? { channels: [] },
649
+ category: parsed.category,
650
+ tags: compactArray((parsed.tags ?? [])),
651
+ license: parsed.license,
652
+ status: health.status,
653
+ stars: repo.stargazers_count,
654
+ forks: repo.forks_count,
655
+ openIssues: repo.open_issues_count,
656
+ lastCommitAt: formatDateOnly(repo.pushed_at) ?? null,
657
+ labels: parsed.labels ?? [],
658
+ lenses: parsed.lenses ?? [],
659
+ projectType: parsed.projectType,
660
+ difficulty: parsed.difficulty,
661
+ codebaseSize: parsed.codebaseSize,
662
+ bestFor: parsed.bestFor ?? [],
663
+ whyListed: parsed.whyListed ?? [],
664
+ caveats: parsed.caveats ?? [],
665
+ scores: parsed.scores ?? {},
666
+ curation: {
667
+ reviewed: false,
668
+ },
669
+ github: {
670
+ repository: repo,
671
+ activity: {
672
+ monthlyCommits,
673
+ totalCommitsKnown: activity.totalCommitsKnown,
674
+ contributorsKnown: activity.contributors,
675
+ openPullRequests: activity.openPullRequests,
676
+ },
677
+ sync: {
678
+ syncedAt: activity.updatedAt,
679
+ source: "legacy-activity-block",
680
+ },
681
+ },
682
+ health: {
683
+ status: health.status,
684
+ tier: health.tier,
685
+ visibility: health.visibility,
686
+ cleanupCandidate: health.cleanupCandidate,
687
+ staleReason: health.staleReason,
688
+ },
689
+ tier: health.tier,
690
+ };
691
+ }
692
+ export function validateAppRecord(raw, fileSlug) {
693
+ try {
694
+ normalizeAppRecord(raw, fileSlug);
695
+ return [];
696
+ }
697
+ catch (err) {
698
+ if (err instanceof z.ZodError) {
699
+ return err.issues.map((issue) => `${fileSlug}: ${issue.path.join(".") || "(root)"} ${issue.message}`);
700
+ }
701
+ return [`${fileSlug}: ${err.message}`];
702
+ }
703
+ }
704
+ export function toIndexApp(app) {
705
+ return {
706
+ slug: app.slug,
707
+ name: app.name,
708
+ description: app.description,
709
+ repoUrl: app.repoUrl,
710
+ homepageUrl: app.homepageUrl,
711
+ category: app.category,
712
+ platforms: app.platforms ?? [],
713
+ distribution: app.distribution,
714
+ primaryStack: app.stack,
715
+ stack: app.stack,
716
+ stacks: app.stacks ?? [],
717
+ technologies: compactArray([app.stack, ...(app.stacks ?? [])]),
718
+ tier: app.health?.tier ?? app.tier,
719
+ status: app.health?.status ?? app.status,
720
+ visibility: app.health?.visibility ?? "keep",
721
+ stars: app.github?.repository?.stargazers_count ?? app.stars,
722
+ forks: app.github?.repository?.forks_count,
723
+ openIssues: app.github?.repository?.open_issues_count,
724
+ license: app.github?.repository?.license?.spdx_id ?? app.license,
725
+ lastCommitAt: formatDateOnly(app.github?.repository?.pushed_at ?? app.lastCommitAt),
726
+ tags: app.tags ?? [],
727
+ };
728
+ }
729
+ //# sourceMappingURL=schema.js.map