@grove-dev/core 0.1.5 → 0.2.5
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/LICENSE +21 -0
- package/dist/build-data.d.ts +22 -34
- package/dist/build-data.d.ts.map +1 -1
- package/dist/build-data.js +108 -71
- package/dist/build-data.js.map +1 -1
- package/dist/config.d.ts +15 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -5
- package/dist/config.js.map +1 -1
- package/dist/decisions.d.ts +15 -12
- package/dist/decisions.d.ts.map +1 -1
- package/dist/decisions.js +52 -32
- package/dist/decisions.js.map +1 -1
- package/dist/enrich.d.ts +2 -2
- package/dist/enrich.js +2 -2
- package/dist/importer.d.ts +3 -2
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +2 -23
- package/dist/importer.js.map +1 -1
- package/dist/llms.d.ts +6 -10
- package/dist/llms.d.ts.map +1 -1
- package/dist/llms.js +55 -44
- package/dist/llms.js.map +1 -1
- package/dist/markdown.d.ts +32 -10
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +95 -55
- package/dist/markdown.js.map +1 -1
- package/dist/schema.d.ts +1312 -577
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +414 -475
- package/dist/schema.js.map +1 -1
- package/dist/sitemap.d.ts +4 -13
- package/dist/sitemap.d.ts.map +1 -1
- package/dist/sitemap.js +10 -8
- package/dist/sitemap.js.map +1 -1
- package/dist/validate.d.ts +46 -2
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +214 -31
- package/dist/validate.js.map +1 -1
- package/package.json +1 -1
package/dist/schema.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { parse, stringify } from "yaml";
|
|
3
3
|
// ──────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Blueprints and kinds
|
|
5
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* A blueprint defines the shape and behavior of a Grove site.
|
|
8
|
+
*
|
|
9
|
+
* V1 ships three fixed blueprints. They are not extensible — no custom
|
|
10
|
+
* blueprint API in V1.
|
|
11
|
+
*/
|
|
12
|
+
export const blueprintSchema = z.enum([
|
|
13
|
+
"project-directory",
|
|
14
|
+
"resource-hub",
|
|
15
|
+
"ecosystem-map",
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Each blueprint is bound to a single resource kind. Records are
|
|
19
|
+
* discriminated by `kind`, which must match the blueprint of the site
|
|
20
|
+
* that owns them.
|
|
21
|
+
*/
|
|
22
|
+
export const resourceKindSchema = z.enum(["project", "resource", "entity"]);
|
|
23
|
+
export const blueprintKind = {
|
|
24
|
+
"project-directory": "project",
|
|
25
|
+
"resource-hub": "resource",
|
|
26
|
+
"ecosystem-map": "entity",
|
|
27
|
+
};
|
|
28
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
4
29
|
// Health & decision labels
|
|
5
30
|
// ──────────────────────────────────────────────────────────────────────
|
|
6
31
|
export const healthStatusSchema = z.enum([
|
|
@@ -30,7 +55,7 @@ export const healthTierSchema = z.enum([
|
|
|
30
55
|
"hidden",
|
|
31
56
|
]);
|
|
32
57
|
// ──────────────────────────────────────────────────────────────────────
|
|
33
|
-
// Curation
|
|
58
|
+
// Curation enums (shared)
|
|
34
59
|
// ──────────────────────────────────────────────────────────────────────
|
|
35
60
|
export const projectTypeSchema = z.enum([
|
|
36
61
|
"real-app",
|
|
@@ -42,8 +67,32 @@ export const projectTypeSchema = z.enum([
|
|
|
42
67
|
"template",
|
|
43
68
|
"historical",
|
|
44
69
|
]);
|
|
45
|
-
export const
|
|
46
|
-
|
|
70
|
+
export const resourceTypeSchema = z.enum([
|
|
71
|
+
"guide",
|
|
72
|
+
"comparison",
|
|
73
|
+
"link",
|
|
74
|
+
"explainer",
|
|
75
|
+
"tool",
|
|
76
|
+
"video",
|
|
77
|
+
"article",
|
|
78
|
+
"course",
|
|
79
|
+
"book",
|
|
80
|
+
"podcast",
|
|
81
|
+
"other",
|
|
82
|
+
]);
|
|
83
|
+
export const entityTypeSchema = z.enum([
|
|
84
|
+
"company",
|
|
85
|
+
"organization",
|
|
86
|
+
"community",
|
|
87
|
+
"school",
|
|
88
|
+
"university",
|
|
89
|
+
"research-lab",
|
|
90
|
+
"agency",
|
|
91
|
+
"service",
|
|
92
|
+
"product",
|
|
93
|
+
"person",
|
|
94
|
+
"other",
|
|
95
|
+
]);
|
|
47
96
|
export const appLabelSchema = z.enum(["new", "hot", "mature", "featured"]);
|
|
48
97
|
export const scoreSchema = z.object({
|
|
49
98
|
activity: z.number().min(0).max(100).optional(),
|
|
@@ -54,34 +103,19 @@ export const scoreSchema = z.object({
|
|
|
54
103
|
overall: z.number().min(0).max(100).optional(),
|
|
55
104
|
});
|
|
56
105
|
// ──────────────────────────────────────────────────────────────────────
|
|
57
|
-
//
|
|
58
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
59
|
-
export const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
});
|
|
106
|
+
// Links
|
|
107
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
108
|
+
export const linksSchema = z
|
|
109
|
+
.object({
|
|
110
|
+
github: z.string().url().optional(),
|
|
111
|
+
website: z.string().url().optional(),
|
|
112
|
+
docs: z.string().url().optional(),
|
|
113
|
+
source: z.string().url().optional(),
|
|
114
|
+
})
|
|
115
|
+
.catchall(z.string().url())
|
|
116
|
+
.default({});
|
|
83
117
|
// ──────────────────────────────────────────────────────────────────────
|
|
84
|
-
//
|
|
118
|
+
// Health metadata
|
|
85
119
|
// ──────────────────────────────────────────────────────────────────────
|
|
86
120
|
export const githubLicenseSchema = z
|
|
87
121
|
.object({
|
|
@@ -125,205 +159,6 @@ export const githubRepositorySchema = z
|
|
|
125
159
|
pushed_at: z.string().nullable().optional(),
|
|
126
160
|
})
|
|
127
161
|
.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
162
|
export const githubMetadataSchema = z.object({
|
|
328
163
|
fullName: z.string().optional(),
|
|
329
164
|
stars: z.number().int().nonnegative().default(0),
|
|
@@ -356,19 +191,20 @@ export const githubMetadataSchema = z.object({
|
|
|
356
191
|
]))
|
|
357
192
|
.optional(),
|
|
358
193
|
});
|
|
194
|
+
export const healthBlockSchema = z.object({
|
|
195
|
+
status: healthStatusSchema.default("unknown"),
|
|
196
|
+
maturity: z.enum(["experimental", "useful", "mature", "unknown"]).default("unknown"),
|
|
197
|
+
tier: healthTierSchema.default("experimental"),
|
|
198
|
+
visibility: decisionVisibilitySchema.default("keep"),
|
|
199
|
+
cleanupCandidate: z.boolean().default(false),
|
|
200
|
+
staleReason: z.string().nullable().optional(),
|
|
201
|
+
confidence: z.enum(["low", "medium", "high"]).default("medium"),
|
|
202
|
+
reasons: z.array(z.string()).default([]),
|
|
203
|
+
});
|
|
359
204
|
export const healthEntrySchema = z.object({
|
|
360
205
|
id: z.string().min(1),
|
|
361
206
|
github: githubMetadataSchema.optional(),
|
|
362
|
-
health:
|
|
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
|
-
}),
|
|
207
|
+
health: healthBlockSchema,
|
|
372
208
|
});
|
|
373
209
|
export const healthFileSchema = z.union([
|
|
374
210
|
z.array(healthEntrySchema),
|
|
@@ -391,7 +227,7 @@ export const decisionsFileSchema = z.union([
|
|
|
391
227
|
z.object({ decisions: z.array(decisionSchema) }),
|
|
392
228
|
]);
|
|
393
229
|
// ──────────────────────────────────────────────────────────────────────
|
|
394
|
-
// Overrides (manual corrections to imported
|
|
230
|
+
// Overrides (manual corrections to imported records)
|
|
395
231
|
// ──────────────────────────────────────────────────────────────────────
|
|
396
232
|
export const overrideSchema = z.object({
|
|
397
233
|
id: z.string().min(1),
|
|
@@ -402,36 +238,225 @@ export const overridesFileSchema = z.union([
|
|
|
402
238
|
z.object({ overrides: z.array(overrideSchema) }),
|
|
403
239
|
]);
|
|
404
240
|
// ──────────────────────────────────────────────────────────────────────
|
|
405
|
-
//
|
|
241
|
+
// Curation: shared block
|
|
242
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
243
|
+
const curationBlockSchema = z
|
|
244
|
+
.object({
|
|
245
|
+
reviewed: z.boolean().default(false),
|
|
246
|
+
reviewedBy: z.string().optional(),
|
|
247
|
+
reviewedAt: z.string().optional(),
|
|
248
|
+
notes: z.string().optional(),
|
|
249
|
+
labels: z.array(appLabelSchema).default([]),
|
|
250
|
+
lenses: z.array(z.string()).default([]),
|
|
251
|
+
})
|
|
252
|
+
.default({ reviewed: false, labels: [], lenses: [] });
|
|
253
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
254
|
+
// Resource base (shared by all blueprints)
|
|
255
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
256
|
+
const resourceBaseSchema = z.object({
|
|
257
|
+
slug: z.string().min(1),
|
|
258
|
+
description: z.string().default(""),
|
|
259
|
+
category: z.string().min(1).default("uncategorized"),
|
|
260
|
+
tags: z.array(z.string()).default([]),
|
|
261
|
+
links: linksSchema,
|
|
262
|
+
content: z.string().optional(), // path to markdown body under content/records/
|
|
263
|
+
source: z
|
|
264
|
+
.object({
|
|
265
|
+
type: z.enum(["manual", "github-topic", "awesome-list", "submit", "import"])
|
|
266
|
+
.default("manual"),
|
|
267
|
+
file: z.string().optional(),
|
|
268
|
+
url: z.string().optional(),
|
|
269
|
+
provider: z.string().optional(),
|
|
270
|
+
owner: z.string().optional(),
|
|
271
|
+
repo: z.string().optional(),
|
|
272
|
+
})
|
|
273
|
+
.default({ type: "manual" }),
|
|
274
|
+
curation: curationBlockSchema,
|
|
275
|
+
scores: scoreSchema.default({}),
|
|
276
|
+
/**
|
|
277
|
+
* Effective visibility after any decisions.yml override. For project
|
|
278
|
+
* records the canonical signal lives in `health.visibility`; for
|
|
279
|
+
* resource-hub / ecosystem-map records (which have no `health`
|
|
280
|
+
* block) the top-level `visibility` is the source of truth.
|
|
281
|
+
* Defaults to "keep" so existing YAML files without the field
|
|
282
|
+
* continue to validate.
|
|
283
|
+
*/
|
|
284
|
+
visibility: decisionVisibilitySchema.default("keep"),
|
|
285
|
+
});
|
|
286
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
287
|
+
// Blueprint: project-directory — kind: project
|
|
406
288
|
// ──────────────────────────────────────────────────────────────────────
|
|
407
|
-
export const
|
|
289
|
+
export const projectRecordSchema = resourceBaseSchema.extend({
|
|
290
|
+
kind: z.literal("project"),
|
|
408
291
|
name: z.string().min(1),
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
292
|
+
projectType: projectTypeSchema.optional(),
|
|
293
|
+
stack: z.string().optional(),
|
|
294
|
+
stacks: z.array(z.string()).default([]),
|
|
295
|
+
platforms: z.array(z.string()).default([]),
|
|
296
|
+
difficulty: z.enum(["beginner", "intermediate", "advanced"]).optional(),
|
|
297
|
+
codebaseSize: z.enum(["small", "medium", "large", "huge"]).optional(),
|
|
298
|
+
// Canonical repo URL. Distinct from `links.github`, which is the
|
|
299
|
+
// human-facing link on the page; `repoUrl` is the single source
|
|
300
|
+
// for owner/repo extraction, avatar fallback, and "view repo" CTAs.
|
|
412
301
|
repoUrl: z.string().url().optional(),
|
|
413
|
-
|
|
302
|
+
// Optional project logo/avatar URL. Falls back to the GitHub
|
|
303
|
+
// owner avatar (when `repoUrl` is a github.com URL) or to the
|
|
304
|
+
// first-2-letters initials placeholder.
|
|
305
|
+
logoUrl: z.string().url().optional(),
|
|
306
|
+
bestFor: z.array(z.string()).default([]),
|
|
307
|
+
whyListed: z.array(z.string()).default([]),
|
|
308
|
+
caveats: z.array(z.string()).default([]),
|
|
309
|
+
distribution: z
|
|
310
|
+
.object({
|
|
311
|
+
channels: z
|
|
312
|
+
.array(z
|
|
313
|
+
.object({
|
|
314
|
+
type: z.string().min(1),
|
|
315
|
+
platform: z.string().optional(),
|
|
316
|
+
label: z.string().optional(),
|
|
317
|
+
url: z.string().url(),
|
|
318
|
+
verified: z.boolean().optional(),
|
|
319
|
+
notes: z.string().optional(),
|
|
320
|
+
})
|
|
321
|
+
.passthrough())
|
|
322
|
+
.default([]),
|
|
323
|
+
})
|
|
324
|
+
.default({ channels: [] }),
|
|
325
|
+
github: z
|
|
326
|
+
.object({
|
|
327
|
+
repository: githubRepositorySchema.optional(),
|
|
328
|
+
languages: z.record(z.string(), z.number()).optional(),
|
|
329
|
+
latestRelease: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
330
|
+
activity: z.record(z.string(), z.unknown()).optional(),
|
|
331
|
+
files: z.record(z.string(), z.boolean()).optional(),
|
|
332
|
+
labels: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
333
|
+
sync: z.record(z.string(), z.unknown()).optional(),
|
|
334
|
+
})
|
|
335
|
+
.passthrough()
|
|
336
|
+
.optional(),
|
|
337
|
+
health: healthBlockSchema.optional(),
|
|
338
|
+
});
|
|
339
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
340
|
+
// Blueprint: resource-hub — kind: resource
|
|
341
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
342
|
+
export const resourceRecordSchema = resourceBaseSchema.extend({
|
|
343
|
+
kind: z.literal("resource"),
|
|
344
|
+
title: z.string().min(1),
|
|
345
|
+
type: resourceTypeSchema,
|
|
346
|
+
topic: z.string().min(1),
|
|
347
|
+
related: z.array(z.string()).default([]), // slugs of related resources
|
|
348
|
+
publishedAt: z.string().optional(),
|
|
349
|
+
author: z.string().optional(),
|
|
350
|
+
});
|
|
351
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
352
|
+
// Blueprint: ecosystem-map — kind: entity
|
|
353
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
354
|
+
export const entityRecordSchema = resourceBaseSchema.extend({
|
|
355
|
+
kind: z.literal("entity"),
|
|
356
|
+
name: z.string().min(1),
|
|
357
|
+
type: entityTypeSchema,
|
|
358
|
+
founded: z.string().optional(),
|
|
359
|
+
location: z.string().optional(),
|
|
360
|
+
members: z.number().int().nonnegative().optional(),
|
|
361
|
+
parent: z.string().optional(), // slug of parent entity
|
|
362
|
+
});
|
|
363
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
364
|
+
// Discriminated union: Resource
|
|
365
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
366
|
+
export const resourceSchema = z.discriminatedUnion("kind", [
|
|
367
|
+
projectRecordSchema,
|
|
368
|
+
resourceRecordSchema,
|
|
369
|
+
entityRecordSchema,
|
|
370
|
+
]);
|
|
371
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
372
|
+
// Records file (V1: one resource per file)
|
|
373
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
374
|
+
//
|
|
375
|
+
// V1 is intentionally narrow: one YAML file = one resource. The
|
|
376
|
+
// filename (minus `.yml`) is the canonical slug. This keeps PR
|
|
377
|
+
// review clean, removes an entire class of "which slug wins" bugs,
|
|
378
|
+
// and matches what every contributor actually wants to read.
|
|
379
|
+
//
|
|
380
|
+
// The previous "array or { records: [...] }" shape leaked into the
|
|
381
|
+
// parser and forced every consumer to call `unwrapRecords`. The
|
|
382
|
+
// `resourceSchema.parse(raw)` call below is now a direct mapping
|
|
383
|
+
// parse; multi-record files are a V2 feature and will live behind
|
|
384
|
+
// a separate `recordsBundleSchema`.
|
|
385
|
+
export const recordsFileSchema = resourceSchema;
|
|
386
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
387
|
+
// Grove config
|
|
388
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
389
|
+
export const navItemSchema = z.object({
|
|
390
|
+
label: z.string().min(1),
|
|
391
|
+
href: z.string().min(1),
|
|
392
|
+
});
|
|
393
|
+
export const githubIntegrationSchema = z.union([
|
|
394
|
+
z.boolean(),
|
|
395
|
+
z.object({
|
|
396
|
+
metadata: z.boolean().default(false),
|
|
397
|
+
contributors: z.boolean().default(false),
|
|
398
|
+
health: z.boolean().default(false),
|
|
399
|
+
}),
|
|
400
|
+
]);
|
|
401
|
+
export const themeSchema = z.object({
|
|
402
|
+
primaryColor: z.string().default("#16a34a"),
|
|
403
|
+
radius: z.enum(["none", "soft", "round"]).default("soft"),
|
|
404
|
+
density: z.enum(["compact", "comfortable", "spacious"]).default("comfortable"),
|
|
405
|
+
containerWidth: z.string().default("72rem"),
|
|
406
|
+
});
|
|
407
|
+
export const componentOverrideSchema = z.object({
|
|
408
|
+
Header: z.string().optional(),
|
|
409
|
+
Footer: z.string().optional(),
|
|
410
|
+
Hero: z.string().optional(),
|
|
411
|
+
ItemCard: z.string().optional(),
|
|
412
|
+
DetailHeader: z.string().optional(),
|
|
413
|
+
});
|
|
414
|
+
export const groveConfigSchema = z.object({
|
|
415
|
+
blueprint: blueprintSchema.default("project-directory"),
|
|
416
|
+
site: z.object({
|
|
417
|
+
name: z.string().min(1),
|
|
418
|
+
tagline: z.string().default("A growing community knowledge site."),
|
|
419
|
+
description: z.string().optional(),
|
|
420
|
+
url: z.string().url().optional(),
|
|
421
|
+
repoUrl: z.string().url().optional(),
|
|
422
|
+
}),
|
|
423
|
+
nav: z.array(navItemSchema).default([]),
|
|
424
|
+
facets: z.array(z.string()).default(["category", "tags"]),
|
|
425
|
+
integrations: z
|
|
426
|
+
.object({
|
|
427
|
+
github: githubIntegrationSchema.default(false),
|
|
428
|
+
})
|
|
429
|
+
.default({ github: false }),
|
|
430
|
+
theme: themeSchema.default({
|
|
431
|
+
primaryColor: "#16a34a",
|
|
432
|
+
radius: "soft",
|
|
433
|
+
density: "comfortable",
|
|
434
|
+
containerWidth: "72rem",
|
|
435
|
+
}),
|
|
436
|
+
components: componentOverrideSchema.default({}),
|
|
414
437
|
paths: z
|
|
415
438
|
.object({
|
|
416
|
-
sourcesDir: z.string().default("sources"),
|
|
417
439
|
dataDir: z.string().default("data"),
|
|
418
440
|
contentDir: z.string().default("content"),
|
|
419
|
-
|
|
441
|
+
recordsDir: z.string().default("data/records"),
|
|
442
|
+
pagesDir: z.string().default("content/pages"),
|
|
443
|
+
bodiesDir: z.string().default("content/records"),
|
|
444
|
+
publicDir: z.string().default("public"),
|
|
420
445
|
taxonomyDir: z.string().default("data/taxonomy"),
|
|
421
446
|
generatedDir: z.string().default("data/generated"),
|
|
422
|
-
items: z.string().default("data/items.yml"),
|
|
423
447
|
health: z.string().default("data/health.yml"),
|
|
424
448
|
decisions: z.string().default("data/decisions.yml"),
|
|
425
449
|
overrides: z.string().default("data/overrides.yml"),
|
|
426
450
|
})
|
|
427
451
|
.default({
|
|
428
|
-
sourcesDir: "sources",
|
|
429
452
|
dataDir: "data",
|
|
430
453
|
contentDir: "content",
|
|
431
|
-
|
|
454
|
+
recordsDir: "data/records",
|
|
455
|
+
pagesDir: "content/pages",
|
|
456
|
+
bodiesDir: "content/records",
|
|
457
|
+
publicDir: "public",
|
|
432
458
|
taxonomyDir: "data/taxonomy",
|
|
433
459
|
generatedDir: "data/generated",
|
|
434
|
-
items: "data/items.yml",
|
|
435
460
|
health: "data/health.yml",
|
|
436
461
|
decisions: "data/decisions.yml",
|
|
437
462
|
overrides: "data/overrides.yml",
|
|
@@ -440,9 +465,6 @@ export const curatedConfigSchema = z.object({
|
|
|
440
465
|
// ──────────────────────────────────────────────────────────────────────
|
|
441
466
|
// Helpers
|
|
442
467
|
// ──────────────────────────────────────────────────────────────────────
|
|
443
|
-
export function unwrapItems(value) {
|
|
444
|
-
return Array.isArray(value) ? value : value.items;
|
|
445
|
-
}
|
|
446
468
|
export function unwrapHealth(value) {
|
|
447
469
|
return Array.isArray(value) ? value : value.health;
|
|
448
470
|
}
|
|
@@ -452,17 +474,139 @@ export function unwrapDecisions(value) {
|
|
|
452
474
|
export function unwrapOverrides(value) {
|
|
453
475
|
return Array.isArray(value) ? value : value.overrides;
|
|
454
476
|
}
|
|
455
|
-
|
|
456
|
-
|
|
477
|
+
/**
|
|
478
|
+
* Parse a single record YAML mapping into a typed Resource. The
|
|
479
|
+
* `kind` field on the YAML selects which blueprint schema to apply.
|
|
480
|
+
*/
|
|
481
|
+
export function parseRecordYaml(text, fileSlug) {
|
|
482
|
+
const raw = parse(text) ?? {};
|
|
483
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
484
|
+
throw new Error(`${fileSlug}: record file must contain a YAML mapping`);
|
|
485
|
+
}
|
|
486
|
+
return raw;
|
|
487
|
+
}
|
|
488
|
+
export function stringifyRecordYaml(record) {
|
|
489
|
+
return stringify(record, {
|
|
490
|
+
lineWidth: 100,
|
|
491
|
+
singleQuote: false,
|
|
492
|
+
defaultStringType: "PLAIN",
|
|
493
|
+
});
|
|
457
494
|
}
|
|
458
|
-
function
|
|
459
|
-
if (!
|
|
460
|
-
return
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
463
|
-
return
|
|
464
|
-
return
|
|
495
|
+
export function getOwnerRepoFromUrl(repoUrl) {
|
|
496
|
+
if (!repoUrl)
|
|
497
|
+
return null;
|
|
498
|
+
const m = String(repoUrl).match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
499
|
+
if (!m)
|
|
500
|
+
return null;
|
|
501
|
+
return { owner: m[1], repo: m[2].replace(/\.git$/, "") };
|
|
465
502
|
}
|
|
503
|
+
export function isIndexProject(r) {
|
|
504
|
+
return r.kind === "project";
|
|
505
|
+
}
|
|
506
|
+
export function isIndexResource(r) {
|
|
507
|
+
return r.kind === "resource";
|
|
508
|
+
}
|
|
509
|
+
export function isIndexEntity(r) {
|
|
510
|
+
return r.kind === "entity";
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Project a Resource to a stable, search-index-friendly summary
|
|
514
|
+
* object. Strips out blueprint-specific heavy fields and leaves
|
|
515
|
+
* only what the public list/detail UIs need.
|
|
516
|
+
*
|
|
517
|
+
* The return type is the discriminated `IndexRecord` union. The
|
|
518
|
+
* function body uses a single `kind` literal cast per branch
|
|
519
|
+
* because the `record.kind` is a `ResourceKind` (the union of
|
|
520
|
+
* all three) and TypeScript cannot narrow the literal from a
|
|
521
|
+
* runtime check alone. The casts are safe because each branch
|
|
522
|
+
* is gated on the literal string compare.
|
|
523
|
+
*/
|
|
524
|
+
export function toIndexRecord(record) {
|
|
525
|
+
const base = {
|
|
526
|
+
slug: record.slug,
|
|
527
|
+
kind: record.kind,
|
|
528
|
+
category: record.category,
|
|
529
|
+
tags: record.tags ?? [],
|
|
530
|
+
links: record.links,
|
|
531
|
+
description: record.description,
|
|
532
|
+
content: record.content,
|
|
533
|
+
curation: record.curation,
|
|
534
|
+
};
|
|
535
|
+
if (record.kind === "project") {
|
|
536
|
+
const r = record;
|
|
537
|
+
return {
|
|
538
|
+
...base,
|
|
539
|
+
kind: "project",
|
|
540
|
+
name: r.name,
|
|
541
|
+
stack: r.stack,
|
|
542
|
+
stacks: r.stacks ?? [],
|
|
543
|
+
platforms: r.platforms ?? [],
|
|
544
|
+
projectType: r.projectType,
|
|
545
|
+
repoUrl: r.repoUrl,
|
|
546
|
+
logoUrl: r.logoUrl,
|
|
547
|
+
difficulty: r.difficulty,
|
|
548
|
+
codebaseSize: r.codebaseSize,
|
|
549
|
+
bestFor: r.bestFor,
|
|
550
|
+
whyListed: r.whyListed,
|
|
551
|
+
caveats: r.caveats,
|
|
552
|
+
// Health surfaced alongside the record so list/detail UIs can
|
|
553
|
+
// show staleness/curation tier without a second lookup.
|
|
554
|
+
health: r.health,
|
|
555
|
+
// Visibility comes from either the health block (auto-derived
|
|
556
|
+
// from signals) or the curation override (`decisions.yml`).
|
|
557
|
+
// The generate step folds the latter in before serialising,
|
|
558
|
+
// so this is a best-effort projection of the current state.
|
|
559
|
+
visibility: r.health?.visibility ?? "keep",
|
|
560
|
+
github: r.github?.repository
|
|
561
|
+
? {
|
|
562
|
+
fullName: r.github.repository.full_name,
|
|
563
|
+
stars: r.github.repository.stargazers_count ?? 0,
|
|
564
|
+
forks: r.github.repository.forks_count ?? 0,
|
|
565
|
+
openIssues: r.github.repository.open_issues_count ?? 0,
|
|
566
|
+
language: r.github.repository.language ?? null,
|
|
567
|
+
pushedAt: r.github.repository.pushed_at ?? null,
|
|
568
|
+
archived: Boolean(r.github.repository.archived),
|
|
569
|
+
license: r.github.repository.license?.spdx_id ?? null,
|
|
570
|
+
topics: r.github.repository.topics ?? [],
|
|
571
|
+
}
|
|
572
|
+
: undefined,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (record.kind === "resource") {
|
|
576
|
+
const r = record;
|
|
577
|
+
return {
|
|
578
|
+
...base,
|
|
579
|
+
kind: "resource",
|
|
580
|
+
title: r.title,
|
|
581
|
+
type: r.type,
|
|
582
|
+
topic: r.topic,
|
|
583
|
+
related: r.related,
|
|
584
|
+
publishedAt: r.publishedAt,
|
|
585
|
+
author: r.author,
|
|
586
|
+
// Non-project records store effective visibility at the top
|
|
587
|
+
// level (no `health` block); applyDecision has already merged
|
|
588
|
+
// any decisions.yml override into `r.visibility` by the time
|
|
589
|
+
// we get here.
|
|
590
|
+
visibility: r.visibility,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
// entity
|
|
594
|
+
const e = record;
|
|
595
|
+
return {
|
|
596
|
+
...base,
|
|
597
|
+
kind: "entity",
|
|
598
|
+
name: e.name,
|
|
599
|
+
type: e.type,
|
|
600
|
+
founded: e.founded,
|
|
601
|
+
location: e.location,
|
|
602
|
+
members: e.members,
|
|
603
|
+
parent: e.parent,
|
|
604
|
+
visibility: e.visibility,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
608
|
+
// Health signal derivation
|
|
609
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
466
610
|
function daysSince(value) {
|
|
467
611
|
if (!value)
|
|
468
612
|
return Infinity;
|
|
@@ -472,7 +616,7 @@ function daysSince(value) {
|
|
|
472
616
|
return (Date.now() - d.valueOf()) / 86_400_000;
|
|
473
617
|
}
|
|
474
618
|
export function healthFromSignals(params) {
|
|
475
|
-
const { repo, monthlyCommits
|
|
619
|
+
const { repo, monthlyCommits } = params;
|
|
476
620
|
const stars = repo?.stargazers_count ?? 0;
|
|
477
621
|
const lastCommitAt = repo?.pushed_at;
|
|
478
622
|
const archived = Boolean(repo?.archived);
|
|
@@ -503,10 +647,13 @@ export function healthFromSignals(params) {
|
|
|
503
647
|
? "listed"
|
|
504
648
|
: "experimental";
|
|
505
649
|
return {
|
|
506
|
-
status
|
|
650
|
+
status,
|
|
507
651
|
tier,
|
|
508
652
|
visibility: tier === "hidden" ? "hide" : "keep",
|
|
509
|
-
cleanupCandidate: status === "stale" ||
|
|
653
|
+
cleanupCandidate: status === "stale" ||
|
|
654
|
+
status === "archived" ||
|
|
655
|
+
status === "inactive" ||
|
|
656
|
+
status === "unavailable",
|
|
510
657
|
staleReason: status === "inactive"
|
|
511
658
|
? "no_commits_24_months"
|
|
512
659
|
: status === "stale"
|
|
@@ -518,212 +665,4 @@ export function healthFromSignals(params) {
|
|
|
518
665
|
: null,
|
|
519
666
|
};
|
|
520
667
|
}
|
|
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
668
|
//# sourceMappingURL=schema.js.map
|