Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,11 @@ the offending property, the expected shape, and a remediation hint.
[Documentation landing pages](#documentation-landing-pages).
4. Add relationships with `related`, or `supersededBy`/`supersedes` for
archived projects.
5. Run `just validate` — the manifest schema, catalogue page, project page,
5. Credit upstream work with `acknowledgments` (`name`, `url`, and an optional
`description`) when the project is based on, or forked from, another
project — for example ZodSharp credits the original Zod and the
`guinhx/ZodSharp` port it was forked from.
6. Run `just validate` — the manifest schema, catalogue page, project page,
docs aggregation, and release transforms are all regenerated from this one
file.

Expand Down
15 changes: 11 additions & 4 deletions src/src/data/projects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ projects:
shortDescription: Zero-allocation schema validation for C# — a Zod port.
description: >-
A high-performance schema validation library for C#, ported from
TypeScript Zod. Struct-based rules, a fluent API, JSON Schema export and
import, source-generated validators, and zero-allocation validation on
every valid input path. Integrates with System.Text.Json, Newtonsoft.Json,
and ASP.NET Core ProblemDetails.
TypeScript Zod by way of the guinhx/ZodSharp C# port. Struct-based rules,
a fluent API, JSON Schema export and import, source-generated validators,
and zero-allocation validation on every valid input path. Integrates with
System.Text.Json, Newtonsoft.Json, and ASP.NET Core ProblemDetails.
origin: >-
Schema validation was repeatedly needed at application boundaries, but existing approaches
did not combine a fluent Zod-style model with predictable, allocation-conscious execution.
Expand Down Expand Up @@ -150,6 +150,13 @@ projects:
description: ASP.NET Core ProblemDetails integration.
related:
- value-objects
acknowledgments:
- name: Zod
url: https://github.com/colinhacks/zod
description: The original JavaScript/TypeScript schema validation library.
- name: ZodSharp (guinhx)
url: https://github.com/guinhx/ZodSharp
description: The C# port of Zod, and the fork this project is based on.

- id: value-objects
name: Value Objects
Expand Down
2 changes: 2 additions & 0 deletions src/src/lib/manifest/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ResolvedProject extends ProjectRecord {
order: number;
packages: NonNullable<ProjectRecord['packages']>;
related: NonNullable<ProjectRecord['related']>;
acknowledgments: NonNullable<ProjectRecord['acknowledgments']>;
discussions: boolean;
install: NonNullable<ProjectRecord['install']>;
}
Expand Down Expand Up @@ -54,6 +55,7 @@ function withDefaults(record: ProjectRecord): ResolvedProject {
order: record.order ?? PROJECT_DEFAULTS.order,
packages: record.packages ?? PROJECT_DEFAULTS.packages,
related: record.related ?? PROJECT_DEFAULTS.related,
acknowledgments: record.acknowledgments ?? PROJECT_DEFAULTS.acknowledgments,
discussions: record.discussions ?? PROJECT_DEFAULTS.discussions,
install: record.install ?? PROJECT_DEFAULTS.install,
repoOwner,
Expand Down
14 changes: 14 additions & 0 deletions src/src/lib/manifest/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ const packageSchema = z.object({
targetFrameworks: z.array(z.string()).optional(),
});

/**
* Upstream work a project credits: the original library it was based on and/or
* the project it was forked from. Optional — only derived projects declare it.
*/
const acknowledgmentSchema = z.object({
name: z.string().min(1, 'must be a non-empty name'),
url: z.url('must be a valid URL'),
description: z.string().optional(),
});

const projectSchema = z.object({
id: z.string().regex(/^[a-z0-9-]+$/, 'must be a lowercase slug using [a-z0-9-] only'),
name: z.string().min(1, 'must be a non-empty display name'),
Expand All @@ -69,6 +79,7 @@ const projectSchema = z.object({
targetFrameworks: z.array(z.string()).optional(),
packages: z.array(packageSchema).optional(),
related: z.array(z.string()).optional(),
acknowledgments: z.array(acknowledgmentSchema).optional(),
supersedes: z.string().optional(),
supersededBy: z.string().optional(),
discussions: z.boolean().optional(),
Expand Down Expand Up @@ -101,19 +112,22 @@ export type ProjectRecord = z.infer<typeof projectSchema>;
export type ExternalProjectRecord = z.infer<typeof externalProjectSchema>;
export type ProjectDocsConfig = z.infer<typeof docsSchema>;
export type ProjectPackage = z.infer<typeof packageSchema>;
export type ProjectAcknowledgment = z.infer<typeof acknowledgmentSchema>;

export const PROJECT_DEFAULTS = {
featured: false,
order: 1000,
packages: [] as ProjectPackage[],
related: [] as string[],
acknowledgments: [] as ProjectAcknowledgment[],
discussions: false,
install: 'nuget',
} satisfies {
featured: boolean;
order: number;
packages: ProjectPackage[];
related: string[];
acknowledgments: ProjectAcknowledgment[];
discussions: boolean;
install: (typeof INSTALL_KINDS)[number];
};
38 changes: 34 additions & 4 deletions src/src/pages/projects/[project].astro
Original file line number Diff line number Diff line change
Expand Up @@ -260,15 +260,45 @@ const packageRows: PackageRow[] = releases.packages.map((pkg) => ({
<p class="mt-4 leading-relaxed text-muted">{project.origin}</p>
</section>

{
project.acknowledgments.length > 0 && (
<section>
<h2 class="text-xl font-semibold">Acknowledgments</h2>
<p class="mt-4 leading-relaxed text-muted">
{project.name} builds on the work of others.
</p>
<ul class="mt-4 list-none space-y-3">
{project.acknowledgments.map((acknowledgment) => (
<li class="rounded-xl border border-border bg-surface p-4">
<a
href={acknowledgment.url}
rel="noopener noreferrer"
target="_blank"
class="pv-link font-medium"
>
{acknowledgment.name}
</a>
{acknowledgment.description && (
<p class="mt-1 text-sm leading-relaxed text-muted">
{acknowledgment.description}
</p>
)}
</li>
))}
</ul>
</section>
)
}

<section>
<h2 class="text-xl font-semibold">Where it fits</h2>
<div class="mt-4 grid gap-4 sm:grid-cols-2">
<div class="rounded-xl border border-brand/20 bg-brand/5 p-5">
<h3 class="font-semibold">Use it when</h3>
<div class="pv-fit-panel rounded-xl p-5">
<h3 class="pv-fit-panel-title font-semibold">Use it when</h3>
<p class="mt-2 text-sm leading-relaxed text-muted">{project.useWhen}</p>
</div>
<div class="rounded-xl border border-border bg-surface p-5">
<h3 class="font-semibold">It may not fit when</h3>
<div class="pv-avoid-panel rounded-xl p-5">
<h3 class="pv-avoid-panel-title font-semibold">It may not fit when</h3>
<p class="mt-2 text-sm leading-relaxed text-muted">{project.avoidWhen}</p>
</div>
</div>
Expand Down
27 changes: 27 additions & 0 deletions src/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,33 @@
}
}

/* "Where it fits" comparison panels (the manifest `useWhen` / `avoidWhen`
* pair on the project page). Tailwind opacity utilities (`bg-brand/5`,
* `border-brand/20`) mix against transparency rather than against
* `--color-surface`, so in dark mode the affirmative and the neutral panel
* resolved to near-identical values and the two were hard to tell apart.
* Mixing against `--color-surface` — the technique `.pv-status-*` and
* `.beta-badge` already use — keeps the affirmative panel visibly branded
* in both themes. The neutral panel is deliberately plain and dashed, so the
* pair stays distinguishable without relying on hue alone. */
.pv-fit-panel {
border: 1px solid color-mix(in oklab, var(--color-brand) 30%, var(--color-border));
background-color: color-mix(in oklab, var(--color-brand) 12%, var(--color-surface));
}

.pv-fit-panel-title {
color: var(--color-brand-emphasis);
}

.pv-avoid-panel {
border: 1px dashed var(--color-border);
background-color: var(--color-surface);
}

.pv-avoid-panel-title {
color: var(--color-muted);
}

/* Chips are single-line labels: a squeezed flex row must shrink the content
* beside a chip, never wrap the chip's own label onto a second line (which
* made the status chip grow two lines tall next to a card title). */
Expand Down
19 changes: 19 additions & 0 deletions src/tests/dist/built-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ describe('built per-project llms outputs', () => {
}
});
});
describe('built project pages', () => {
test('the ZodSharp project page credits its upstream work', () => {
const content = requireBuilt('projects/zodsharp/index.html');
expect(content).toContain('Acknowledgments');
expect(content).toContain('https://github.com/colinhacks/zod');
expect(content).toContain('https://github.com/guinhx/ZodSharp');
});

test('the Where it fits panels use theme-aware classes', () => {
const content = requireBuilt('projects/zodsharp/index.html');
expect(content).toContain('class="pv-fit-panel rounded-xl p-5"');
expect(content).toContain('class="pv-avoid-panel rounded-xl p-5"');
// The previous utilities mixed against transparency rather than against
// --color-surface, so the tint vanished in dark mode and the two panels
// became indistinguishable.
expect(content).not.toContain('border-brand/20 bg-brand/5 p-5');
});
});

describe('built llms links', () => {
// The LLM text bundles are plain files rather than site pages, so every link
// to one must opt into the external-link treatment.
Expand Down
30 changes: 30 additions & 0 deletions src/tests/unit/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,36 @@ describe('project manifest', () => {
expect(external.some((project) => project.id.startsWith('purview-'))).toBe(false);
});

test('records the upstream work credited by derived projects', () => {
const projects = loadProjects();
const zodsharp = projects.find((project) => project.id === 'zodsharp');
expect(zodsharp).toBeDefined();
expect(zodsharp?.acknowledgments.map((item) => item.url)).toEqual([
'https://github.com/colinhacks/zod',
'https://github.com/guinhx/ZodSharp',
]);

// Every other project resolves to an empty list rather than undefined.
const telemetry = projects.find((project) => project.id === 'telemetry-sourcegenerator');
expect(telemetry?.acknowledgments).toEqual([]);
});

test('rejects an acknowledgment with an invalid url', () => {
expect(() =>
parseManifest(
{
projects: [
{
...makeProject('a'),
acknowledgments: [{ name: 'Zod', url: 'not-a-url' }],
},
],
},
'fixture.yml',
),
).toThrow(ManifestValidationError);
});

test('sorts projects by declared order', () => {
const projects = loadProjects();
const orders = projects.map((p) => p.order);
Expand Down
Loading