@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.
- package/README.md +13 -1
- package/badges/RegistryMember.ts +18 -0
- package/badges/ResolvedTier.ts +17 -0
- package/badges/Tier.ts +14 -0
- package/badges/TierSource.ts +14 -0
- package/badges/badgeSvg.ts +137 -0
- package/badges/constants.ts +17 -0
- package/badges/cratisMark.ts +15 -0
- package/badges/index.ts +12 -0
- package/badges/registry.ts +68 -0
- package/badges/resolveTier.ts +24 -0
- package/cast-json.schema.json +1496 -0
- package/cast.schema.json +1113 -0
- package/config/CastConfig.ts +17 -0
- package/config/CastConfigError.ts +10 -0
- package/config/LiveStat.ts +17 -0
- package/config/ResolvedSection.ts +13 -0
- package/config/SectionId.ts +21 -0
- package/config/ThemeMode.ts +9 -0
- package/config/calendar.ts +34 -0
- package/config/castSchema.ts +263 -0
- package/config/index.ts +12 -0
- package/config/parseCast.ts +58 -0
- package/config/resolveSections.ts +48 -0
- package/github/GitHubSnapshot.ts +66 -0
- package/github/fetchGitHub.ts +235 -0
- package/github/gitHubSnapshotSchema.ts +58 -0
- package/github/parse.ts +63 -0
- package/index.ts +88 -0
- package/integration/CastData.ts +20 -0
- package/integration/CastOptions.ts +17 -0
- package/integration/Snapshot.ts +12 -0
- package/integration/loadRegistry.ts +37 -0
- package/integration/resolveData.ts +138 -0
- package/integration/snapshotFile.ts +23 -0
- package/integration/virtualModule.ts +43 -0
- package/machine/castJson.ts +77 -0
- package/machine/castJsonSchema.ts +35 -0
- package/machine/index.ts +6 -0
- package/machine/llmsText.ts +69 -0
- package/machine/ogImageSvg.ts +79 -0
- package/machine/profileFromCastJson.ts +214 -0
- package/machine/resumeJson.ts +87 -0
- package/package.json +80 -8
- package/registry/members.yaml +16 -0
- package/routes/404.astro +42 -0
- package/routes/apple-touch-icon.png.ts +17 -0
- package/routes/badge-card.svg.ts +13 -0
- package/routes/badge.svg.ts +11 -0
- package/routes/cast.json.ts +17 -0
- package/routes/cv.astro +9 -0
- package/routes/favicon.svg.ts +16 -0
- package/routes/index.astro +9 -0
- package/routes/llms.txt.ts +10 -0
- package/routes/og.png.ts +23 -0
- package/routes/resume.json.ts +13 -0
- package/routes/robots.txt.ts +10 -0
- package/site/components/Activity.astro +196 -0
- package/site/components/Agentic.astro +144 -0
- package/site/components/CareerReplay.astro +415 -0
- package/site/components/CastBadge.astro +132 -0
- package/site/components/CommandPalette.astro +122 -0
- package/site/components/Contact.astro +166 -0
- package/site/components/Cratis.astro +235 -0
- package/site/components/CratisMark.astro +18 -0
- package/site/components/EventModel.astro +265 -0
- package/site/components/Footer.astro +33 -0
- package/site/components/Hero.astro +211 -0
- package/site/components/Inline.astro +45 -0
- package/site/components/Monogram.astro +28 -0
- package/site/components/Nav.astro +127 -0
- package/site/components/Now.astro +116 -0
- package/site/components/Projects.astro +185 -0
- package/site/components/PullRequestFeed.astro +88 -0
- package/site/components/SectionHeader.astro +30 -0
- package/site/components/Skills.astro +44 -0
- package/site/components/Stats.astro +106 -0
- package/site/components/Talks.astro +76 -0
- package/site/components/Terminal.astro +164 -0
- package/site/components/Writing.astro +128 -0
- package/site/data.ts +11 -0
- package/site/layouts/Base.astro +156 -0
- package/site/lib/accent.ts +52 -0
- package/site/lib/blog.ts +58 -0
- package/site/lib/career.ts +148 -0
- package/site/lib/clipboard.ts +12 -0
- package/site/lib/contributions.ts +63 -0
- package/site/lib/github.ts +72 -0
- package/site/lib/inline.ts +79 -0
- package/site/lib/lifecycle.ts +44 -0
- package/site/lib/motion.ts +43 -0
- package/site/lib/nav.ts +49 -0
- package/site/lib/reltime.ts +33 -0
- package/site/lib/safeJson.ts +15 -0
- package/site/lib/sections.ts +41 -0
- package/site/lib/theme.ts +28 -0
- package/site/pages/CvPage.astro +379 -0
- package/site/pages/ProfilePage.astro +78 -0
- package/site/profile/CastHost.ts +32 -0
- package/site/profile/CastView.ts +96 -0
- package/site/profile/assertRenderable.ts +151 -0
- package/site/profile/clientCast.ts +117 -0
- package/site/profile/index.ts +8 -0
- package/site/scripts/ambient.ts +93 -0
- package/site/scripts/event-board.ts +403 -0
- package/site/scripts/event-model.ts +155 -0
- package/site/scripts/heatmap.ts +94 -0
- package/site/scripts/live.ts +155 -0
- package/site/scripts/palette.ts +313 -0
- package/site/scripts/replay.ts +252 -0
- package/site/scripts/terminal.ts +559 -0
- package/site/styles/global.css +508 -0
- package/virtual.d.ts +15 -0
- package/writing/WritingSnapshot.ts +20 -0
- package/writing/fetchWriting.ts +69 -0
- package/writing/parse.ts +72 -0
- package/writing/writingSnapshotSchema.ts +27 -0
|
@@ -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 { z } from 'zod';
|
|
5
|
+
import type { castSchema } from './castSchema';
|
|
6
|
+
|
|
7
|
+
/** cast.yaml as written, before defaults are applied. */
|
|
8
|
+
export type CastInput = z.input<typeof castSchema>;
|
|
9
|
+
|
|
10
|
+
/** cast.yaml after validation and defaults. Career events always carry a `seq`; `github.login` always equals `person.handle`. */
|
|
11
|
+
export type CastConfig = Omit<z.output<typeof castSchema>, 'career'> & {
|
|
12
|
+
career: { events: CareerEvent[] };
|
|
13
|
+
github: z.output<typeof castSchema>['github'] & { login: string };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** One fact in the career stream. */
|
|
17
|
+
export type CareerEvent = Omit<z.output<typeof castSchema>['career']['events'][number], 'seq'> & { seq: number };
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
/** Thrown when cast.yaml does not validate. The message lists every problem with its path. */
|
|
5
|
+
export class CastConfigError extends Error {
|
|
6
|
+
constructor(readonly problems: string[], source = 'cast.yaml') {
|
|
7
|
+
super(`${source} is not valid:\n${problems.map(problem => ` - ${problem}`).join('\n')}`);
|
|
8
|
+
this.name = 'CastConfigError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -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
|
+
/** Build-time sources a stat can take its value from instead of the value written in cast.yaml. */
|
|
5
|
+
export enum LiveStat {
|
|
6
|
+
/** GitHub contributions in the last year, floored to hundreds. */
|
|
7
|
+
ContributionsLastYear = 'contributionsLastYear',
|
|
8
|
+
|
|
9
|
+
/** Merged public pull requests in the configured organizations, floored to hundreds. */
|
|
10
|
+
MergedPullRequests = 'mergedPullRequests',
|
|
11
|
+
|
|
12
|
+
/** Number of tracked repositories where the person is the top human committer. */
|
|
13
|
+
RankOneRepositories = 'rankOneRepositories',
|
|
14
|
+
|
|
15
|
+
/** Number of entries in `cratis.clientLanguages`. */
|
|
16
|
+
ClientLanguages = 'clientLanguages',
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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 { SectionId } from './SectionId';
|
|
5
|
+
|
|
6
|
+
/** A section that will render, with its two-digit index and any copy overrides. */
|
|
7
|
+
export type ResolvedSection = {
|
|
8
|
+
id: SectionId;
|
|
9
|
+
index: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
intro?: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
/** Every section a Cast home page can render, in their default order. */
|
|
5
|
+
export enum SectionId {
|
|
6
|
+
Hero = 'hero',
|
|
7
|
+
Now = 'now',
|
|
8
|
+
Stats = 'stats',
|
|
9
|
+
Career = 'career',
|
|
10
|
+
Model = 'model',
|
|
11
|
+
Cratis = 'cratis',
|
|
12
|
+
Activity = 'activity',
|
|
13
|
+
Agentic = 'agentic',
|
|
14
|
+
Projects = 'projects',
|
|
15
|
+
Writing = 'writing',
|
|
16
|
+
Talks = 'talks',
|
|
17
|
+
Skills = 'skills',
|
|
18
|
+
Contact = 'contact',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const defaultSectionOrder: SectionId[] = Object.values(SectionId);
|
|
@@ -0,0 +1,9 @@
|
|
|
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 theme a first-time visitor sees. Visitors can always switch, and their choice is remembered. */
|
|
5
|
+
export enum ThemeMode {
|
|
6
|
+
Dark = 'dark',
|
|
7
|
+
Light = 'light',
|
|
8
|
+
System = 'system',
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
const dayPattern = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
5
|
+
|
|
6
|
+
/** True when `year`, `month` and `day` name a real day: '2026-02-30' and '2026-99-99' do not. */
|
|
7
|
+
function isRealDay(year: number, month: number, day: number): boolean {
|
|
8
|
+
if (year < 1000 || month < 1 || month > 12 || day < 1) return false;
|
|
9
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
10
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** True for a real calendar day written as 'YYYY-MM-DD'. */
|
|
14
|
+
export function isCalendarDate(value: string): boolean {
|
|
15
|
+
const match = dayPattern.exec(value);
|
|
16
|
+
return match !== null && match[0].length === value.length && isRealDay(Number(match[1]), Number(match[2]), Number(match[3]));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** True for 'YYYY' or 'YYYY-MM' with a real month. */
|
|
20
|
+
export function isCalendarMonth(value: string): boolean {
|
|
21
|
+
const match = /^(\d{4})(?:-(\d{2}))?$/.exec(value);
|
|
22
|
+
return match !== null && isRealDay(Number(match[1]), Number(match[2] ?? 1), 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** True for 'YYYY-MM-DD', 'YYYY-MM' or 'YYYY', each a real day or month. */
|
|
26
|
+
export function isPartialDate(value: string): boolean {
|
|
27
|
+
return value.length === 10 ? isCalendarDate(value) : isCalendarMonth(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** True for an ISO 8601 date or date and time that starts with a real calendar day and that `Date` can read. */
|
|
31
|
+
export function isInstant(value: string): boolean {
|
|
32
|
+
const match = dayPattern.exec(value);
|
|
33
|
+
return match !== null && isRealDay(Number(match[1]), Number(match[2]), Number(match[3])) && Number.isFinite(Date.parse(value));
|
|
34
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
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 { z } from 'zod';
|
|
5
|
+
import { isCalendarMonth, isPartialDate } from './calendar';
|
|
6
|
+
import { LiveStat } from './LiveStat';
|
|
7
|
+
import { SectionId } from './SectionId';
|
|
8
|
+
import { ThemeMode } from './ThemeMode';
|
|
9
|
+
|
|
10
|
+
/** 'YYYY-MM' or 'YYYY'. */
|
|
11
|
+
const month = z.string().regex(/^\d{4}(-\d{2})?$/, 'Use YYYY-MM or YYYY').refine(isCalendarMonth, 'Use a real month: YYYY-MM or YYYY');
|
|
12
|
+
|
|
13
|
+
/** 'YYYY-MM-DD', 'YYYY-MM' or 'YYYY'. */
|
|
14
|
+
const date = z.string().regex(/^\d{4}(-\d{2}(-\d{2})?)?$/, 'Use YYYY-MM-DD, YYYY-MM or YYYY').refine(isPartialDate, 'Use a real date: YYYY-MM-DD, YYYY-MM or YYYY');
|
|
15
|
+
|
|
16
|
+
const url = z.url({ protocol: /^https?$/ });
|
|
17
|
+
const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Use a six-digit hex color such as #8b5cf6');
|
|
18
|
+
|
|
19
|
+
/** Inline text may use *emphasis*, `code` and [links](https://example.com). */
|
|
20
|
+
const text = z.string().min(1);
|
|
21
|
+
|
|
22
|
+
export const personSchema = z.object({
|
|
23
|
+
name: z.string().min(1).describe('Display name, for example "Ada Lovelace".'),
|
|
24
|
+
fullName: z.string().optional().describe('Full legal name when it differs from the display name.'),
|
|
25
|
+
handle: z.string().regex(/^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/, 'Must be your GitHub login').describe('Your GitHub login: your one Cast identity. Tier, badge, /cast.json and the verification page all use it.'),
|
|
26
|
+
title: z.string().min(1).describe('Role line under your name.'),
|
|
27
|
+
titleShort: z.string().optional().describe('Shorter role line for small screens.'),
|
|
28
|
+
location: z.string().optional(),
|
|
29
|
+
country: z.string().length(2).optional().describe('ISO 3166 two-letter country code, used in structured data.'),
|
|
30
|
+
email: z.email().optional(),
|
|
31
|
+
photo: z.string().optional().describe('Portrait: a path relative to cast.yaml (for example ./avatar.jpeg) or an https URL.'),
|
|
32
|
+
links: z.record(z.string(), url).default({}).describe('Profile links by name: github, linkedin, mastodon, bluesky, website or anything else.'),
|
|
33
|
+
tagline: text.describe('The hero headline. Wrap the words to accent in *asterisks*.'),
|
|
34
|
+
intro: text.describe('One paragraph under the headline.'),
|
|
35
|
+
about: z.array(text).default([]),
|
|
36
|
+
interests: z.array(z.string()).default([]),
|
|
37
|
+
languages: z.array(z.string()).default([]),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const careerEventSchema = z.object({
|
|
41
|
+
seq: z.number().int().positive().optional().describe('Stream position. Assigned in file order when omitted.'),
|
|
42
|
+
date: month,
|
|
43
|
+
endDate: month.optional().describe('End of an engagement that does not set a role.'),
|
|
44
|
+
type: z.string().regex(/^[A-Z][A-Za-z]+$/, 'Event types are past-tense PascalCase, for example EmploymentStarted').describe('Past-tense event name.'),
|
|
45
|
+
title: z.string().min(1),
|
|
46
|
+
org: z.string().optional(),
|
|
47
|
+
orgUrl: url.optional(),
|
|
48
|
+
location: z.string().optional(),
|
|
49
|
+
summary: text,
|
|
50
|
+
skills: z.array(z.string()).optional(),
|
|
51
|
+
role: z.string().optional().describe('Sets the current role in the projection.'),
|
|
52
|
+
endsRole: z.string().optional().describe('Organization whose open role this event ends.'),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const statSchema = z.object({
|
|
56
|
+
value: z.number(),
|
|
57
|
+
prefix: z.string().default(''),
|
|
58
|
+
suffix: z.string().default(''),
|
|
59
|
+
label: z.string().min(1),
|
|
60
|
+
live: z.enum(LiveStat).optional().describe('Replace the value with a build-time number.'),
|
|
61
|
+
plain: z.boolean().default(false).describe('Show the value without counting up or thousands separators.'),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const cratisRepositorySchema = z.object({
|
|
65
|
+
name: z.string().min(1).describe('Repository name in the Cratis organization.'),
|
|
66
|
+
url: url.optional().describe('Omit for repositories that are not public; they are then never fetched.'),
|
|
67
|
+
kind: z.string().min(1),
|
|
68
|
+
group: z.enum(['pipeline', 'tooling', 'client']).default('tooling'),
|
|
69
|
+
step: z.string().optional(),
|
|
70
|
+
note: z.string().optional(),
|
|
71
|
+
role: z.string().optional().describe('Fallback role text; replaced by the live rank when GitHub data is available.'),
|
|
72
|
+
rank: z.number().int().min(0).default(0),
|
|
73
|
+
founded: z.boolean().default(false),
|
|
74
|
+
commits: z.number().int().min(0).default(0),
|
|
75
|
+
prs: z.number().int().min(0).default(0),
|
|
76
|
+
desc: text,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const cratisSchema = z.object({
|
|
80
|
+
title: z.string().default('What I lead at Cratis.'),
|
|
81
|
+
label: z.string().default('Cratis'),
|
|
82
|
+
pitch: text.optional(),
|
|
83
|
+
role: text.describe('What you do at Cratis, in your own words.'),
|
|
84
|
+
links: z.record(z.string(), url).default({ 'cratis.io': 'https://cratis.io', 'github.com/Cratis': 'https://github.com/Cratis' }),
|
|
85
|
+
pipeline: z.array(z.string()).default([]).describe('Repository names shown as the model-to-running-app pipeline, left to right.'),
|
|
86
|
+
clientLanguages: z.array(z.string()).default([]),
|
|
87
|
+
repos: z.array(cratisRepositorySchema).default([]),
|
|
88
|
+
highlights: z.array(z.string()).default([]),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const projectSchema = z.object({
|
|
92
|
+
name: z.string().min(1),
|
|
93
|
+
url: url,
|
|
94
|
+
pitch: text,
|
|
95
|
+
features: z.array(z.string()).default([]),
|
|
96
|
+
stack: z.array(z.string()).default([]),
|
|
97
|
+
install: z.string().optional().describe('Install command with a copy button.'),
|
|
98
|
+
version: z.string().optional().describe('Fallback version; replaced by the latest GitHub release when tracked.'),
|
|
99
|
+
category: z.string().default('project'),
|
|
100
|
+
featured: z.boolean().default(false).describe('Show the card at double width.'),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const projectCategorySchema = z.object({
|
|
104
|
+
id: z.string().min(1),
|
|
105
|
+
label: z.string().min(1),
|
|
106
|
+
filter: z.string().optional().describe('Filter chip label; defaults to label.'),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const talkSchema = z.object({
|
|
110
|
+
title: z.string().min(1),
|
|
111
|
+
lang: z.string().default('en').describe('BCP 47 language of the talk, for example "en" or "no".'),
|
|
112
|
+
host: z.string().min(1),
|
|
113
|
+
with: z.string().optional(),
|
|
114
|
+
date: date,
|
|
115
|
+
url: url.optional(),
|
|
116
|
+
summary: text,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const communitySchema = z.object({
|
|
120
|
+
title: z.string().min(1),
|
|
121
|
+
detail: z.string().default(''),
|
|
122
|
+
date: date,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const writingSourceSchema = z.union([
|
|
126
|
+
z.object({
|
|
127
|
+
rss: url.describe('RSS or Atom feed URL.'),
|
|
128
|
+
author: z.string().optional().describe('Only include entries by this author name.'),
|
|
129
|
+
}),
|
|
130
|
+
z.object({
|
|
131
|
+
starlightBlogAuthor: url.describe('Author page of a Starlight Blog site, for example https://blog.cratis.io/authors/your-name/.'),
|
|
132
|
+
}),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
export const writingThemeSchema = z.object({
|
|
136
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
137
|
+
label: z.string().min(1),
|
|
138
|
+
tags: z.array(z.string()).min(1),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export const writingSchema = z.object({
|
|
142
|
+
sources: z.array(writingSourceSchema).default([]),
|
|
143
|
+
themes: z.array(writingThemeSchema).default([]).describe('Filter chips, each a set of post tags.'),
|
|
144
|
+
agenticTheme: z.string().optional().describe('Theme id whose posts the agentic section lists.'),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export const agenticStepSchema = z.object({
|
|
148
|
+
step: z.string().min(1),
|
|
149
|
+
event: z.string().regex(/^[A-Z][A-Za-z]+$/),
|
|
150
|
+
who: z.string().min(1),
|
|
151
|
+
text: text,
|
|
152
|
+
loop: z.string().optional().describe('A loop-back note shown on the step, for example "blocker → Implement".'),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export const agenticItemSchema = z.object({
|
|
156
|
+
name: z.string().min(1),
|
|
157
|
+
url: url.optional(),
|
|
158
|
+
kind: z.string().min(1),
|
|
159
|
+
text: text,
|
|
160
|
+
list: z.array(z.string()).optional(),
|
|
161
|
+
listLabel: z.string().optional(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export const agenticSchema = z.object({
|
|
165
|
+
title: z.string().default('I build the harness.'),
|
|
166
|
+
pitch: text,
|
|
167
|
+
label: z.string().default('Agentic engineering'),
|
|
168
|
+
workflowLabel: z.string().default('every change in my setup'),
|
|
169
|
+
workflow: z.array(agenticStepSchema).default([]),
|
|
170
|
+
items: z.array(agenticItemSchema).default([]),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export const modelCardSchema = z.object({
|
|
174
|
+
id: z.string().min(1),
|
|
175
|
+
kind: z.enum(['ui', 'command', 'event', 'view']),
|
|
176
|
+
name: z.string().min(1),
|
|
177
|
+
lane: z.string().min(1),
|
|
178
|
+
col: z.number().int().min(0),
|
|
179
|
+
source: z.string().min(1),
|
|
180
|
+
note: text,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export const eventModelSchema = z.object({
|
|
184
|
+
title: z.string().default('Start with the *event model.*'),
|
|
185
|
+
label: z.string().default('How I think'),
|
|
186
|
+
intro: text.optional().describe('Paragraph under the title. Defaults to a short explanation of the board.'),
|
|
187
|
+
summary: z.string().optional().describe('Line under the board before a card is selected.'),
|
|
188
|
+
name: z.string().min(1),
|
|
189
|
+
repo: url,
|
|
190
|
+
sourceRoot: z.string().default(''),
|
|
191
|
+
branch: z.string().default('main'),
|
|
192
|
+
columns: z.number().int().positive(),
|
|
193
|
+
lanes: z.array(z.object({ id: z.string().min(1), label: z.string().min(1) })).min(1),
|
|
194
|
+
cards: z.array(modelCardSchema).min(1),
|
|
195
|
+
edges: z.array(z.tuple([z.string(), z.string()])).default([]),
|
|
196
|
+
steps: z.array(z.object({ title: z.string().min(1), text: text, cards: z.array(z.string()) })).default([]),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export const educationSchema = z.object({
|
|
200
|
+
degree: z.string().min(1),
|
|
201
|
+
school: z.string().min(1),
|
|
202
|
+
from: z.string().min(1),
|
|
203
|
+
to: z.string().optional(),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export const certificationSchema = z.object({
|
|
207
|
+
title: z.string().min(1),
|
|
208
|
+
issuer: z.string().min(1),
|
|
209
|
+
year: z.string().min(1),
|
|
210
|
+
url: url.optional(),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
export const githubSchema = z.object({
|
|
214
|
+
login: z.string().optional().describe('Must equal person.handle (case-insensitive) when given; your GitHub login is your one identity.'),
|
|
215
|
+
orgs: z.array(z.string()).default(['Cratis']).describe('Organizations whose merged public pull requests are counted. The first one feeds the live pull request feed.'),
|
|
216
|
+
releases: z.array(z.string().regex(/^[\w.-]+\/[\w.-]+$/)).default([]).describe('owner/name repositories whose latest release versions your projects.'),
|
|
217
|
+
stars: z.array(z.string().regex(/^[\w.-]+\/[\w.-]+$/)).default([]).describe('owner/name repositories whose star count is shown.'),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
export const themeSchema = z.object({
|
|
221
|
+
accent: hex.default('#8b5cf6').describe('Your accent color. Defaults to Cratis violet.'),
|
|
222
|
+
mode: z.enum(ThemeMode).default(ThemeMode.Dark),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
export const sectionEntrySchema = z.union([
|
|
226
|
+
z.enum(SectionId),
|
|
227
|
+
z.object({
|
|
228
|
+
id: z.enum(SectionId),
|
|
229
|
+
title: z.string().optional(),
|
|
230
|
+
label: z.string().optional(),
|
|
231
|
+
intro: text.optional(),
|
|
232
|
+
}),
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
export const siteSchema = z.object({
|
|
236
|
+
url: url.describe('Public URL of the site, for example https://ada.dev.'),
|
|
237
|
+
description: z.string().optional().describe('Meta description; defaults to the tagline and intro.'),
|
|
238
|
+
source: url.optional().describe('Source repository, linked in the footer.'),
|
|
239
|
+
language: z.string().default('en'),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
export const castSchema = z.object({
|
|
243
|
+
$schema: z.string().optional(),
|
|
244
|
+
site: siteSchema,
|
|
245
|
+
person: personSchema,
|
|
246
|
+
now: z.record(z.string(), z.union([z.string(), z.array(z.string())])).default({}).describe('The "now" read model: ordered key/value rows. YYYY-MM values are shown as months.'),
|
|
247
|
+
stats: z.array(statSchema).default([]),
|
|
248
|
+
career: z.object({ events: z.array(careerEventSchema).default([]) }).default({ events: [] }),
|
|
249
|
+
cratis: cratisSchema.optional(),
|
|
250
|
+
projects: z.array(projectSchema).default([]),
|
|
251
|
+
projectCategories: z.array(projectCategorySchema).default([]),
|
|
252
|
+
talks: z.array(talkSchema).default([]),
|
|
253
|
+
community: z.array(communitySchema).default([]),
|
|
254
|
+
writing: writingSchema.default({ sources: [], themes: [] }),
|
|
255
|
+
agentic: agenticSchema.optional(),
|
|
256
|
+
eventModel: eventModelSchema.optional(),
|
|
257
|
+
skills: z.record(z.string(), z.array(z.string())).default({}),
|
|
258
|
+
education: z.array(educationSchema).default([]),
|
|
259
|
+
certifications: z.array(certificationSchema).default([]),
|
|
260
|
+
github: githubSchema.default({ orgs: ['Cratis'], releases: [], stars: [] }),
|
|
261
|
+
theme: themeSchema.default({ accent: '#8b5cf6', mode: ThemeMode.Dark }),
|
|
262
|
+
sections: z.array(sectionEntrySchema).optional().describe('Section order. Listed sections render in this order; omitted ones are hidden. Omit the key to show every section that has data.'),
|
|
263
|
+
}).strict();
|
package/config/index.ts
ADDED
|
@@ -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 './castSchema';
|
|
5
|
+
export * from './CastConfig';
|
|
6
|
+
export * from './CastConfigError';
|
|
7
|
+
export * from './LiveStat';
|
|
8
|
+
export * from './parseCast';
|
|
9
|
+
export * from './ResolvedSection';
|
|
10
|
+
export * from './resolveSections';
|
|
11
|
+
export * from './SectionId';
|
|
12
|
+
export * from './ThemeMode';
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { castSchema } from './castSchema';
|
|
6
|
+
import { CastConfigError } from './CastConfigError';
|
|
7
|
+
import type { CastConfig } from './CastConfig';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates an already-parsed cast.yaml document and applies defaults.
|
|
11
|
+
* Career events without `seq` get their position in the file; the stream is then ordered by `seq`.
|
|
12
|
+
*/
|
|
13
|
+
export function parseCastObject(document: unknown, source = 'cast.yaml'): CastConfig {
|
|
14
|
+
if (document && typeof document === 'object' && 'tier' in document) {
|
|
15
|
+
throw new CastConfigError(['tier: a site cannot declare its own tier. Core and Partner come from the reviewed Cast registry; Contributor is computed from merged pull requests.'], source);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = castSchema.safeParse(document ?? {});
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
const problems = result.error.issues.map(issue => `${issue.path.join('.') || '(root)'}: ${issue.message}`);
|
|
21
|
+
throw new CastConfigError(problems, source);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const config = result.data;
|
|
25
|
+
|
|
26
|
+
// One identity: the GitHub login. person.handle is that login; github.login may repeat it, never differ.
|
|
27
|
+
if (config.github.login !== undefined && config.github.login.toLowerCase() !== config.person.handle.toLowerCase()) {
|
|
28
|
+
throw new CastConfigError([`github.login: '${config.github.login}' differs from person.handle '${config.person.handle}'. Both are your GitHub login, which is your Cast identity; use the same value or leave github.login out.`], source);
|
|
29
|
+
}
|
|
30
|
+
const events = config.career.events
|
|
31
|
+
.map((event, index) => ({ ...event, seq: event.seq ?? index + 1 }))
|
|
32
|
+
.sort((left, right) => left.seq - right.seq);
|
|
33
|
+
|
|
34
|
+
const sequences = new Set<number>();
|
|
35
|
+
for (const event of events) {
|
|
36
|
+
if (sequences.has(event.seq)) {
|
|
37
|
+
throw new CastConfigError([`career.events: seq ${event.seq} is used more than once`], source);
|
|
38
|
+
}
|
|
39
|
+
sequences.add(event.seq);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...config,
|
|
44
|
+
career: { events },
|
|
45
|
+
github: { ...config.github, login: config.github.login ?? config.person.handle },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Parses cast.yaml text. */
|
|
50
|
+
export function parseCast(yamlText: string, source = 'cast.yaml'): CastConfig {
|
|
51
|
+
let document: unknown;
|
|
52
|
+
try {
|
|
53
|
+
document = parseYaml(yamlText);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new CastConfigError([`YAML: ${error instanceof Error ? error.message : String(error)}`], source);
|
|
56
|
+
}
|
|
57
|
+
return parseCastObject(document, source);
|
|
58
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { CastConfig } from './CastConfig';
|
|
5
|
+
import type { ResolvedSection } from './ResolvedSection';
|
|
6
|
+
import { defaultSectionOrder, SectionId } from './SectionId';
|
|
7
|
+
|
|
8
|
+
const hasData: Record<SectionId, (config: CastConfig) => boolean> = {
|
|
9
|
+
[SectionId.Hero]: () => true,
|
|
10
|
+
[SectionId.Now]: config => Object.keys(config.now).length > 0 || config.person.about.length > 0,
|
|
11
|
+
[SectionId.Stats]: config => config.stats.length > 0,
|
|
12
|
+
[SectionId.Career]: config => config.career.events.length > 0,
|
|
13
|
+
[SectionId.Model]: config => config.eventModel !== undefined,
|
|
14
|
+
[SectionId.Cratis]: config => config.cratis !== undefined,
|
|
15
|
+
[SectionId.Activity]: () => true,
|
|
16
|
+
[SectionId.Agentic]: config => config.agentic !== undefined,
|
|
17
|
+
[SectionId.Projects]: config => config.projects.length > 0,
|
|
18
|
+
[SectionId.Writing]: config => config.writing.sources.length > 0,
|
|
19
|
+
[SectionId.Talks]: config => config.talks.length > 0 || config.community.length > 0,
|
|
20
|
+
[SectionId.Skills]: config => Object.keys(config.skills).length > 0,
|
|
21
|
+
[SectionId.Contact]: config => config.person.email !== undefined || Object.keys(config.person.links).length > 0,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The sections the home page renders, in order, numbered from 01. The hero is never numbered.
|
|
26
|
+
* Sections listed in cast.yaml render even when they have no data yet (they show their empty state),
|
|
27
|
+
* except the ones that cannot render without data.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveSections(config: CastConfig): ResolvedSection[] {
|
|
30
|
+
const requiresData = new Set([SectionId.Model, SectionId.Cratis, SectionId.Agentic]);
|
|
31
|
+
const entries = config.sections === undefined
|
|
32
|
+
? defaultSectionOrder.filter(id => hasData[id](config)).map(id => ({ id }))
|
|
33
|
+
: config.sections
|
|
34
|
+
.map(entry => (typeof entry === 'string' ? { id: entry } : entry))
|
|
35
|
+
.filter(entry => !requiresData.has(entry.id) || hasData[entry.id](config));
|
|
36
|
+
|
|
37
|
+
const seen = new Set<SectionId>();
|
|
38
|
+
let counter = 0;
|
|
39
|
+
const sections: ResolvedSection[] = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (seen.has(entry.id)) continue;
|
|
42
|
+
seen.add(entry.id);
|
|
43
|
+
const numbered = entry.id !== SectionId.Hero && entry.id !== SectionId.Stats;
|
|
44
|
+
if (numbered) counter++;
|
|
45
|
+
sections.push({ ...entry, index: numbered ? String(counter).padStart(2, '0') : '' });
|
|
46
|
+
}
|
|
47
|
+
return sections;
|
|
48
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
/** A merged pull request in the live feed. */
|
|
5
|
+
export type PullRequest = {
|
|
6
|
+
title: string;
|
|
7
|
+
/** owner/name */
|
|
8
|
+
repository: string;
|
|
9
|
+
number: number;
|
|
10
|
+
url: string;
|
|
11
|
+
mergedAt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** One week of the contribution calendar, starting on `start` (a Sunday). */
|
|
15
|
+
export type ContributionWeek = { start: string; counts: number[] };
|
|
16
|
+
|
|
17
|
+
/** A person's standing in one repository, among human committers only. */
|
|
18
|
+
export type RepositoryStanding = { commits: number; rank: number; prs: number };
|
|
19
|
+
|
|
20
|
+
/** A release of a tracked repository. */
|
|
21
|
+
export type Release = { tag: string; publishedAt: string; url: string };
|
|
22
|
+
|
|
23
|
+
/** Public GitHub profile fields. */
|
|
24
|
+
export type GitHubProfile = {
|
|
25
|
+
login: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
bio?: string;
|
|
28
|
+
location?: string;
|
|
29
|
+
blog?: string;
|
|
30
|
+
avatarUrl?: string;
|
|
31
|
+
followers?: number;
|
|
32
|
+
publicRepositories?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Everything Cast captures from GitHub at build time. Only public data. */
|
|
36
|
+
export type GitHubSnapshot = {
|
|
37
|
+
fetchedAt?: string;
|
|
38
|
+
login: string;
|
|
39
|
+
orgs: string[];
|
|
40
|
+
profile?: GitHubProfile;
|
|
41
|
+
contributions: { total: number; commits?: number; weeks: ContributionWeek[] };
|
|
42
|
+
mergedPullRequests: {
|
|
43
|
+
/** Across every configured organization. */
|
|
44
|
+
total: number;
|
|
45
|
+
/** Per organization, including Cratis even when it is not configured (it decides Contributor). */
|
|
46
|
+
byOrg: Record<string, number>;
|
|
47
|
+
latest: PullRequest[];
|
|
48
|
+
};
|
|
49
|
+
/** By owner/name. */
|
|
50
|
+
repositories: Record<string, RepositoryStanding>;
|
|
51
|
+
/** By owner/name. */
|
|
52
|
+
releases: Record<string, Release>;
|
|
53
|
+
/** By owner/name. */
|
|
54
|
+
stars: Record<string, number>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** A snapshot with nothing in it, for first builds without a token. */
|
|
58
|
+
export const emptySnapshot = (login: string, orgs: string[]): GitHubSnapshot => ({
|
|
59
|
+
login,
|
|
60
|
+
orgs,
|
|
61
|
+
contributions: { total: 0, weeks: [] },
|
|
62
|
+
mergedPullRequests: { total: 0, byOrg: {}, latest: [] },
|
|
63
|
+
repositories: {},
|
|
64
|
+
releases: {},
|
|
65
|
+
stars: {},
|
|
66
|
+
});
|