@cratis/cast 0.0.1 → 0.2.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 (117) hide show
  1. package/README.md +13 -1
  2. package/badges/RegistryMember.ts +18 -0
  3. package/badges/ResolvedTier.ts +17 -0
  4. package/badges/Tier.ts +14 -0
  5. package/badges/TierSource.ts +14 -0
  6. package/badges/badgeSvg.ts +137 -0
  7. package/badges/constants.ts +17 -0
  8. package/badges/cratisMark.ts +15 -0
  9. package/badges/index.ts +12 -0
  10. package/badges/registry.ts +68 -0
  11. package/badges/resolveTier.ts +24 -0
  12. package/cast-json.schema.json +1496 -0
  13. package/cast.schema.json +1113 -0
  14. package/config/CastConfig.ts +17 -0
  15. package/config/CastConfigError.ts +10 -0
  16. package/config/LiveStat.ts +17 -0
  17. package/config/ResolvedSection.ts +13 -0
  18. package/config/SectionId.ts +21 -0
  19. package/config/ThemeMode.ts +9 -0
  20. package/config/calendar.ts +34 -0
  21. package/config/castSchema.ts +263 -0
  22. package/config/index.ts +12 -0
  23. package/config/parseCast.ts +58 -0
  24. package/config/resolveSections.ts +48 -0
  25. package/github/GitHubSnapshot.ts +66 -0
  26. package/github/fetchGitHub.ts +235 -0
  27. package/github/gitHubSnapshotSchema.ts +58 -0
  28. package/github/parse.ts +63 -0
  29. package/index.ts +88 -0
  30. package/integration/CastData.ts +20 -0
  31. package/integration/CastOptions.ts +17 -0
  32. package/integration/Snapshot.ts +12 -0
  33. package/integration/loadRegistry.ts +37 -0
  34. package/integration/resolveData.ts +138 -0
  35. package/integration/snapshotFile.ts +23 -0
  36. package/integration/virtualModule.ts +43 -0
  37. package/machine/castJson.ts +77 -0
  38. package/machine/castJsonSchema.ts +35 -0
  39. package/machine/index.ts +6 -0
  40. package/machine/llmsText.ts +69 -0
  41. package/machine/ogImageSvg.ts +79 -0
  42. package/machine/profileFromCastJson.ts +214 -0
  43. package/machine/resumeJson.ts +87 -0
  44. package/package.json +80 -8
  45. package/registry/members.yaml +16 -0
  46. package/routes/404.astro +42 -0
  47. package/routes/apple-touch-icon.png.ts +17 -0
  48. package/routes/badge-card.svg.ts +13 -0
  49. package/routes/badge.svg.ts +11 -0
  50. package/routes/cast.json.ts +17 -0
  51. package/routes/cv.astro +9 -0
  52. package/routes/favicon.svg.ts +16 -0
  53. package/routes/index.astro +9 -0
  54. package/routes/llms.txt.ts +10 -0
  55. package/routes/og.png.ts +23 -0
  56. package/routes/resume.json.ts +13 -0
  57. package/routes/robots.txt.ts +10 -0
  58. package/site/components/Activity.astro +196 -0
  59. package/site/components/Agentic.astro +144 -0
  60. package/site/components/CareerReplay.astro +415 -0
  61. package/site/components/CastBadge.astro +132 -0
  62. package/site/components/CommandPalette.astro +122 -0
  63. package/site/components/Contact.astro +166 -0
  64. package/site/components/Cratis.astro +235 -0
  65. package/site/components/CratisMark.astro +18 -0
  66. package/site/components/EventModel.astro +265 -0
  67. package/site/components/Footer.astro +33 -0
  68. package/site/components/Hero.astro +211 -0
  69. package/site/components/Inline.astro +45 -0
  70. package/site/components/Monogram.astro +28 -0
  71. package/site/components/Nav.astro +127 -0
  72. package/site/components/Now.astro +116 -0
  73. package/site/components/Projects.astro +185 -0
  74. package/site/components/PullRequestFeed.astro +88 -0
  75. package/site/components/SectionHeader.astro +30 -0
  76. package/site/components/Skills.astro +44 -0
  77. package/site/components/Stats.astro +106 -0
  78. package/site/components/Talks.astro +76 -0
  79. package/site/components/Terminal.astro +164 -0
  80. package/site/components/Writing.astro +128 -0
  81. package/site/data.ts +11 -0
  82. package/site/layouts/Base.astro +156 -0
  83. package/site/lib/accent.ts +52 -0
  84. package/site/lib/blog.ts +58 -0
  85. package/site/lib/career.ts +148 -0
  86. package/site/lib/clipboard.ts +12 -0
  87. package/site/lib/contributions.ts +63 -0
  88. package/site/lib/github.ts +72 -0
  89. package/site/lib/inline.ts +79 -0
  90. package/site/lib/lifecycle.ts +44 -0
  91. package/site/lib/motion.ts +43 -0
  92. package/site/lib/nav.ts +49 -0
  93. package/site/lib/reltime.ts +33 -0
  94. package/site/lib/safeJson.ts +15 -0
  95. package/site/lib/sections.ts +41 -0
  96. package/site/lib/theme.ts +28 -0
  97. package/site/pages/CvPage.astro +379 -0
  98. package/site/pages/ProfilePage.astro +78 -0
  99. package/site/profile/CastHost.ts +32 -0
  100. package/site/profile/CastView.ts +96 -0
  101. package/site/profile/assertRenderable.ts +151 -0
  102. package/site/profile/clientCast.ts +117 -0
  103. package/site/profile/index.ts +8 -0
  104. package/site/scripts/ambient.ts +93 -0
  105. package/site/scripts/event-board.ts +403 -0
  106. package/site/scripts/event-model.ts +155 -0
  107. package/site/scripts/heatmap.ts +94 -0
  108. package/site/scripts/live.ts +155 -0
  109. package/site/scripts/palette.ts +313 -0
  110. package/site/scripts/replay.ts +252 -0
  111. package/site/scripts/terminal.ts +559 -0
  112. package/site/styles/global.css +508 -0
  113. package/virtual.d.ts +15 -0
  114. package/writing/WritingSnapshot.ts +20 -0
  115. package/writing/fetchWriting.ts +69 -0
  116. package/writing/parse.ts +72 -0
  117. package/writing/writingSnapshotSchema.ts +27 -0
package/README.md CHANGED
@@ -1,3 +1,15 @@
1
1
  # @cratis/cast
2
2
 
3
- Placeholder that reserves the package name for [Cratis Cast](https://github.com/Cratis/Cast). The first release is coming; do not install this version.
3
+ The engine behind [Cratis Cast](https://cast.cratis.io) sites: an Astro integration that turns one `cast.yaml` into a verified, event-sourced personal site.
4
+
5
+ ```js
6
+ // astro.config.mjs
7
+ import cast from '@cratis/cast';
8
+ import { defineConfig } from 'astro/config';
9
+
10
+ export default defineConfig({ integrations: [cast()] });
11
+ ```
12
+
13
+ The quickest start is `npm create @cratis/cast@latest`. Documentation: [getting started](https://github.com/Cratis/Cast/blob/main/Documentation/getting-started.md), [cast.yaml reference](https://github.com/Cratis/Cast/blob/main/Documentation/cast-yaml.md), [cast.json reference](https://github.com/Cratis/Cast/blob/main/Documentation/cast-json.md), [badges and the registry](https://github.com/Cratis/Cast/blob/main/Documentation/badges-and-registry.md).
14
+
15
+ Besides the integration, the package exports what other Astro projects need to render a Cast profile: `@cratis/cast/profile` (`castView`, the hosts, the browser data), `@cratis/cast/machine` (`castJson`, `profileFromCastJson`, the cast.json schema) and the page components `@cratis/cast/site/pages/ProfilePage.astro` and `CvPage.astro`. The JSON Schemas are `@cratis/cast/cast.schema.json` (cast.yaml) and `@cratis/cast/cast-json.schema.json` (cast.json, version 2).
@@ -0,0 +1,18 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { Tier } from './Tier';
5
+
6
+ /** One reviewed line in Registry/members.yaml. */
7
+ export type RegistryMember = {
8
+ github: string;
9
+ tier: Tier.Core | Tier.Partner;
10
+ since?: string;
11
+ role?: string;
12
+ site?: string;
13
+ /** URL of the member's /cast.json, for sites that publish it elsewhere. Defaults to `<site>/cast.json`. */
14
+ profile?: string;
15
+ company?: string;
16
+ /** 1-based line in members.yaml where this entry starts, for "verified from" links. */
17
+ line?: number;
18
+ };
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { RegistryMember } from './RegistryMember';
5
+ import type { Tier } from './Tier';
6
+ import type { TierSource } from './TierSource';
7
+
8
+ /** The tier a handle holds, as verified at build time. */
9
+ export type ResolvedTier = {
10
+ handle: string;
11
+ tier?: Tier;
12
+ source: TierSource;
13
+ /** The registry entry, when the tier came from the registry. */
14
+ member?: RegistryMember;
15
+ /** Merged public pull requests in the Cratis organization, when known. */
16
+ mergedPullRequests?: number;
17
+ };
package/badges/Tier.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** Cast membership tiers. Core and Partner come only from the reviewed registry; Contributor is earned. */
5
+ export enum Tier {
6
+ /** Founders and maintainers of Cratis. */
7
+ Core = 'core',
8
+
9
+ /** Consultants and companies Cratis vouches for. */
10
+ Partner = 'partner',
11
+
12
+ /** Anyone with at least one merged public pull request in the Cratis organization. */
13
+ Contributor = 'contributor',
14
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** Where a resolved tier came from. */
5
+ export enum TierSource {
6
+ /** A reviewed line in Registry/members.yaml. */
7
+ Registry = 'registry',
8
+
9
+ /** Merged public pull requests in the Cratis organization, counted at build time. */
10
+ Contributions = 'contributions',
11
+
12
+ /** Neither: the person is not (yet) part of the Cast. */
13
+ None = 'none',
14
+ }
@@ -0,0 +1,137 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { castDirectoryUrl } from './constants';
5
+ import { cratisMarkPaths } from './cratisMark';
6
+ import { Tier } from './Tier';
7
+
8
+ /** How a tier looks on a badge. */
9
+ export type TierStyle = {
10
+ label: string;
11
+ description: string;
12
+ /** Gradient stops, left to right. */
13
+ from: string;
14
+ to: string;
15
+ /** Text on the tier segment. */
16
+ ink: string;
17
+ };
18
+
19
+ export const tierStyles: Record<Tier, TierStyle> = {
20
+ [Tier.Core]: { label: 'Core', description: 'Founder or maintainer of Cratis', from: '#a78bfa', to: '#5eead4', ink: '#140b2e' },
21
+ [Tier.Partner]: { label: 'Partner', description: 'Vouched for by Cratis', from: '#2dd4bf', to: '#99f6e4', ink: '#042f2b' },
22
+ [Tier.Contributor]: { label: 'Contributor', description: 'Merged work in the Cratis organization', from: '#f2994a', to: '#fcd34d', ink: '#2d1503' },
23
+ };
24
+
25
+ const unverified: TierStyle = { label: 'not verified', description: 'Not in the Cast registry and no merged Cratis pull requests yet', from: '#3a3a4a', to: '#4a4a5c', ink: '#e8e6f0' };
26
+
27
+ const fontStack = "Inter, 'Inter Variable', -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif";
28
+ const serifStack = "Fraunces, 'Fraunces Variable', Georgia, 'Times New Roman', serif";
29
+ const monoStack = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
30
+
31
+ const narrow = new Set('fijlrtI.,:;\'!|() '.split(''));
32
+ const wide = new Set('mwMWO@%'.split(''));
33
+
34
+ /** Approximate rendered width of a string at a font size. `textLength` makes the final rendering exact. */
35
+ export function measureText(text: string, fontSize: number): number {
36
+ let units = 0;
37
+ for (const character of text) {
38
+ if (narrow.has(character)) units += 0.32;
39
+ else if (wide.has(character)) units += 0.86;
40
+ else if (character >= 'A' && character <= 'Z') units += 0.66;
41
+ else units += 0.56;
42
+ }
43
+ return Math.ceil(units * fontSize);
44
+ }
45
+
46
+ const escapeXml = (value: string): string =>
47
+ value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
48
+
49
+ const mark = (x: number, y: number, size: number, fill: string): string =>
50
+ `<g transform="translate(${x} ${y}) scale(${(size / 328).toFixed(5)})"><g transform="translate(0 328) scale(.1 -.1)" fill="${fill}">${cratisMarkPaths.map(path => `<path d="${path}"/>`).join('')}</g></g>`;
51
+
52
+ /**
53
+ * The inline badge: 20px high, shields-like, with the Cratis mark. Works on light and dark backgrounds
54
+ * because it carries its own ground.
55
+ */
56
+ export function badgeSvg(tier: Tier | undefined): string {
57
+ const style = tier ? tierStyles[tier] : unverified;
58
+ const height = 20;
59
+ const leftText = 'Cast';
60
+ const leftTextWidth = measureText(leftText, 11);
61
+ const leftWidth = 6 + 12 + 5 + leftTextWidth + 7;
62
+ const rightTextWidth = measureText(style.label, 11);
63
+ const rightWidth = rightTextWidth + 16;
64
+ const width = leftWidth + rightWidth;
65
+ const title = tier ? `Cratis Cast: ${style.label}` : 'Cratis Cast: not verified';
66
+ const id = `cast-${tier ?? 'none'}`;
67
+
68
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(title)}">
69
+ <title>${escapeXml(title)}</title>
70
+ <defs>
71
+ <linearGradient id="${id}-tier" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="${style.from}"/><stop offset="1" stop-color="${style.to}"/></linearGradient>
72
+ <linearGradient id="${id}-gloss" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".18"/><stop offset=".5" stop-color="#fff" stop-opacity="0"/></linearGradient>
73
+ <clipPath id="${id}-round"><rect width="${width}" height="${height}" rx="5"/></clipPath>
74
+ </defs>
75
+ <g clip-path="url(#${id}-round)">
76
+ <rect width="${leftWidth}" height="${height}" fill="#0c0c15"/>
77
+ <rect x="${leftWidth}" width="${rightWidth}" height="${height}" fill="url(#${id}-tier)"/>
78
+ <rect width="${width}" height="${height}" fill="url(#${id}-gloss)"/>
79
+ <rect x=".5" y=".5" width="${width - 1}" height="${height - 1}" rx="4.5" fill="none" stroke="#fff" stroke-opacity=".12"/>
80
+ </g>
81
+ ${mark(4, 4, 12, '#f5f3f0')}
82
+ <g font-family="${fontStack}" font-size="11" font-weight="600" text-rendering="geometricPrecision">
83
+ <text x="${6 + 12 + 5}" y="14" fill="#f5f3f0" textLength="${leftTextWidth}" lengthAdjust="spacingAndGlyphs">${leftText}</text>
84
+ <text x="${leftWidth + 8}" y="14" fill="${style.ink}" textLength="${rightTextWidth}" lengthAdjust="spacingAndGlyphs">${escapeXml(style.label)}</text>
85
+ </g>
86
+ </svg>
87
+ `;
88
+ }
89
+
90
+ /** Options for the large badge card. */
91
+ export type BadgeCardOptions = {
92
+ handle: string;
93
+ name?: string;
94
+ since?: string;
95
+ };
96
+
97
+ /**
98
+ * The large badge card (480 × 168): a dark card with a tier-colored edge, the Cratis mark, the tier in
99
+ * the italic serif and the verification address. Meant for READMEs, slides and profile pages.
100
+ */
101
+ export function badgeCardSvg(tier: Tier | undefined, options: BadgeCardOptions): string {
102
+ const style = tier ? tierStyles[tier] : unverified;
103
+ const width = 480;
104
+ const height = 168;
105
+ const id = `cast-card-${tier ?? 'none'}`;
106
+ const handle = options.handle.toLowerCase();
107
+ const address = `${castDirectoryUrl.replace(/^https:\/\//, '')}/${handle}`;
108
+ const who = options.name ? `${options.name} · @${options.handle}` : `@${options.handle}`;
109
+ const sinceText = options.since ? ` · since ${options.since}` : '';
110
+ const title = tier ? `${who}: Cratis Cast ${style.label}` : `${who}: not verified in the Cratis Cast`;
111
+
112
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeXml(title)}">
113
+ <title>${escapeXml(title)}</title>
114
+ <defs>
115
+ <linearGradient id="${id}-edge" x1="0" x2="1" y1="0" y2="1"><stop offset="0" stop-color="${style.from}"/><stop offset="1" stop-color="${style.to}"/></linearGradient>
116
+ <radialGradient id="${id}-glow" cx="1" cy="0" r="1"><stop offset="0" stop-color="${style.from}" stop-opacity=".28"/><stop offset=".6" stop-color="${style.from}" stop-opacity="0"/></radialGradient>
117
+ <linearGradient id="${id}-ground" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#12121d"/><stop offset="1" stop-color="#08080e"/></linearGradient>
118
+ <pattern id="${id}-grid" width="16" height="16" patternUnits="userSpaceOnUse"><path d="M16 0H0V16" fill="none" stroke="#fff" stroke-opacity=".035"/></pattern>
119
+ </defs>
120
+ <rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="17" fill="url(#${id}-ground)"/>
121
+ <rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="17" fill="url(#${id}-grid)"/>
122
+ <rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="17" fill="url(#${id}-glow)"/>
123
+ <rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="17" fill="none" stroke="url(#${id}-edge)" stroke-width="2"/>
124
+ <rect x="24" y="24" width="56" height="56" rx="14" fill="url(#${id}-edge)"/>
125
+ ${mark(30, 30, 44, style.ink)}
126
+ <g font-family="${monoStack}" font-size="11" letter-spacing="1.4" fill="#9b9aa8">
127
+ <text x="98" y="38">CRATIS CAST${tier ? ' · VERIFIED' : ''}</text>
128
+ </g>
129
+ <text x="96" y="78" font-family="${serifStack}" font-style="italic" font-weight="600" font-size="38" fill="${style.from}">${escapeXml(style.label)}</text>
130
+ <g font-family="${fontStack}" fill="#f5f3f0">
131
+ <text x="24" y="118" font-size="16" font-weight="600">${escapeXml(who)}</text>
132
+ <text x="24" y="140" font-size="12.5" fill="#9b9aa8">${escapeXml(style.description + sinceText)}</text>
133
+ </g>
134
+ <text x="${width - 24}" y="140" text-anchor="end" font-family="${monoStack}" font-size="11" fill="#6c6b7a">${escapeXml(address)}</text>
135
+ </svg>
136
+ `;
137
+ }
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** The Cast directory. Every verification page lives at `${castDirectoryUrl}/<handle>`. */
5
+ export const castDirectoryUrl = 'https://cast.cratis.io';
6
+
7
+ /** The organization whose merged public pull requests make someone a Contributor. */
8
+ export const cratisOrganization = 'Cratis';
9
+
10
+ /** The reviewed registry, read at build time. A copy ships in the package as a fallback. */
11
+ export const registryUrl = 'https://raw.githubusercontent.com/Cratis/Cast/main/Registry/members.yaml';
12
+
13
+ /** Human-readable location of the registry, for "verified from" links. */
14
+ export const registryBlobUrl = 'https://github.com/Cratis/Cast/blob/main/Registry/members.yaml';
15
+
16
+ /** The verification page for a handle. */
17
+ export const verificationUrl = (handle: string): string => `${castDirectoryUrl}/${handle.toLowerCase()}`;
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /**
5
+ * The Cratis mark (bracket and hexagon), in a 328 × 328 box drawn with `translate(0 328) scale(.1 -.1)`.
6
+ * Taken from the cratis.no favicon.
7
+ */
8
+ export const cratisMarkPaths: readonly string[] = [
9
+ 'M1444 2779c-319-182-592-343-607-358-16-14-34-35-40-46-8-14-10-229-9-735l3-715 23-35c18-26 74-64 222-151 110-64 374-219 589-344 214-126 400-231 413-233 13-3 31 3 42 13 19 17 20 33 20 307 0 233-3 294-15 317-10 21-93 74-327 211-172 101-324 194-338 207l-25 23v802l27 23c14 12 161 100 327 194 177 101 310 184 325 202l26 31v293c0 318-1 325-55 325-12 0-283-149-601-331z',
10
+ 'M1965 2009c-178-98-219-126-237-157-15-26-18-57-18-215 0-175 1-186 23-218 30-45 326-219 372-219 34 0 319 157 361 199 24 23 24 26 24 230v207l-27 31c-39 44-319 203-358 203-19 0-73-24-140-61z',
11
+ ];
12
+
13
+ /** The mark as a standalone SVG element, colored with `currentColor`. */
14
+ export const cratisMarkSvg = (className = ''): string =>
15
+ `<svg viewBox="0 0 328 328" class="${className}" aria-hidden="true" focusable="false"><g transform="translate(0 328) scale(.1 -.1)" fill="currentColor">${cratisMarkPaths.map(path => `<path d="${path}"/>`).join('')}</g></svg>`;
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ export * from './badgeSvg';
5
+ export * from './constants';
6
+ export * from './cratisMark';
7
+ export * from './registry';
8
+ export * from './RegistryMember';
9
+ export * from './ResolvedTier';
10
+ export * from './resolveTier';
11
+ export * from './Tier';
12
+ export * from './TierSource';
@@ -0,0 +1,68 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { parseDocument, isSeq, isMap } from 'yaml';
5
+ import { z } from 'zod';
6
+ import type { RegistryMember } from './RegistryMember';
7
+ import { Tier } from './Tier';
8
+
9
+ export const registryMemberSchema = z.object({
10
+ github: z.string().regex(/^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/, 'Must be a GitHub login'),
11
+ tier: z.enum([Tier.Core, Tier.Partner]),
12
+ since: z.string().regex(/^\d{4}-\d{2}$/, 'Use YYYY-MM').optional(),
13
+ role: z.string().min(1).optional(),
14
+ site: z.url({ protocol: /^https$/ }).optional(),
15
+ profile: z.url({ protocol: /^https$/ }).optional(),
16
+ company: z.string().min(1).optional(),
17
+ }).strict();
18
+
19
+ export const registrySchema = z.array(registryMemberSchema);
20
+
21
+ /** Thrown when members.yaml does not validate. */
22
+ export class RegistryError extends Error {
23
+ constructor(readonly problems: string[]) {
24
+ super(`Registry/members.yaml is not valid:\n${problems.map(problem => ` - ${problem}`).join('\n')}`);
25
+ this.name = 'RegistryError';
26
+ }
27
+ }
28
+
29
+ /** Parses and validates the registry. Handles are unique, compared case-insensitively. */
30
+ export function parseRegistry(yamlText: string): RegistryMember[] {
31
+ const document = parseDocument(yamlText);
32
+ if (document.errors.length > 0) {
33
+ throw new RegistryError(document.errors.map(error => error.message));
34
+ }
35
+
36
+ const result = registrySchema.safeParse(document.toJS() ?? []);
37
+ if (!result.success) {
38
+ throw new RegistryError(result.error.issues.map(issue => `${issue.path.join('.') || '(root)'}: ${issue.message}`));
39
+ }
40
+
41
+ const lines = entryLines(document, yamlText);
42
+ const seen = new Set<string>();
43
+ const problems: string[] = [];
44
+ const members = result.data.map((member, index) => {
45
+ const key = member.github.toLowerCase();
46
+ if (seen.has(key)) problems.push(`${index}.github: '${member.github}' is listed more than once`);
47
+ seen.add(key);
48
+ return { ...member, line: lines[index] } as RegistryMember;
49
+ });
50
+ if (problems.length > 0) throw new RegistryError(problems);
51
+ return members;
52
+ }
53
+
54
+ function entryLines(document: ReturnType<typeof parseDocument>, yamlText: string): (number | undefined)[] {
55
+ if (!isSeq(document.contents)) return [];
56
+ return document.contents.items.map(item => {
57
+ const offset = isMap(item) ? item.range?.[0] : undefined;
58
+ return offset === undefined ? undefined : yamlText.slice(0, offset).split('\n').length;
59
+ });
60
+ }
61
+
62
+ /** Where the directory reads a member's profile: `profile`, else `<site>/cast.json`, else nowhere. */
63
+ export const profileUrlOf = (member: Pick<RegistryMember, 'site' | 'profile'>): string | undefined =>
64
+ member.profile ?? (member.site ? new URL('/cast.json', member.site).toString() : undefined);
65
+
66
+ /** Finds a member by GitHub login, case-insensitively. */
67
+ export const findMember = (members: readonly RegistryMember[], handle: string): RegistryMember | undefined =>
68
+ members.find(member => member.github.toLowerCase() === handle.toLowerCase());
@@ -0,0 +1,24 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { findMember } from './registry';
5
+ import type { RegistryMember } from './RegistryMember';
6
+ import type { ResolvedTier } from './ResolvedTier';
7
+ import { Tier } from './Tier';
8
+ import { TierSource } from './TierSource';
9
+
10
+ /**
11
+ * Resolves a handle's tier. The registry decides Core and Partner. Without a registry entry, one or more
12
+ * merged public pull requests in the Cratis organization make a Contributor. There is no other input:
13
+ * nothing a site writes about itself can change the result.
14
+ */
15
+ export function resolveTier(handle: string, registry: readonly RegistryMember[], mergedPullRequests?: number): ResolvedTier {
16
+ const member = findMember(registry, handle);
17
+ if (member) {
18
+ return { handle: member.github, tier: member.tier, source: TierSource.Registry, member, mergedPullRequests };
19
+ }
20
+ if (mergedPullRequests !== undefined && mergedPullRequests >= 1) {
21
+ return { handle, tier: Tier.Contributor, source: TierSource.Contributions, mergedPullRequests };
22
+ }
23
+ return { handle, source: TierSource.None, mergedPullRequests };
24
+ }