Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8bb1887
Add maturity label feature design spec
meganrm Jun 4, 2026
d5cf561
Fix type update note in maturity label spec
meganrm Jun 4, 2026
52dc277
Add maturity label implementation plan
meganrm Jun 4, 2026
44bf646
feat: add maturity resolver with speculative default
meganrm Jun 4, 2026
1979d16
feat: add maturity field to IdeaPost GraphQL schema
meganrm Jun 4, 2026
fc61499
feat: add maturity level config and getMaturityConfig utility
meganrm Jun 4, 2026
4dcc5a2
feat: add maturity select field to CMS config
meganrm Jun 4, 2026
3e1ca81
feat: add MaturityBadge component
meganrm Jun 4, 2026
4b96777
fix: use correct CSS module import pattern and normalize badge border
meganrm Jun 5, 2026
88e253c
feat: show maturity badge in idea detail metadata strip
meganrm Jun 5, 2026
bc646e8
feat: show maturity badge in idea list
meganrm Jun 5, 2026
46a6171
fix: white badge background, teal border ramp, spacing from tags
meganrm Jun 5, 2026
43232cd
add example maturity labels
meganrm Jun 5, 2026
68c787d
change to popover to share styles
meganrm Jun 5, 2026
b9bd27f
add maturity level
meganrm Jun 5, 2026
bf2bbed
update docs based on changes
meganrm Jun 5, 2026
50b85bc
change hint text
meganrm Jun 5, 2026
9b4dbdb
move badges to the right
meganrm Jun 5, 2026
35c32a1
change colors
meganrm Jun 5, 2026
e129360
style: replace teal with ALLEN_BLUE opacity ramp on maturity badge
meganrm Jun 11, 2026
991e62e
style: consolidate inlineDotted text-decoration into shorthand
meganrm Jun 11, 2026
43b738e
feat: add variant prop to MaturityBadge for inline dotted rendering
meganrm Jun 11, 2026
642a4f2
style: explicitly inherit font sizing in inlineDotted variant
meganrm Jun 11, 2026
9780300
feat: show maturity badge inline with title in idea list
meganrm Jun 11, 2026
360c671
style: use gap on titleRow for badge spacing; fix title display
meganrm Jun 11, 2026
906a2be
feat: use inline maturity badge variant in post metadata strip
meganrm Jun 11, 2026
388d3cb
update cursors
meganrm Jun 12, 2026
1f63fcd
adjust alignment
meganrm Jun 12, 2026
d1f7978
fix text color
meganrm Aug 12, 2026
1524a23
fix format
meganrm Aug 17, 2026
e50c911
remove unused class
meganrm Aug 20, 2026
bb63205
put specs into a changelog
meganrm Aug 20, 2026
e95261a
Merge branch 'main' into feature/maturity-attribution
meganrm Aug 20, 2026
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
661 changes: 661 additions & 0 deletions docs/design/plans/2026-06-04-maturity-label.md

Large diffs are not rendered by default.

164 changes: 164 additions & 0 deletions docs/design/specs/2026-06-04-maturity-label-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Maturity Label Feature Design

**Date:** 2026-06-04 (updated 2026-06-11)
**Branch:** feature/maturity-attribution

## Problem

Scientists are reluctant to share early-stage ideas because they don't want to be held accountable if the idea turns out to be wrong. A maturity label gives authors a way to signal "this is as-is" — framing uncertainty positively rather than as a warning.

## Decision Summary

- 4 fixed levels with evidence-based labels: Speculative, Exploratory, Supported, Validated
- Visual: ALLEN_BLUE opacity ramp pill badge with tooltip hint text (no emoji on rendered badge)
- Two rendering contexts: pill badge in the idea list (after title), plain text with dotted underline in the idea detail metadata strip
- Appears in both the idea list and the idea detail page (metadata strip)
- Required in the CMS for new ideas; existing ideas default to Speculative via a schema resolver

---

## 1. Data Model

### Frontmatter

New optional string field on idea `.md` files. Existing files omit it; the resolver supplies the default.

```yaml
maturity: speculative # speculative | exploratory | supported | validated
```

### GraphQL Schema (`gatsby/schema/base.gql`)

Added to `IdeaPost` as a nullable `String`:

```graphql
type IdeaPost implements Node {
...
maturity: String
}
```

### Gatsby Resolver (`gatsby/resolvers/resolvers.js`)

Returns `"speculative"` when the frontmatter field is absent:

```js
IdeaPost: {
maturity: {
resolve: (source) => source.maturity ?? "speculative"
}
}
```

This ensures every idea always has a maturity value at query time without touching existing files.

---

## 2. CMS Configuration (`static/admin/config.yml`)

New `select` widget in the `ideas` collection, placed after the `type` field:

```yaml
- label: "Maturity"
name: "maturity"
widget: "select"
required: true
default: "speculative"
options:
- { label: "🌱 Speculative — Untested, shared to invite discussion", value: "speculative" }
- { label: "🌿 Exploratory — Early investigation, findings are preliminary", value: "exploratory" }
- { label: "🌳 Supported — Backed by data or analysis, not yet exhaustive", value: "supported" }
- { label: "🍎 Validated — Well-evidenced and reproducible", value: "validated" }
```

The emoji in the CMS dropdown helps authors scan options visually. The rendered badge on the site uses the ALLEN_BLUE color scale only (no emoji).

---

## 3. `MaturityBadge` Component

**Files:**
- `src/components/MaturityBadge.tsx`
- `src/style/maturity-badge.module.css`

### Level config

| Value | Label | Tooltip hint |
| ------------- | ----------- | ---------------------------------------------------------------------------- |
| `speculative` | Speculative | Untested: shared to invite discussion and future investigation. Not a claim. |
| `exploratory` | Exploratory | Early investigation — findings are preliminary |
| `supported` | Supported | Backed by data or analysis, but not yet exhaustive. Needs further work. |
| `validated` | Validated | Well-evidenced and reproducible |

### Visual

Four CSS classes (`.speculative`, `.exploratory`, `.supported`, `.validated`) on a shared pill shape. Colors use `--ALLEN_BLUE` with a `color-mix` opacity ramp — faint at Speculative, full blue at Validated:

| Level | Text opacity | Border opacity | Background opacity |
|-------------|-------------|----------------|-------------------|
| speculative | 45% | 25% | 8% |
| exploratory | 65% | 40% | 12% |
| supported | 85% | 60% | 16% |
| validated | 100% | 80% | 20% |

Tooltip uses Ant Design `<Popover>` component. Unknown values fall back to Speculative rendering.

A `.inlineDotted` class provides the non-pill rendering: `text-decoration: underline dotted`, `cursor: help`, no border or background. Used with the `"inline"` variant (see Props).

### Props

```ts
interface MaturityBadgeProps {
maturity: string;
className?: string;
variant?: "badge" | "inline"; // default: "badge"
}
```

- **`"badge"`** (default): pill rendering using `.badge` + level class. Used in the idea list.
- **`"inline"`**: plain text rendering using `.inlineDotted` + level class (color only, no pill). Used in the post detail metadata strip. The `Popover` wraps both variants.

---

## 4. Rendering Locations

### List view (`src/components/IdeaRoll.tsx`)

- `maturity` is already in the GraphQL query
- Render `<MaturityBadge maturity={item.maturity} />` (default `"badge"` variant) inline after the title `<Link>`, as a sibling element
- `FigureThumbnail` appears on the right side of each list row (88×56px pill shape). Rows without a figure have no reserved gap.
- **Note:** `IdeaRoll.tsx` and `idea-roll.module.css` have unresolved merge conflicts (`UU` git status) from integrating the thumbnail branch. Resolution keeps both the thumbnail and the badge rendering.

### Detail page (`src/templates/idea-post.tsx`)

- `maturity` is already in the `IdeaPostByID` page query
- `<MaturityBadge maturity={maturity} variant="inline" />` renders as colored text with a dotted underline in the `metaGroup` — no pill. The `Popover` hint remains active on hover.

### Type update

`IdeaPostNode` in `src/types/index.ts` is auto-derived from the GraphQL query via `Queries.IdeaPostByIDQuery`. No manual edit is needed — running `gatsby develop` after updating the query regenerates the Gatsby type and `maturity` flows through automatically. The inline `IdeaListItem` type in `IdeaRoll.tsx` similarly picks up the field once the query is updated.

---

## 5. Testing

### `MaturityBadge` unit test

- Each of the four level values renders the correct label and tooltip hint text
- An unknown value falls back gracefully to Speculative rendering

### Resolver unit test

- Returns the field value when `maturity` is present in frontmatter
- Returns `"speculative"` when `maturity` is absent

No changes to existing tests are required — the new field is additive.

---

## Out of Scope

- Allowing authors to update the maturity level of existing ideas in bulk (authors update individually via CMS)
- Filtering or sorting ideas by maturity level on the index page
- Any automated progression of maturity level
- Thumbnail design changes (size, shape, shadow — kept as-is)
3 changes: 3 additions & 0 deletions gatsby/resolvers/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ const createIdeaPostResolver = (reporter) => ({
};
},
},
maturity: {
resolve: (source) => source.maturity ?? "speculative",
},
});

module.exports = { createIdeaPostResolver };
34 changes: 34 additions & 0 deletions gatsby/resolvers/test/resolvers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";

import { createIdeaPostResolver } from "../resolvers";

const mockReporter = { error: () => {} };

describe("createIdeaPostResolver - maturity", () => {
const resolver = createIdeaPostResolver(mockReporter);

it("returns the maturity value when present", () => {
expect(resolver.maturity.resolve({ maturity: "speculative" })).toBe(
"speculative",
);
expect(resolver.maturity.resolve({ maturity: "exploratory" })).toBe(
"exploratory",
);
expect(resolver.maturity.resolve({ maturity: "supported" })).toBe(
"supported",
);
expect(resolver.maturity.resolve({ maturity: "validated" })).toBe(
"validated",
);
});

it("returns 'speculative' when maturity is absent", () => {
expect(resolver.maturity.resolve({})).toBe("speculative");
expect(resolver.maturity.resolve({ maturity: null })).toBe(
"speculative",
);
expect(resolver.maturity.resolve({ maturity: undefined })).toBe(
"speculative",
);
});
});
1 change: 1 addition & 0 deletions gatsby/schema/base.gql
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type IdeaPost implements Node {
description: String
draft: Boolean
introduction: String
maturity: String
nextSteps: String
preliminaryFindings: PreliminaryFindings
primaryContact: Allenite
Expand Down
16 changes: 13 additions & 3 deletions src/components/IdeaRoll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import { Link, graphql, useStaticQuery } from "gatsby";

import FigureThumbnail from "./FigureThumbnail";
import { MaturityBadge } from "./MaturityBadge";
import { TagPopover } from "./TagPopover";

const {
Expand All @@ -15,6 +16,7 @@ const {
textBlock,
thumbnail,
title,
titleRow,
} = require("../style/idea-roll.module.css");

type IdeaNode = Queries.IdeaRollQuery["allIdeaPost"]["nodes"][number];
Expand All @@ -38,6 +40,7 @@ const IdeaRoll = ({ count }: IdeaRollProps) => {
slug
title
tags
maturity
authors {
name
}
Expand Down Expand Up @@ -104,9 +107,16 @@ const IdeaRoll = ({ count }: IdeaRollProps) => {
))}
</div>
)}
<Link to={item.slug} className={title}>
{item.title}
</Link>
<div className={titleRow}>
<Link to={item.slug} className={title}>
{item.title}
</Link>
{item.maturity && (
<MaturityBadge
maturity={item.maturity}
/>
)}
</div>
<div className={byline}>
by{" "}
{item.authors
Expand Down
34 changes: 34 additions & 0 deletions src/components/MaturityBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";

import { Popover } from "antd";

import { getMaturityConfig } from "../constants/maturityLevels";
import * as styles from "../style/maturity-badge.module.css";
import * as popoverStyles from "../style/tag-popover.module.css";

interface MaturityBadgeProps {
maturity: string;
className?: string;
variant?: "badge" | "inline";
}

export const MaturityBadge: React.FC<MaturityBadgeProps> = ({
className,
maturity,
variant = "badge",
}) => {
const config = getMaturityConfig(maturity);
const levelClass = styles[maturity] ?? styles.speculative;
const baseClass = variant === "inline" ? styles.inlineDotted : styles.badge;
return (
<Popover title={config.hint} className={popoverStyles.popoverContent}>
<span
className={[baseClass, levelClass, className]
.filter(Boolean)
.join(" ")}
>
{config.label}
</span>
</Popover>
Comment on lines +24 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have little to no attention paid to keyboard nav, I'd argue we should basically ignore it until we get to a more complete place and then do a concentrated accessibility pass rather than add it in piece meal right now.

);
};
32 changes: 32 additions & 0 deletions src/constants/maturityLevels.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";

import { getMaturityConfig } from "./maturityLevels";

describe("getMaturityConfig", () => {
it("returns the correct label for each level", () => {
expect(getMaturityConfig("speculative").label).toBe("Speculative");
expect(getMaturityConfig("exploratory").label).toBe("Exploratory");
expect(getMaturityConfig("supported").label).toBe("Supported");
expect(getMaturityConfig("validated").label).toBe("Validated");
});

it("returns the correct hint for each level", () => {
expect(getMaturityConfig("speculative").hint).toBe(
"Untested: shared to invite discussion and future investigation. Not a claim.",
);
expect(getMaturityConfig("exploratory").hint).toBe(
"Early investigation — findings are preliminary",
);
expect(getMaturityConfig("supported").hint).toBe(
"Backed by data or analysis, but not yet exhaustive. Needs further work.",
);
expect(getMaturityConfig("validated").hint).toBe(
"Well-evidenced and reproducible",
);
});

it("falls back to Speculative for unknown values", () => {
expect(getMaturityConfig("unknown").label).toBe("Speculative");
expect(getMaturityConfig("").label).toBe("Speculative");
});
});
27 changes: 27 additions & 0 deletions src/constants/maturityLevels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
interface MaturityConfig {
label: string;
hint: string;
}

export const MATURITY_CONFIG: Record<string, MaturityConfig> = {
speculative: {
label: "Speculative",
hint: "Untested: shared to invite discussion and future investigation. Not a claim.",
},
exploratory: {
label: "Exploratory",
hint: "Early investigation — findings are preliminary",
},
supported: {
label: "Supported",
hint: "Backed by data or analysis, but not yet exhaustive. Needs further work.",
},
validated: {
label: "Validated",
hint: "Well-evidenced and reproducible",
},
};

export function getMaturityConfig(maturity: string): MaturityConfig {
return MATURITY_CONFIG[maturity] ?? MATURITY_CONFIG.speculative;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
---
templateKey: idea-post
title: Relate cell-cell junction remodeling to migration onset
resources:
- released-emt-dataset
program:
- EMT
date: 2025-10-28T19:10:00.000Z
Comment on lines -4 to -8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why were these removed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they were just reordered

type: analysis of existing data
maturity: exploratory
date: 2025-10-28T19:10:00.000Z
authors:
- Gokhan Dalgin
program:
- EMT
resources:
- released-emt-dataset
tags:
- EMT
- migration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
templateKey: idea-post
title: Compare actin filament compression between different fiber-scale simulators
type: micropublication
maturity: validated
date: 2026-02-06T11:42:00.000Z
authors:
- Jessica Yu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
templateKey: idea-post
title: Simulations of actin compression at monomer- and fiber-scale
type: micropublication
maturity: supported
date: 2026-02-06T11:28:00.000Z
authors:
- Jessica Yu
Expand Down
Loading
Loading