diff --git a/README.md b/README.md index eee4ea8..329e11d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/src/data/projects.yml b/src/src/data/projects.yml index fb66fd6..0fa1b6a 100644 --- a/src/src/data/projects.yml +++ b/src/src/data/projects.yml @@ -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. @@ -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 diff --git a/src/src/lib/manifest/load.ts b/src/src/lib/manifest/load.ts index 0efacc1..bff3ec1 100644 --- a/src/src/lib/manifest/load.ts +++ b/src/src/lib/manifest/load.ts @@ -22,6 +22,7 @@ export interface ResolvedProject extends ProjectRecord { order: number; packages: NonNullable; related: NonNullable; + acknowledgments: NonNullable; discussions: boolean; install: NonNullable; } @@ -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, diff --git a/src/src/lib/manifest/schema.ts b/src/src/lib/manifest/schema.ts index 41f321a..b40e9ca 100644 --- a/src/src/lib/manifest/schema.ts +++ b/src/src/lib/manifest/schema.ts @@ -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'), @@ -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(), @@ -101,12 +112,14 @@ export type ProjectRecord = z.infer; export type ExternalProjectRecord = z.infer; export type ProjectDocsConfig = z.infer; export type ProjectPackage = z.infer; +export type ProjectAcknowledgment = z.infer; export const PROJECT_DEFAULTS = { featured: false, order: 1000, packages: [] as ProjectPackage[], related: [] as string[], + acknowledgments: [] as ProjectAcknowledgment[], discussions: false, install: 'nuget', } satisfies { @@ -114,6 +127,7 @@ export const PROJECT_DEFAULTS = { order: number; packages: ProjectPackage[]; related: string[]; + acknowledgments: ProjectAcknowledgment[]; discussions: boolean; install: (typeof INSTALL_KINDS)[number]; }; diff --git a/src/src/pages/projects/[project].astro b/src/src/pages/projects/[project].astro index 66385d4..f0afbd2 100644 --- a/src/src/pages/projects/[project].astro +++ b/src/src/pages/projects/[project].astro @@ -260,15 +260,45 @@ const packageRows: PackageRow[] = releases.packages.map((pkg) => ({

{project.origin}

+ { + project.acknowledgments.length > 0 && ( +
+

Acknowledgments

+

+ {project.name} builds on the work of others. +

+
    + {project.acknowledgments.map((acknowledgment) => ( +
  • + + {acknowledgment.name} + + {acknowledgment.description && ( +

    + {acknowledgment.description} +

    + )} +
  • + ))} +
+
+ ) + } +

Where it fits

-
-

Use it when

+
+

Use it when

{project.useWhen}

-
-

It may not fit when

+
+

It may not fit when

{project.avoidWhen}

diff --git a/src/src/styles/global.css b/src/src/styles/global.css index a6ce5ce..2f8c9fa 100644 --- a/src/src/styles/global.css +++ b/src/src/styles/global.css @@ -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). */ diff --git a/src/tests/dist/built-output.test.ts b/src/tests/dist/built-output.test.ts index 0329347..e823f62 100644 --- a/src/tests/dist/built-output.test.ts +++ b/src/tests/dist/built-output.test.ts @@ -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. diff --git a/src/tests/unit/manifest.test.ts b/src/tests/unit/manifest.test.ts index bb9deb2..5832226 100644 --- a/src/tests/unit/manifest.test.ts +++ b/src/tests/unit/manifest.test.ts @@ -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);