@grove-dev/core 0.7.0 → 0.9.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 (159) hide show
  1. package/dist/audit.d.ts +3 -3
  2. package/dist/audit.d.ts.map +1 -1
  3. package/dist/audit.js +25 -6
  4. package/dist/audit.js.map +1 -1
  5. package/dist/awesome-readme.d.ts.map +1 -1
  6. package/dist/awesome-readme.js +31 -31
  7. package/dist/awesome-readme.js.map +1 -1
  8. package/dist/build-data.d.ts +1 -1
  9. package/dist/build-data.d.ts.map +1 -1
  10. package/dist/build-data.js +110 -80
  11. package/dist/build-data.js.map +1 -1
  12. package/dist/candidate.d.ts +34 -0
  13. package/dist/candidate.d.ts.map +1 -0
  14. package/dist/candidate.js +128 -0
  15. package/dist/candidate.js.map +1 -0
  16. package/dist/collections-io.d.ts +1 -1
  17. package/dist/collections-io.js +8 -8
  18. package/dist/collections-io.js.map +1 -1
  19. package/dist/collections.d.ts +2 -2
  20. package/dist/collections.d.ts.map +1 -1
  21. package/dist/collections.js +8 -9
  22. package/dist/collections.js.map +1 -1
  23. package/dist/collector.d.ts +1 -1
  24. package/dist/collector.d.ts.map +1 -1
  25. package/dist/collector.js +2 -2
  26. package/dist/collector.js.map +1 -1
  27. package/dist/config.d.ts +1 -1
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +4 -4
  30. package/dist/config.js.map +1 -1
  31. package/dist/content-body.d.ts.map +1 -1
  32. package/dist/content-body.js +22 -22
  33. package/dist/content-body.js.map +1 -1
  34. package/dist/contributors.d.ts.map +1 -1
  35. package/dist/contributors.js +22 -23
  36. package/dist/contributors.js.map +1 -1
  37. package/dist/decisions.d.ts +1 -1
  38. package/dist/decisions.d.ts.map +1 -1
  39. package/dist/decisions.js +18 -20
  40. package/dist/decisions.js.map +1 -1
  41. package/dist/directory-client.d.ts +6 -4
  42. package/dist/directory-client.d.ts.map +1 -1
  43. package/dist/directory-client.js +9 -4
  44. package/dist/directory-client.js.map +1 -1
  45. package/dist/directory-facets.d.ts +1 -1
  46. package/dist/directory-facets.d.ts.map +1 -1
  47. package/dist/directory-facets.js +35 -17
  48. package/dist/directory-facets.js.map +1 -1
  49. package/dist/directory-filter-keys.d.ts +53 -0
  50. package/dist/directory-filter-keys.d.ts.map +1 -0
  51. package/dist/directory-filter-keys.js +66 -0
  52. package/dist/directory-filter-keys.js.map +1 -0
  53. package/dist/directory-format.d.ts.map +1 -1
  54. package/dist/directory-format.js +15 -15
  55. package/dist/directory-format.js.map +1 -1
  56. package/dist/directory-lenses.d.ts +1 -1
  57. package/dist/directory-lenses.d.ts.map +1 -1
  58. package/dist/directory-lenses.js +66 -61
  59. package/dist/directory-lenses.js.map +1 -1
  60. package/dist/directory-repo.js +2 -2
  61. package/dist/directory-scores.d.ts.map +1 -1
  62. package/dist/directory-scores.js +25 -25
  63. package/dist/directory-scores.js.map +1 -1
  64. package/dist/directory-search.d.ts +4 -4
  65. package/dist/directory-search.d.ts.map +1 -1
  66. package/dist/directory-search.js +129 -110
  67. package/dist/directory-search.js.map +1 -1
  68. package/dist/directory-taxonomy.d.ts +1 -1
  69. package/dist/directory-taxonomy.d.ts.map +1 -1
  70. package/dist/directory-taxonomy.js +2 -2
  71. package/dist/directory-taxonomy.js.map +1 -1
  72. package/dist/enrich.d.ts.map +1 -1
  73. package/dist/enrich.js +15 -12
  74. package/dist/enrich.js.map +1 -1
  75. package/dist/evidence.d.ts +33 -0
  76. package/dist/evidence.d.ts.map +1 -0
  77. package/dist/evidence.js +181 -0
  78. package/dist/evidence.js.map +1 -0
  79. package/dist/github-client.js +6 -6
  80. package/dist/github.d.ts +9 -1
  81. package/dist/github.d.ts.map +1 -1
  82. package/dist/github.js +40 -13
  83. package/dist/github.js.map +1 -1
  84. package/dist/health.d.ts +1 -1
  85. package/dist/health.js +44 -44
  86. package/dist/host.js +2 -2
  87. package/dist/import-types.d.ts +24 -0
  88. package/dist/import-types.d.ts.map +1 -0
  89. package/dist/import-types.js +2 -0
  90. package/dist/import-types.js.map +1 -0
  91. package/dist/import-write.d.ts +12 -0
  92. package/dist/import-write.d.ts.map +1 -0
  93. package/dist/import-write.js +39 -0
  94. package/dist/import-write.js.map +1 -0
  95. package/dist/importer.d.ts +5 -3
  96. package/dist/importer.d.ts.map +1 -1
  97. package/dist/importer.js +4 -2
  98. package/dist/importer.js.map +1 -1
  99. package/dist/index.d.ts +74 -63
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +94 -75
  102. package/dist/index.js.map +1 -1
  103. package/dist/io.js +8 -8
  104. package/dist/llms.d.ts +1 -1
  105. package/dist/llms.d.ts.map +1 -1
  106. package/dist/llms.js +65 -64
  107. package/dist/llms.js.map +1 -1
  108. package/dist/markdown.d.ts +3 -33
  109. package/dist/markdown.d.ts.map +1 -1
  110. package/dist/markdown.js +2 -39
  111. package/dist/markdown.js.map +1 -1
  112. package/dist/og-image.d.ts +8 -8
  113. package/dist/og-image.d.ts.map +1 -1
  114. package/dist/og-image.js +91 -84
  115. package/dist/og-image.js.map +1 -1
  116. package/dist/page-document.d.ts +8 -8
  117. package/dist/page-document.d.ts.map +1 -1
  118. package/dist/page-document.js +48 -54
  119. package/dist/page-document.js.map +1 -1
  120. package/dist/parseReadme.d.ts.map +1 -1
  121. package/dist/parseReadme.js +9 -11
  122. package/dist/parseReadme.js.map +1 -1
  123. package/dist/prepare.d.ts +5 -5
  124. package/dist/prepare.d.ts.map +1 -1
  125. package/dist/prepare.js +32 -38
  126. package/dist/prepare.js.map +1 -1
  127. package/dist/readme-health.d.ts +33 -0
  128. package/dist/readme-health.d.ts.map +1 -0
  129. package/dist/readme-health.js +88 -0
  130. package/dist/readme-health.js.map +1 -0
  131. package/dist/related.d.ts +1 -1
  132. package/dist/repository-health.d.ts +11 -0
  133. package/dist/repository-health.d.ts.map +1 -0
  134. package/dist/repository-health.js +94 -0
  135. package/dist/repository-health.js.map +1 -0
  136. package/dist/schema.d.ts +72 -30
  137. package/dist/schema.d.ts.map +1 -1
  138. package/dist/schema.js +156 -144
  139. package/dist/schema.js.map +1 -1
  140. package/dist/site-artifacts.js +1 -1
  141. package/dist/sitemap.d.ts +2 -2
  142. package/dist/sitemap.d.ts.map +1 -1
  143. package/dist/sitemap.js +39 -39
  144. package/dist/sitemap.js.map +1 -1
  145. package/dist/slug.js +4 -4
  146. package/dist/sync-icons.js +15 -15
  147. package/dist/taxonomy-inference.d.ts +27 -0
  148. package/dist/taxonomy-inference.d.ts.map +1 -0
  149. package/dist/taxonomy-inference.js +38 -0
  150. package/dist/taxonomy-inference.js.map +1 -0
  151. package/dist/validate.d.ts +3 -3
  152. package/dist/validate.d.ts.map +1 -1
  153. package/dist/validate.js +73 -62
  154. package/dist/validate.js.map +1 -1
  155. package/dist/yaml.d.ts +47 -0
  156. package/dist/yaml.d.ts.map +1 -0
  157. package/dist/yaml.js +56 -0
  158. package/dist/yaml.js.map +1 -0
  159. package/package.json +6 -1
package/dist/schema.js CHANGED
@@ -1,6 +1,6 @@
1
- import { z } from "zod";
2
- import { parse, stringify } from "yaml";
3
- import { DEFAULT_FACETS, FACET_IDS } from "./directory-facets.js";
1
+ import { parse, stringify } from 'yaml';
2
+ import { z } from 'zod';
3
+ import { DEFAULT_FACETS, FACET_IDS } from './directory-facets.js';
4
4
  // ──────────────────────────────────────────────────────────────────────
5
5
  // Blueprints and kinds
6
6
  // ──────────────────────────────────────────────────────────────────────
@@ -10,91 +10,82 @@ import { DEFAULT_FACETS, FACET_IDS } from "./directory-facets.js";
10
10
  * V1 ships three fixed blueprints. They are not extensible — no custom
11
11
  * blueprint API in V1.
12
12
  */
13
- export const blueprintSchema = z.enum([
14
- "project-directory",
15
- "resource-hub",
16
- "ecosystem-map",
17
- ]);
13
+ export const blueprintSchema = z.enum(['project-directory', 'resource-hub', 'ecosystem-map']);
18
14
  /**
19
15
  * Each blueprint is bound to a single resource kind. Records are
20
16
  * discriminated by `kind`, which must match the blueprint of the site
21
17
  * that owns them.
22
18
  */
23
- export const resourceKindSchema = z.enum(["project", "resource", "entity"]);
19
+ export const resourceKindSchema = z.enum(['project', 'resource', 'entity']);
24
20
  export const blueprintKind = {
25
- "project-directory": "project",
26
- "resource-hub": "resource",
27
- "ecosystem-map": "entity",
21
+ 'project-directory': 'project',
22
+ 'resource-hub': 'resource',
23
+ 'ecosystem-map': 'entity',
28
24
  };
29
25
  // ──────────────────────────────────────────────────────────────────────
30
26
  // Health & decision labels
31
27
  // ──────────────────────────────────────────────────────────────────────
32
28
  export const healthStatusSchema = z.enum([
33
- "active",
34
- "mature",
35
- "stale",
36
- "inactive",
37
- "archived",
38
- "unknown",
39
- "historical",
40
- "needs_review",
41
- "quiet",
42
- "unavailable",
29
+ 'active',
30
+ 'mature',
31
+ 'stale',
32
+ 'inactive',
33
+ 'archived',
34
+ 'unknown',
35
+ 'historical',
36
+ 'needs_review',
37
+ 'quiet',
38
+ 'unavailable',
43
39
  ]);
44
40
  export const decisionVisibilitySchema = z.enum([
45
- "highlight",
46
- "keep",
47
- "needs_review",
48
- "hide",
49
- "remove",
50
- "historical",
51
- ]);
52
- export const healthTierSchema = z.enum([
53
- "curated",
54
- "listed",
55
- "experimental",
56
- "hidden",
41
+ 'highlight',
42
+ 'keep',
43
+ 'needs_review',
44
+ 'hide',
45
+ 'remove',
46
+ 'historical',
57
47
  ]);
48
+ export const healthTierSchema = z.enum(['curated', 'listed', 'experimental', 'hidden']);
58
49
  // ──────────────────────────────────────────────────────────────────────
59
50
  // Curation enums (shared)
60
51
  // ──────────────────────────────────────────────────────────────────────
61
52
  export const projectTypeSchema = z.enum([
62
- "real-app",
63
- "production",
64
- "reference",
65
- "library",
66
- "tool",
67
- "demo",
68
- "template",
69
- "historical",
53
+ 'real-app',
54
+ 'production',
55
+ 'reference',
56
+ 'library',
57
+ 'tool',
58
+ 'demo',
59
+ 'template',
60
+ 'historical',
70
61
  ]);
71
62
  export const resourceTypeSchema = z.enum([
72
- "guide",
73
- "comparison",
74
- "link",
75
- "explainer",
76
- "tool",
77
- "video",
78
- "article",
79
- "course",
80
- "book",
81
- "podcast",
82
- "other",
63
+ 'guide',
64
+ 'comparison',
65
+ 'link',
66
+ 'explainer',
67
+ 'tool',
68
+ 'video',
69
+ 'article',
70
+ 'course',
71
+ 'book',
72
+ 'podcast',
73
+ 'other',
83
74
  ]);
84
75
  export const entityTypeSchema = z.enum([
85
- "company",
86
- "organization",
87
- "community",
88
- "school",
89
- "university",
90
- "research-lab",
91
- "agency",
92
- "service",
93
- "product",
94
- "person",
95
- "other",
76
+ 'company',
77
+ 'organization',
78
+ 'community',
79
+ 'school',
80
+ 'university',
81
+ 'research-lab',
82
+ 'agency',
83
+ 'service',
84
+ 'product',
85
+ 'person',
86
+ 'other',
96
87
  ]);
97
- export const appLabelSchema = z.enum(["new", "hot", "mature", "featured"]);
88
+ export const appLabelSchema = z.enum(['new', 'hot', 'mature', 'featured']);
98
89
  export const scoreSchema = z.object({
99
90
  activity: z.number().min(0).max(100).optional(),
100
91
  maturity: z.number().min(0).max(100).optional(),
@@ -189,20 +180,17 @@ export const githubMetadataSchema = z.object({
189
180
  size: z.number().int().nonnegative().optional(),
190
181
  files: z.record(z.string(), z.boolean()).optional(),
191
182
  monthlyCommits: z
192
- .array(z.union([
193
- z.number(),
194
- z.object({ month: z.string(), commits: z.number() }),
195
- ]))
183
+ .array(z.union([z.number(), z.object({ month: z.string(), commits: z.number() })]))
196
184
  .optional(),
197
185
  });
198
186
  export const healthBlockSchema = z.object({
199
- status: healthStatusSchema.default("unknown"),
200
- maturity: z.enum(["experimental", "useful", "mature", "unknown"]).default("unknown"),
201
- tier: healthTierSchema.default("experimental"),
202
- visibility: decisionVisibilitySchema.default("keep"),
187
+ status: healthStatusSchema.default('unknown'),
188
+ maturity: z.enum(['experimental', 'useful', 'mature', 'unknown']).default('unknown'),
189
+ tier: healthTierSchema.default('experimental'),
190
+ visibility: decisionVisibilitySchema.default('keep'),
203
191
  cleanupCandidate: z.boolean().default(false),
204
192
  staleReason: z.string().nullable().optional(),
205
- confidence: z.enum(["low", "medium", "high"]).default("medium"),
193
+ confidence: z.enum(['low', 'medium', 'high']).default('medium'),
206
194
  reasons: z.array(z.string()).default([]),
207
195
  });
208
196
  export const healthEntrySchema = z.object({
@@ -259,7 +247,18 @@ const curationBlockSchema = z
259
247
  // ──────────────────────────────────────────────────────────────────────
260
248
  const resourceBaseSchema = z.object({
261
249
  slug: z.string().min(1),
262
- description: z.string().default(""),
250
+ /**
251
+ * When this record joined the directory — an ISO date or date-time.
252
+ *
253
+ * Distinct from `curation.reviewedAt`, which records when a human
254
+ * last reviewed the record. A record can be added today and reviewed
255
+ * never, or added last year and reviewed this morning; the
256
+ * `recently-added` sort and llms.txt's `added:` line want the former.
257
+ * Optional so records written before this field existed still
258
+ * validate — `recordAddedAt` falls back for them.
259
+ */
260
+ addedAt: z.string().optional(),
261
+ description: z.string().default(''),
263
262
  /**
264
263
  * Open Apps-written summary, surfaced as the lead paragraph on the
265
264
  * detail page above the original (GitHub-sourced) description.
@@ -275,21 +274,22 @@ const resourceBaseSchema = z.object({
275
274
  * overwriting what GitHub says the project is.
276
275
  */
277
276
  sourceDescription: z.string().optional(),
278
- category: z.string().min(1).default("uncategorized"),
277
+ category: z.string().min(1).default('uncategorized'),
279
278
  tags: z.array(z.string()).default([]),
280
279
  links: linksSchema,
281
280
  content: z.string().optional(), // path to markdown body under content/records/
282
281
  source: z
283
282
  .object({
284
- type: z.enum(["manual", "github-topic", "awesome-list", "submit", "import"])
285
- .default("manual"),
283
+ type: z
284
+ .enum(['manual', 'github-topic', 'awesome-list', 'submit', 'import'])
285
+ .default('manual'),
286
286
  file: z.string().optional(),
287
287
  url: z.string().optional(),
288
288
  provider: z.string().optional(),
289
289
  owner: z.string().optional(),
290
290
  repo: z.string().optional(),
291
291
  })
292
- .default({ type: "manual" }),
292
+ .default({ type: 'manual' }),
293
293
  curation: curationBlockSchema,
294
294
  scores: scoreSchema.default({}),
295
295
  /**
@@ -300,13 +300,26 @@ const resourceBaseSchema = z.object({
300
300
  * Defaults to "keep" so existing YAML files without the field
301
301
  * continue to validate.
302
302
  */
303
- visibility: decisionVisibilitySchema.default("keep"),
303
+ visibility: decisionVisibilitySchema.default('keep'),
304
+ /**
305
+ * Curator override for the detail page's `<title>`/meta description.
306
+ * Mirrors `CollectionSeo` (`collections.ts`) — when set, it wins
307
+ * verbatim over the computed title/descriptor so a curator can hand-
308
+ * write a sharper title than the generic "Open-source {category}
309
+ * {noun}" fallback for a specific record.
310
+ */
311
+ seo: z
312
+ .object({
313
+ title: z.string().optional(),
314
+ description: z.string().optional(),
315
+ })
316
+ .optional(),
304
317
  });
305
318
  // ──────────────────────────────────────────────────────────────────────
306
319
  // Blueprint: project-directory — kind: project
307
320
  // ──────────────────────────────────────────────────────────────────────
308
321
  export const projectRecordSchema = resourceBaseSchema.extend({
309
- kind: z.literal("project"),
322
+ kind: z.literal('project'),
310
323
  name: z.string().min(1),
311
324
  projectType: projectTypeSchema.optional(),
312
325
  stack: z.string().optional(),
@@ -316,8 +329,8 @@ export const projectRecordSchema = resourceBaseSchema.extend({
316
329
  // authored when a record is curated; otherwise derived from the
317
330
  // GitHub sync's `license.spdx_id` field at build time.
318
331
  licenses: z.array(z.string()).default([]),
319
- difficulty: z.enum(["beginner", "intermediate", "advanced"]).optional(),
320
- codebaseSize: z.enum(["small", "medium", "large", "huge"]).optional(),
332
+ difficulty: z.enum(['beginner', 'intermediate', 'advanced']).optional(),
333
+ codebaseSize: z.enum(['small', 'medium', 'large', 'huge']).optional(),
321
334
  // Canonical repo URL. Distinct from `links.github`, which is the
322
335
  // human-facing link on the page; `repoUrl` is the single source
323
336
  // for owner/repo extraction, avatar fallback, and "view repo" CTAs.
@@ -365,10 +378,7 @@ export const projectRecordSchema = resourceBaseSchema.extend({
365
378
  .object({
366
379
  repository: githubRepositorySchema.optional(),
367
380
  languages: z.record(z.string(), z.number()).optional(),
368
- latestRelease: z.record(z.string(), z.unknown()).nullable().optional(),
369
381
  activity: z.record(z.string(), z.unknown()).optional(),
370
- files: z.record(z.string(), z.boolean()).optional(),
371
- labels: z.array(z.record(z.string(), z.unknown())).optional(),
372
382
  sync: z.record(z.string(), z.unknown()).optional(),
373
383
  })
374
384
  .passthrough()
@@ -379,7 +389,7 @@ export const projectRecordSchema = resourceBaseSchema.extend({
379
389
  // Blueprint: resource-hub — kind: resource
380
390
  // ──────────────────────────────────────────────────────────────────────
381
391
  export const resourceRecordSchema = resourceBaseSchema.extend({
382
- kind: z.literal("resource"),
392
+ kind: z.literal('resource'),
383
393
  title: z.string().min(1),
384
394
  type: resourceTypeSchema,
385
395
  topic: z.string().min(1),
@@ -391,7 +401,7 @@ export const resourceRecordSchema = resourceBaseSchema.extend({
391
401
  // Blueprint: ecosystem-map — kind: entity
392
402
  // ──────────────────────────────────────────────────────────────────────
393
403
  export const entityRecordSchema = resourceBaseSchema.extend({
394
- kind: z.literal("entity"),
404
+ kind: z.literal('entity'),
395
405
  name: z.string().min(1),
396
406
  type: entityTypeSchema,
397
407
  founded: z.string().optional(),
@@ -402,7 +412,7 @@ export const entityRecordSchema = resourceBaseSchema.extend({
402
412
  // ──────────────────────────────────────────────────────────────────────
403
413
  // Discriminated union: Resource
404
414
  // ──────────────────────────────────────────────────────────────────────
405
- export const resourceSchema = z.discriminatedUnion("kind", [
415
+ export const resourceSchema = z.discriminatedUnion('kind', [
406
416
  projectRecordSchema,
407
417
  resourceRecordSchema,
408
418
  entityRecordSchema,
@@ -472,7 +482,7 @@ export const githubIntegrationSchema = z.union([
472
482
  * callers (`grove sync`, CI workflows) can gate each feature.
473
483
  */
474
484
  export function normalizeGithubIntegration(value) {
475
- if (typeof value === "boolean") {
485
+ if (typeof value === 'boolean') {
476
486
  return { metadata: value, contributors: value, health: value };
477
487
  }
478
488
  return {
@@ -489,23 +499,23 @@ export const themeSchema = z.object({
489
499
  */
490
500
  primaryColor: z
491
501
  .string()
492
- .regex(/^#[0-9a-fA-F]{3,8}$/, "theme.primaryColor must be a hex color such as #4f46e5")
502
+ .regex(/^#[0-9a-fA-F]{3,8}$/, 'theme.primaryColor must be a hex color such as #4f46e5')
493
503
  .optional(),
494
- radius: z.enum(["none", "soft", "round"]).default("soft"),
495
- density: z.enum(["compact", "comfortable", "spacious"]).default("comfortable"),
496
- containerWidth: z.string().default("72rem"),
504
+ radius: z.enum(['none', 'soft', 'round']).default('soft'),
505
+ density: z.enum(['compact', 'comfortable', 'spacious']).default('comfortable'),
506
+ containerWidth: z.string().default('72rem'),
497
507
  });
498
508
  // ──────────────────────────────────────────────────────────────────────
499
509
  // Audit manifest (consumed by `grove audit`)
500
510
  // ──────────────────────────────────────────────────────────────────────
501
511
  const auditPageTypeSchema = z.enum([
502
- "home",
503
- "directory",
504
- "collection",
505
- "record",
506
- "content",
507
- "empty",
508
- "404",
512
+ 'home',
513
+ 'directory',
514
+ 'collection',
515
+ 'record',
516
+ 'content',
517
+ 'empty',
518
+ '404',
509
519
  ]);
510
520
  const auditPageManifestEntrySchema = z.object({
511
521
  path: z.string().min(1),
@@ -542,10 +552,10 @@ export const readmeConfigSchema = z.object({
542
552
  showBrowseLink: z.boolean().default(true),
543
553
  });
544
554
  export const groveConfigSchema = z.object({
545
- blueprint: blueprintSchema.default("project-directory"),
555
+ blueprint: blueprintSchema.default('project-directory'),
546
556
  site: z.object({
547
557
  name: z.string().min(1),
548
- tagline: z.string().default("A growing community knowledge site."),
558
+ tagline: z.string().default('A growing community knowledge site.'),
549
559
  description: z.string().optional(),
550
560
  url: z.string().url().optional(),
551
561
  repoUrl: z.string().url().optional(),
@@ -565,7 +575,7 @@ export const groveConfigSchema = z.object({
565
575
  * BCP-47 language tag for the published site. Drives `<html lang>`,
566
576
  * `og:locale`, and JSON-LD `inLanguage`.
567
577
  */
568
- locale: z.string().default("en"),
578
+ locale: z.string().default('en'),
569
579
  /**
570
580
  * Twitter/X handle for the site (e.g. `@grove`). Emitted as
571
581
  * `twitter:site` on every page when set.
@@ -576,7 +586,7 @@ export const groveConfigSchema = z.object({
576
586
  .object({
577
587
  googleAnalyticsId: z
578
588
  .string()
579
- .regex(/^G-[A-Z0-9]+$/, "Expected a GA4 measurement ID such as G-XXXXXX")
589
+ .regex(/^G-[A-Z0-9]+$/, 'Expected a GA4 measurement ID such as G-XXXXXX')
580
590
  .optional(),
581
591
  })
582
592
  .default({}),
@@ -622,7 +632,7 @@ export const groveConfigSchema = z.object({
622
632
  facets: z
623
633
  .array(z.enum(FACET_IDS))
624
634
  .refine((facets) => new Set(facets).size === facets.length, {
625
- message: "browse.facets contains duplicate entries",
635
+ message: 'browse.facets contains duplicate entries',
626
636
  })
627
637
  .default([...DEFAULT_FACETS]),
628
638
  })
@@ -638,8 +648,8 @@ export const groveConfigSchema = z.object({
638
648
  .unknown()
639
649
  .optional()
640
650
  .refine((value) => value === undefined, {
641
- message: "`facets` moved to `browse.facets` in grove.config.ts. " +
642
- `Supported ids: ${FACET_IDS.join(", ")} (canonical spellings only).`,
651
+ message: '`facets` moved to `browse.facets` in grove.config.ts. ' +
652
+ `Supported ids: ${FACET_IDS.join(', ')} (canonical spellings only).`,
643
653
  }),
644
654
  integrations: z
645
655
  .object({
@@ -678,30 +688,30 @@ export const groveConfigSchema = z.object({
678
688
  readme: readmeConfigSchema.optional(),
679
689
  paths: z
680
690
  .object({
681
- dataDir: z.string().default("data"),
682
- contentDir: z.string().default("content"),
683
- recordsDir: z.string().default("data/records"),
684
- pagesDir: z.string().default("content/pages"),
685
- bodiesDir: z.string().default("content/records"),
686
- publicDir: z.string().default("public"),
687
- taxonomyDir: z.string().default("data/taxonomy"),
688
- generatedDir: z.string().default("data/generated"),
689
- health: z.string().default("data/health.yml"),
690
- decisions: z.string().default("data/decisions.yml"),
691
- overrides: z.string().default("data/overrides.yml"),
691
+ dataDir: z.string().default('data'),
692
+ contentDir: z.string().default('content'),
693
+ recordsDir: z.string().default('data/records'),
694
+ pagesDir: z.string().default('content/pages'),
695
+ bodiesDir: z.string().default('content/records'),
696
+ publicDir: z.string().default('public'),
697
+ taxonomyDir: z.string().default('data/taxonomy'),
698
+ generatedDir: z.string().default('data/generated'),
699
+ health: z.string().default('data/health.yml'),
700
+ decisions: z.string().default('data/decisions.yml'),
701
+ overrides: z.string().default('data/overrides.yml'),
692
702
  })
693
703
  .default({
694
- dataDir: "data",
695
- contentDir: "content",
696
- recordsDir: "data/records",
697
- pagesDir: "content/pages",
698
- bodiesDir: "content/records",
699
- publicDir: "public",
700
- taxonomyDir: "data/taxonomy",
701
- generatedDir: "data/generated",
702
- health: "data/health.yml",
703
- decisions: "data/decisions.yml",
704
- overrides: "data/overrides.yml",
704
+ dataDir: 'data',
705
+ contentDir: 'content',
706
+ recordsDir: 'data/records',
707
+ pagesDir: 'content/pages',
708
+ bodiesDir: 'content/records',
709
+ publicDir: 'public',
710
+ taxonomyDir: 'data/taxonomy',
711
+ generatedDir: 'data/generated',
712
+ health: 'data/health.yml',
713
+ decisions: 'data/decisions.yml',
714
+ overrides: 'data/overrides.yml',
705
715
  }),
706
716
  });
707
717
  // ──────────────────────────────────────────────────────────────────────
@@ -725,8 +735,8 @@ export function parseRecordYaml(text, fileSlug) {
725
735
  // record file cannot smuggle `!!binary` / `!!js/function` / etc.
726
736
  // into host objects. The default schema would interpret them.
727
737
  // (Implementation-checklist.md #27.)
728
- const raw = parse(text, { schema: "core" }) ?? {};
729
- if (typeof raw !== "object" || Array.isArray(raw)) {
738
+ const raw = parse(text, { schema: 'core' }) ?? {};
739
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
730
740
  throw new Error(`${fileSlug}: record file must contain a YAML mapping`);
731
741
  }
732
742
  return raw;
@@ -735,7 +745,7 @@ export function stringifyRecordYaml(record) {
735
745
  return stringify(record, {
736
746
  lineWidth: 100,
737
747
  singleQuote: false,
738
- defaultStringType: "PLAIN",
748
+ defaultStringType: 'PLAIN',
739
749
  });
740
750
  }
741
751
  export function getOwnerRepoFromUrl(repoUrl) {
@@ -748,16 +758,16 @@ export function getOwnerRepoFromUrl(repoUrl) {
748
758
  const repo = m[2];
749
759
  if (!owner || !repo)
750
760
  return null;
751
- return { owner, repo: repo.replace(/\.git$/, "") };
761
+ return { owner, repo: repo.replace(/\.git$/, '') };
752
762
  }
753
763
  export function isIndexProject(r) {
754
- return r.kind === "project";
764
+ return r.kind === 'project';
755
765
  }
756
766
  export function isIndexResource(r) {
757
- return r.kind === "resource";
767
+ return r.kind === 'resource';
758
768
  }
759
769
  export function isIndexEntity(r) {
760
- return r.kind === "entity";
770
+ return r.kind === 'entity';
761
771
  }
762
772
  /**
763
773
  * Project a Resource to a stable, search-index-friendly summary
@@ -775,21 +785,22 @@ export function toIndexRecord(record) {
775
785
  const base = {
776
786
  slug: record.slug,
777
787
  kind: record.kind,
788
+ addedAt: record.addedAt,
778
789
  category: record.category,
779
790
  tags: record.tags ?? [],
780
791
  links: record.links,
781
792
  description: record.description,
782
793
  content: record.content,
783
794
  curation: record.curation,
784
- licenses: record.kind === "project" ? record.licenses ?? [] : [],
795
+ licenses: record.kind === 'project' ? (record.licenses ?? []) : [],
785
796
  summary: record.summary,
786
797
  sourceDescription: record.sourceDescription,
787
798
  };
788
- if (record.kind === "project") {
799
+ if (record.kind === 'project') {
789
800
  const r = record;
790
801
  return {
791
802
  ...base,
792
- kind: "project",
803
+ kind: 'project',
793
804
  name: r.name,
794
805
  stack: r.stack,
795
806
  stacks: r.stacks ?? [],
@@ -810,10 +821,11 @@ export function toIndexRecord(record) {
810
821
  // from signals) or the curation override (`decisions.yml`).
811
822
  // The generate step folds the latter in before serialising,
812
823
  // so this is a best-effort projection of the current state.
813
- visibility: r.health?.visibility ?? "keep",
824
+ visibility: r.health?.visibility ?? 'keep',
814
825
  github: r.github?.repository
815
826
  ? {
816
827
  fullName: r.github.repository.full_name,
828
+ createdAt: r.github.repository.created_at ?? null,
817
829
  stars: r.github.repository.stargazers_count ?? 0,
818
830
  forks: r.github.repository.forks_count ?? 0,
819
831
  openIssues: r.github.repository.open_issues_count ?? 0,
@@ -826,11 +838,11 @@ export function toIndexRecord(record) {
826
838
  : undefined,
827
839
  };
828
840
  }
829
- if (record.kind === "resource") {
841
+ if (record.kind === 'resource') {
830
842
  const r = record;
831
843
  return {
832
844
  ...base,
833
- kind: "resource",
845
+ kind: 'resource',
834
846
  title: r.title,
835
847
  type: r.type,
836
848
  topic: r.topic,
@@ -848,7 +860,7 @@ export function toIndexRecord(record) {
848
860
  const e = record;
849
861
  return {
850
862
  ...base,
851
- kind: "entity",
863
+ kind: 'entity',
852
864
  name: e.name,
853
865
  type: e.type,
854
866
  founded: e.founded,