From 8bb188757a04c26a30e532c8591588ae94587897 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 12:16:02 -0700 Subject: [PATCH 01/31] Add maturity label feature design spec Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-06-04-maturity-label-design.md | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 docs/design/specs/2026-06-04-maturity-label-design.md diff --git a/docs/design/specs/2026-06-04-maturity-label-design.md b/docs/design/specs/2026-06-04-maturity-label-design.md new file mode 100644 index 0000000..2da042a --- /dev/null +++ b/docs/design/specs/2026-06-04-maturity-label-design.md @@ -0,0 +1,154 @@ +# Maturity Label Feature Design + +**Date:** 2026-06-04 +**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: light-to-dark teal pill badge with tooltip hint text (no emoji on rendered badge) +- Appears in both the idea list (eyebrow row) 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 teal color scale only (no emoji). + +--- + +## 3. `MaturityBadge` Component + +**Files:** +- `src/components/MaturityBadge.tsx` +- `src/components/MaturityBadge.module.css` + +### Level config + +| Value | Label | Tooltip hint | +|---|---|---| +| `speculative` | Speculative | Untested โ€” shared to invite discussion, not as a claim | +| `exploratory` | Exploratory | Early investigation โ€” findings are preliminary | +| `supported` | Supported | Backed by data or analysis, but not yet exhaustive | +| `validated` | Validated | Well-evidenced and reproducible | + +### Visual + +Four CSS classes (`.speculative`, `.exploratory`, `.supported`, `.validated`) on a shared pill shape. Colors progress from light to dark teal: + +| Level | Background | Text | Border | +|---|---|---|---| +| Speculative | `#e6f4f4` | `#4a9090` | `#b8dede` | +| Exploratory | `#b8dede` | `#2a7070` | `#7dbaba` | +| Supported | `#2a7070` | `#fff` | none | +| Validated | `#0d3d3d` | `#fff` | none | + +Tooltip uses Ant Design `` component. Unknown values fall back to Speculative rendering. + +### Props + +```ts +interface MaturityBadgeProps { + maturity: string; + className?: string; +} +``` + +--- + +## 4. Rendering Locations + +### List view (`src/components/IdeaRoll.tsx`) + +- Add `maturity` to the `IdeaRoll` GraphQL query +- Render `` in the existing `tagEyebrow` row alongside topic tags + +### Detail page (`src/templates/idea-post.tsx`) + +- Add `maturity` to the `IdeaPostByID` page query +- Render `` as a new `metaGroup` in the `metaStrip` alongside Authors / Date / Type / Program + +### Type update (`src/types/index.ts`) + +Add `maturity: string` to `IdeaPostNode` to stay in sync with the GraphQL schema. + +--- + +## 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 From d5cf561c9791eada617c09b76bbd13ea106515ed Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 12:16:47 -0700 Subject: [PATCH 02/31] Fix type update note in maturity label spec Co-Authored-By: Claude Sonnet 4.6 --- docs/design/specs/2026-06-04-maturity-label-design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/specs/2026-06-04-maturity-label-design.md b/docs/design/specs/2026-06-04-maturity-label-design.md index 2da042a..c779301 100644 --- a/docs/design/specs/2026-06-04-maturity-label-design.md +++ b/docs/design/specs/2026-06-04-maturity-label-design.md @@ -125,9 +125,9 @@ interface MaturityBadgeProps { - Add `maturity` to the `IdeaPostByID` page query - Render `` as a new `metaGroup` in the `metaStrip` alongside Authors / Date / Type / Program -### Type update (`src/types/index.ts`) +### Type update -Add `maturity: string` to `IdeaPostNode` to stay in sync with the GraphQL schema. +`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. --- From 52dc277cddb98bc356fabd6669c6009d29597385 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 13:40:23 -0700 Subject: [PATCH 03/31] Add maturity label implementation plan Co-Authored-By: Claude Sonnet 4.6 --- .../design/plans/2026-06-04-maturity-label.md | 661 ++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 docs/design/plans/2026-06-04-maturity-label.md diff --git a/docs/design/plans/2026-06-04-maturity-label.md b/docs/design/plans/2026-06-04-maturity-label.md new file mode 100644 index 0000000..1a02260 --- /dev/null +++ b/docs/design/plans/2026-06-04-maturity-label.md @@ -0,0 +1,661 @@ +# Maturity Label Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `maturity` field to ideas that renders as a light-to-dark teal pill badge (with tooltip) in both the idea list eyebrow row and the idea detail metadata strip. + +**Architecture:** New `maturity` frontmatter field on idea `.md` files, exposed in GraphQL via a resolver that defaults to `"speculative"` for existing ideas. A shared `MaturityBadge` component reads from a `maturityLevels` constants file and renders an Ant Design `Tooltip`-wrapped pill. Both `IdeaRoll` and `idea-post` template wire in the badge. + +**Tech Stack:** Gatsby 5, React 18, TypeScript, Ant Design v5, Vitest, Decap CMS, CSS Modules + +--- + +## File Map + +| Action | Path | Responsibility | +|---|---|---| +| Create | `src/constants/maturityLevels.ts` | MATURITY_CONFIG record + getMaturityConfig pure function | +| Create | `src/constants/maturityLevels.test.ts` | Unit tests for MATURITY_CONFIG and getMaturityConfig | +| Create | `src/components/MaturityBadge.tsx` | Pill badge + Ant Design Tooltip wrapper | +| Create | `src/style/maturity-badge.module.css` | Teal light-to-dark color scale | +| Create | `gatsby/resolvers/test/resolvers.test.js` | Resolver default test | +| Modify | `gatsby/resolvers/resolvers.js` | Add maturity field to createIdeaPostResolver | +| Modify | `gatsby/schema/base.gql` | Add `maturity: String` to IdeaPost type | +| Modify | `static/admin/config.yml` | Add maturity select widget after type field | +| Modify | `src/components/IdeaRoll.tsx` | Add maturity to query; render MaturityBadge in tagEyebrow | +| Modify | `src/templates/idea-post.tsx` | Add maturity to query; render MaturityBadge in metaStrip | + +--- + +## Task 1: Add maturity resolver (TDD) + +**Files:** +- Create: `gatsby/resolvers/test/resolvers.test.js` +- Modify: `gatsby/resolvers/resolvers.js` + +- [ ] **Step 1.1: Write the failing test** + +Create `gatsby/resolvers/test/resolvers.test.js`: + +```js +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"); + }); +}); +``` + +- [ ] **Step 1.2: Run test to verify it fails** + +```bash +yarn test --reporter=verbose +``` + +Expected: FAIL with `Cannot read properties of undefined (reading 'resolve')` โ€” `resolver.maturity` doesn't exist yet. + +- [ ] **Step 1.3: Add maturity resolver** + +In `gatsby/resolvers/resolvers.js`, add the `maturity` entry to the object returned by `createIdeaPostResolver`, after `preliminaryFindings`: + +```js +maturity: { + resolve: (source) => source.maturity ?? "speculative", +}, +``` + +The full function should end with: + +```js + preliminaryFindings: { + resolve: (source) => { + const raw = source.preliminaryFindings; + if (!raw || typeof raw !== "object") { + return { summary: "", figures: [] }; + } + return { + summary: stringWithDefault(raw.summary, ""), + figures: resolveToArray(raw.figures), + }; + }, + }, + maturity: { + resolve: (source) => source.maturity ?? "speculative", + }, +}); +``` + +- [ ] **Step 1.4: Run test to verify it passes** + +```bash +yarn test --reporter=verbose +``` + +Expected: PASS โ€” 2 tests pass in `gatsby/resolvers/test/resolvers.test.js` + +- [ ] **Step 1.5: Commit** + +```bash +git add gatsby/resolvers/resolvers.js gatsby/resolvers/test/resolvers.test.js +git commit -m "feat: add maturity resolver with speculative default" +``` + +--- + +## Task 2: Update GraphQL schema + +**Files:** +- Modify: `gatsby/schema/base.gql` + +- [ ] **Step 2.1: Add maturity field to IdeaPost** + +In `gatsby/schema/base.gql`, add `maturity: String` to `IdeaPost`, in alphabetical order between `introduction` and `nextSteps`: + +```graphql +type IdeaPost implements Node { + authors: [Allenite!]! + date: Date @dateformat + description: String + draft: Boolean + introduction: String + maturity: String + nextSteps: String + preliminaryFindings: PreliminaryFindings + primaryContact: Allenite + program: [String!]! + publication: String + relatedIdeas: [IdeaPost!]! + resources: [Resource!]! + slug: String! + tags: [String!]! + title: String! + type: String +} +``` + +- [ ] **Step 2.2: Regenerate Gatsby types** + +```bash +yarn develop +``` + +Wait until the terminal shows `You can now view idea-board in the browser`, then stop with `Ctrl+C`. This writes updated generated types to `.cache/` so TypeScript knows about `maturity` in `Queries.IdeaPostByIDQuery` and `Queries.IdeaRollQuery`. + +- [ ] **Step 2.3: Commit** + +```bash +git add gatsby/schema/base.gql +git commit -m "feat: add maturity field to IdeaPost GraphQL schema" +``` + +--- + +## Task 3: Update CMS config + +**Files:** +- Modify: `static/admin/config.yml` + +- [ ] **Step 3.1: Add maturity widget after the type widget** + +In `static/admin/config.yml`, locate the `type` widget block in the `ideas` collection (starts with `label: "Type"`). Insert the `maturity` widget immediately after its closing `},`: + +```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", + }, + ], + } +``` + +- [ ] **Step 3.2: Commit** + +```bash +git add static/admin/config.yml +git commit -m "feat: add maturity select field to CMS config" +``` + +--- + +## Task 4: Create maturityLevels constants (TDD) + +**Files:** +- Create: `src/constants/maturityLevels.ts` +- Create: `src/constants/maturityLevels.test.ts` + +- [ ] **Step 4.1: Write the failing test** + +Create `src/constants/maturityLevels.test.ts`: + +```ts +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, not as 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", + ); + 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"); + }); +}); +``` + +- [ ] **Step 4.2: Run test to verify it fails** + +```bash +yarn test --reporter=verbose +``` + +Expected: FAIL โ€” `Failed to resolve import "./maturityLevels"` + +- [ ] **Step 4.3: Create maturityLevels.ts** + +Create `src/constants/maturityLevels.ts`: + +```ts +interface MaturityConfig { + label: string; + hint: string; +} + +export const MATURITY_CONFIG: Record = { + speculative: { + label: "Speculative", + hint: "Untested โ€” shared to invite discussion, not as a claim", + }, + exploratory: { + label: "Exploratory", + hint: "Early investigation โ€” findings are preliminary", + }, + supported: { + label: "Supported", + hint: "Backed by data or analysis, but not yet exhaustive", + }, + validated: { + label: "Validated", + hint: "Well-evidenced and reproducible", + }, +}; + +export function getMaturityConfig(maturity: string): MaturityConfig { + return MATURITY_CONFIG[maturity] ?? MATURITY_CONFIG.speculative; +} +``` + +- [ ] **Step 4.4: Run test to verify it passes** + +```bash +yarn test --reporter=verbose +``` + +Expected: PASS โ€” 3 tests pass in `src/constants/maturityLevels.test.ts` + +- [ ] **Step 4.5: Commit** + +```bash +git add src/constants/maturityLevels.ts src/constants/maturityLevels.test.ts +git commit -m "feat: add maturity level config and getMaturityConfig utility" +``` + +--- + +## Task 5: Create MaturityBadge component + +**Files:** +- Create: `src/style/maturity-badge.module.css` +- Create: `src/components/MaturityBadge.tsx` + +- [ ] **Step 5.1: Create CSS module** + +Create `src/style/maturity-badge.module.css`: + +```css +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 0.78em; + font-weight: 600; + cursor: default; + white-space: nowrap; +} + +.speculative { + background: #e6f4f4; + color: #4a9090; + border: 1px solid #b8dede; +} + +.exploratory { + background: #b8dede; + color: #2a7070; + border: 1px solid #7dbaba; +} + +.supported { + background: #2a7070; + color: #fff; +} + +.validated { + background: #0d3d3d; + color: #fff; +} +``` + +- [ ] **Step 5.2: Create component** + +Create `src/components/MaturityBadge.tsx`: + +```tsx +import React from "react"; + +import { Tooltip } from "antd"; + +import { getMaturityConfig } from "../constants/maturityLevels"; + +const styles = require("../style/maturity-badge.module.css"); + +interface MaturityBadgeProps { + maturity: string; + className?: string; +} + +export const MaturityBadge: React.FC = ({ + maturity, + className, +}) => { + const config = getMaturityConfig(maturity); + const levelClass = styles[maturity] ?? styles.speculative; + return ( + + + {config.label} + + + ); +}; +``` + +- [ ] **Step 5.3: Run typecheck** + +```bash +yarn typeCheck +``` + +Expected: no errors + +- [ ] **Step 5.4: Commit** + +```bash +git add src/components/MaturityBadge.tsx src/style/maturity-badge.module.css +git commit -m "feat: add MaturityBadge component" +``` + +--- + +## Task 6: Wire up IdeaRoll + +**Files:** +- Modify: `src/components/IdeaRoll.tsx` + +- [ ] **Step 6.1: Add MaturityBadge import** + +Add to the imports at the top of `src/components/IdeaRoll.tsx`, after the `TagPopover` import: + +```tsx +import { MaturityBadge } from "./MaturityBadge"; +``` + +- [ ] **Step 6.2: Add maturity to GraphQL query** + +In the `useStaticQuery` call, add `maturity` to the node fields: + +```graphql +query IdeaRoll { + allIdeaPost(sort: { date: DESC }, filter: { draft: { ne: true } }) { + nodes { + id + slug + title + tags + maturity + authors { + name + } + resources { + type + name + } + } + } +} +``` + +- [ ] **Step 6.3: Update tagEyebrow render** + +The current code wraps the eyebrow div in `{item.tags.length > 0 && ...}`, which hides it for tagless ideas. Since maturity always renders, remove that condition so the eyebrow div always shows. Replace the entire tags block: + +```tsx +
+ {item.tags.map((tag, i) => ( + + {i > 0 && ( + + )} + + + ))} + +
+``` + +- [ ] **Step 6.4: Run typecheck** + +```bash +yarn typeCheck +``` + +Expected: no errors. If TypeScript complains that `maturity` doesn't exist on the node type, Gatsby types weren't regenerated yet โ€” run `yarn develop` (Task 2, Step 2.2) first. + +- [ ] **Step 6.5: Commit** + +```bash +git add src/components/IdeaRoll.tsx +git commit -m "feat: show maturity badge in idea list" +``` + +--- + +## Task 7: Wire up idea-post template + +**Files:** +- Modify: `src/templates/idea-post.tsx` + +- [ ] **Step 7.1: Add MaturityBadge import** + +Add to the imports in `src/templates/idea-post.tsx`: + +```tsx +import { MaturityBadge } from "../components/MaturityBadge"; +``` + +- [ ] **Step 7.2: Add maturity to destructured props** + +In the `IdeaPostTemplate` component signature, add `maturity` to the destructured props (alphabetically between `introduction` and `nextSteps`): + +```tsx +export const IdeaPostTemplate: React.FC< + IdeaPostNode & { + onExpandDescription?: ( + content: string, + label: string, + sectionKey: string, + ) => void; + } +> = ({ + authors, + date, + introduction, + maturity, + nextSteps, + onExpandDescription, + preliminaryFindings, + primaryContact, + program, + publication, + relatedIdeas, + resources, + slug, + tags, + title, + type, +}) => { +``` + +- [ ] **Step 7.3: Render MaturityBadge in metaStrip** + +In the `metaStrip` div, add a new `metaGroup` after the existing `type` block: + +```tsx +{type && ( +
+ Type + {type} +
+)} +{maturity && ( +
+ Maturity + +
+)} +``` + +- [ ] **Step 7.4: Add maturity to page query** + +In the `pageQuery` at the bottom of `src/templates/idea-post.tsx`, add `maturity` to the `ideaPost` fields (alphabetically between `introduction` and `title`): + +```graphql +query IdeaPostByID($id: String!) { + ideaPost(id: { eq: $id }) { + slug + authors { + name + contactId + } + primaryContact { + name + contactId + } + publication + date(formatString: "MMMM DD, YYYY") + introduction + maturity + title + description + tags + program + type + preliminaryFindings { + summary + figures { + type + url + file { + childImageSharp { + gatsbyImageData(width: 600, quality: 90) + } + } + caption + } + } + nextSteps + resources { + ...ResourceFields + } + relatedIdeas { + title + slug + } + } +} +``` + +- [ ] **Step 7.5: Run typecheck** + +```bash +yarn typeCheck +``` + +Expected: no errors + +- [ ] **Step 7.6: Commit** + +```bash +git add src/templates/idea-post.tsx +git commit -m "feat: show maturity badge in idea detail metadata strip" +``` + +--- + +## Task 8: End-to-end verification + +- [ ] **Step 8.1: Run dev server** + +```bash +yarn develop +``` + +- [ ] **Step 8.2: Check idea list at http://localhost:8000** + +Verify: +- Every idea in the list shows a teal maturity badge in the eyebrow row +- Ideas without `maturity` in their frontmatter show a light teal "Speculative" badge +- Ideas with tags still show tags alongside the badge +- Ideas with no tags show only the maturity badge (eyebrow row is no longer hidden) +- Hovering over any badge shows the tooltip hint text + +- [ ] **Step 8.3: Check an idea detail page** + +Click into any idea and verify: +- The metadata strip shows a "Maturity" row with a teal badge +- The badge color matches the level (light = Speculative, dark = Validated) +- Hovering shows the correct tooltip hint + +- [ ] **Step 8.4: Run full test suite** + +```bash +yarn test +``` + +Expected: all tests pass, including the 2 new resolver tests and 3 new maturityLevels tests From 44bf646d705673ddb99aafb59578712c75da310b Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 13:44:10 -0700 Subject: [PATCH 04/31] feat: add maturity resolver with speculative default Co-Authored-By: Claude Haiku 4.5 --- gatsby/resolvers/resolvers.js | 3 +++ gatsby/resolvers/test/resolvers.test.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 gatsby/resolvers/test/resolvers.test.js diff --git a/gatsby/resolvers/resolvers.js b/gatsby/resolvers/resolvers.js index 86c818a..0e34dc5 100644 --- a/gatsby/resolvers/resolvers.js +++ b/gatsby/resolvers/resolvers.js @@ -90,6 +90,9 @@ const createIdeaPostResolver = (reporter) => ({ }; }, }, + maturity: { + resolve: (source) => source.maturity ?? "speculative", + }, }); module.exports = { createIdeaPostResolver }; diff --git a/gatsby/resolvers/test/resolvers.test.js b/gatsby/resolvers/test/resolvers.test.js new file mode 100644 index 0000000..1d22630 --- /dev/null +++ b/gatsby/resolvers/test/resolvers.test.js @@ -0,0 +1,21 @@ +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"); + }); +}); From 1979d16b4decf22bd1bb52205a39f894f4a31aa0 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 15:53:59 -0700 Subject: [PATCH 05/31] feat: add maturity field to IdeaPost GraphQL schema Added maturity: String field to the IdeaPost GraphQL type definition, positioned alphabetically between introduction and nextSteps. Co-Authored-By: Claude Haiku 4.5 --- gatsby/schema/base.gql | 1 + 1 file changed, 1 insertion(+) diff --git a/gatsby/schema/base.gql b/gatsby/schema/base.gql index a90cd86..401c19c 100644 --- a/gatsby/schema/base.gql +++ b/gatsby/schema/base.gql @@ -57,6 +57,7 @@ type IdeaPost implements Node { description: String draft: Boolean introduction: String + maturity: String nextSteps: String preliminaryFindings: PreliminaryFindings primaryContact: Allenite From fc61499f890bc0bedb41e608cc959aaabe929628 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 16:12:11 -0700 Subject: [PATCH 06/31] feat: add maturity level config and getMaturityConfig utility Co-Authored-By: Claude Haiku 4.5 --- src/constants/maturityLevels.test.ts | 32 ++++++++++++++++++++++++++++ src/constants/maturityLevels.ts | 27 +++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/constants/maturityLevels.test.ts create mode 100644 src/constants/maturityLevels.ts diff --git a/src/constants/maturityLevels.test.ts b/src/constants/maturityLevels.test.ts new file mode 100644 index 0000000..1125530 --- /dev/null +++ b/src/constants/maturityLevels.test.ts @@ -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, not as 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", + ); + 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"); + }); +}); diff --git a/src/constants/maturityLevels.ts b/src/constants/maturityLevels.ts new file mode 100644 index 0000000..2ba1e7d --- /dev/null +++ b/src/constants/maturityLevels.ts @@ -0,0 +1,27 @@ +interface MaturityConfig { + label: string; + hint: string; +} + +export const MATURITY_CONFIG: Record = { + speculative: { + label: "Speculative", + hint: "Untested โ€” shared to invite discussion, not as a claim", + }, + exploratory: { + label: "Exploratory", + hint: "Early investigation โ€” findings are preliminary", + }, + supported: { + label: "Supported", + hint: "Backed by data or analysis, but not yet exhaustive", + }, + validated: { + label: "Validated", + hint: "Well-evidenced and reproducible", + }, +}; + +export function getMaturityConfig(maturity: string): MaturityConfig { + return MATURITY_CONFIG[maturity] ?? MATURITY_CONFIG.speculative; +} From 4dcc5a2671a38cd57ab84fcb4d581a3747f779bf Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 16:12:37 -0700 Subject: [PATCH 07/31] feat: add maturity select field to CMS config Co-Authored-By: Claude Haiku 4.5 --- static/admin/config.yml | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/static/admin/config.yml b/static/admin/config.yml index 2a24d31..0fe9443 100644 --- a/static/admin/config.yml +++ b/static/admin/config.yml @@ -43,6 +43,36 @@ collections: "analysis of existing data", "hypothesis that requires new experimentation", "micropublication", + "interesting observation", + "converting code to production product", + "applying method or tool to new data", + "other", + ], + } + - { + 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", + }, ], } - { @@ -179,7 +209,6 @@ collections: hint: "Select other resources from the Resources collection, if missing, please add to the Resources collection and then finish editing this entry.", } - { - label: "Related Ideas", name: "related_ideas", widget: "relation", From 3e1ca819b3ea808d284e2f6e6ec6a7759a29f14c Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 16:24:18 -0700 Subject: [PATCH 08/31] feat: add MaturityBadge component Co-Authored-By: Claude Haiku 4.5 --- src/components/MaturityBadge.tsx | 31 +++++++++++++++++++++++++++++ src/style/maturity-badge.module.css | 31 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/components/MaturityBadge.tsx create mode 100644 src/style/maturity-badge.module.css diff --git a/src/components/MaturityBadge.tsx b/src/components/MaturityBadge.tsx new file mode 100644 index 0000000..b726a24 --- /dev/null +++ b/src/components/MaturityBadge.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import { Tooltip } from "antd"; + +import { getMaturityConfig } from "../constants/maturityLevels"; + +const styles = require("../style/maturity-badge.module.css"); + +interface MaturityBadgeProps { + maturity: string; + className?: string; +} + +export const MaturityBadge: React.FC = ({ + className, + maturity, +}) => { + const config = getMaturityConfig(maturity); + const levelClass = styles[maturity] ?? styles.speculative; + return ( + + + {config.label} + + + ); +}; diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css new file mode 100644 index 0000000..f8ab8e5 --- /dev/null +++ b/src/style/maturity-badge.module.css @@ -0,0 +1,31 @@ +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 0.78em; + font-weight: 600; + cursor: default; + white-space: nowrap; +} + +.speculative { + background: #e6f4f4; + color: #4a9090; + border: 1px solid #b8dede; +} + +.exploratory { + background: #b8dede; + color: #2a7070; + border: 1px solid #7dbaba; +} + +.supported { + background: #2a7070; + color: #fff; +} + +.validated { + background: #0d3d3d; + color: #fff; +} From 4b9677774796dfab870f807502da53df5defb839 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 20:16:11 -0700 Subject: [PATCH 09/31] fix: use correct CSS module import pattern and normalize badge border Co-Authored-By: Claude Sonnet 4.6 --- src/components/MaturityBadge.tsx | 3 +-- src/style/maturity-badge.module.css | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MaturityBadge.tsx b/src/components/MaturityBadge.tsx index b726a24..c684b2c 100644 --- a/src/components/MaturityBadge.tsx +++ b/src/components/MaturityBadge.tsx @@ -3,8 +3,7 @@ import React from "react"; import { Tooltip } from "antd"; import { getMaturityConfig } from "../constants/maturityLevels"; - -const styles = require("../style/maturity-badge.module.css"); +import * as styles from "../style/maturity-badge.module.css"; interface MaturityBadgeProps { maturity: string; diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css index f8ab8e5..4add848 100644 --- a/src/style/maturity-badge.module.css +++ b/src/style/maturity-badge.module.css @@ -6,6 +6,7 @@ font-weight: 600; cursor: default; white-space: nowrap; + border: 1px solid transparent; } .speculative { From 88e253cc037e0a880c78523f6671872acd9fb60b Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 20:18:35 -0700 Subject: [PATCH 10/31] feat: show maturity badge in idea detail metadata strip Co-Authored-By: Claude Sonnet 4.6 --- src/templates/idea-post.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/templates/idea-post.tsx b/src/templates/idea-post.tsx index 1f40dc0..b4dfa8d 100644 --- a/src/templates/idea-post.tsx +++ b/src/templates/idea-post.tsx @@ -11,6 +11,7 @@ import { CustomReactMarkdown } from "../components/CustomReactMarkdown"; import ExpandedDescriptionView from "../components/ExpandableDescriptionView"; import FigureGallery from "../components/FigureGallery"; import { MaterialsAndMethodsComponent } from "../components/MaterialsAndMethods"; +import { MaturityBadge } from "../components/MaturityBadge"; import { PageNavSiderMenuItem } from "../components/PageNavSider"; import { TagPopover } from "../components/TagPopover"; import { RESOURCE_TYPES } from "../constants/resourceTypes"; @@ -53,6 +54,7 @@ export const IdeaPostTemplate: React.FC< authors, date, introduction, + maturity, nextSteps, onExpandDescription, preliminaryFindings, @@ -109,6 +111,12 @@ export const IdeaPostTemplate: React.FC< {type} )} + {maturity && ( +
+ Maturity + +
+ )} {program && program.length > 0 && (
Program @@ -358,6 +366,7 @@ export const pageQuery = graphql` publication date(formatString: "MMMM DD, YYYY") introduction + maturity title description tags From bc646e89655a36d60eb357a57bdae07703f7ebf1 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 20:20:44 -0700 Subject: [PATCH 11/31] feat: show maturity badge in idea list Co-Authored-By: Claude Sonnet 4.6 --- src/components/IdeaRoll.tsx | 45 ++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/components/IdeaRoll.tsx b/src/components/IdeaRoll.tsx index 89fb3d3..29fbd18 100644 --- a/src/components/IdeaRoll.tsx +++ b/src/components/IdeaRoll.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Link, graphql, useStaticQuery } from "gatsby"; +import { MaturityBadge } from "./MaturityBadge"; import { TagPopover } from "./TagPopover"; const { @@ -33,6 +34,7 @@ const IdeaRoll = ({ count }: IdeaRollProps) => { slug title tags + maturity authors { name } @@ -57,27 +59,28 @@ const IdeaRoll = ({ count }: IdeaRollProps) => {
    {ideas.map((item) => (
  • - {item.tags.length > 0 && ( -
    - {item.tags.map((tag, i) => ( - - {i > 0 && ( - - )} - - - ))} -
    - )} +
    + {item.tags.map((tag, i) => ( + + {i > 0 && ( + + )} + + + ))} + +
    {item.title} From 46a617101631fce0080c681bc8058ae20b2b2e83 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 20:56:52 -0700 Subject: [PATCH 12/31] fix: white badge background, teal border ramp, spacing from tags Co-Authored-By: Claude Sonnet 4.6 --- src/style/maturity-badge.module.css | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css index 4add848..7fe8e77 100644 --- a/src/style/maturity-badge.module.css +++ b/src/style/maturity-badge.module.css @@ -6,27 +6,27 @@ font-weight: 600; cursor: default; white-space: nowrap; + background: var(--surface-color); border: 1px solid transparent; + margin-left: 8px; } .speculative { - background: #e6f4f4; color: #4a9090; - border: 1px solid #b8dede; + border-color: #b8dede; } .exploratory { - background: #b8dede; color: #2a7070; - border: 1px solid #7dbaba; + border-color: #7dbaba; } .supported { - background: #2a7070; - color: #fff; + color: #1a5555; + border-color: #2a7070; } .validated { - background: #0d3d3d; - color: #fff; + color: #0d3d3d; + border-color: #0d3d3d; } From 43232cd72676dee735ffec554c8c428f5c4517d2 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 22:13:20 -0700 Subject: [PATCH 13/31] add example maturity labels --- ...ell-cell-junction-remodeling-to-migration-onset.md | 11 ++++++----- ...ession-between-different-fiber-scale-simulators.md | 1 + ...of-actin-compression-at-monomer-and-fiber-scale.md | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/ideas/2025-10-29-relate-cell-cell-junction-remodeling-to-migration-onset.md b/src/pages/ideas/2025-10-29-relate-cell-cell-junction-remodeling-to-migration-onset.md index 6bd8a77..47f5d79 100644 --- a/src/pages/ideas/2025-10-29-relate-cell-cell-junction-remodeling-to-migration-onset.md +++ b/src/pages/ideas/2025-10-29-relate-cell-cell-junction-remodeling-to-migration-onset.md @@ -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 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 diff --git a/src/pages/ideas/2026-02-06-compare-actin-filament-compression-between-different-fiber-scale-simulators.md b/src/pages/ideas/2026-02-06-compare-actin-filament-compression-between-different-fiber-scale-simulators.md index 0ab56cf..c5dedf0 100644 --- a/src/pages/ideas/2026-02-06-compare-actin-filament-compression-between-different-fiber-scale-simulators.md +++ b/src/pages/ideas/2026-02-06-compare-actin-filament-compression-between-different-fiber-scale-simulators.md @@ -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 diff --git a/src/pages/ideas/2026-02-06-simulations-of-actin-compression-at-monomer-and-fiber-scale.md b/src/pages/ideas/2026-02-06-simulations-of-actin-compression-at-monomer-and-fiber-scale.md index 208c709..7d138ea 100644 --- a/src/pages/ideas/2026-02-06-simulations-of-actin-compression-at-monomer-and-fiber-scale.md +++ b/src/pages/ideas/2026-02-06-simulations-of-actin-compression-at-monomer-and-fiber-scale.md @@ -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 From 68c787dcfdceb5ef0d9dce6515755207b01f8aff Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 22:13:43 -0700 Subject: [PATCH 14/31] change to popover to share styles --- src/components/MaturityBadge.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/MaturityBadge.tsx b/src/components/MaturityBadge.tsx index c684b2c..11b8c06 100644 --- a/src/components/MaturityBadge.tsx +++ b/src/components/MaturityBadge.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { Tooltip } from "antd"; +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; @@ -17,7 +18,7 @@ export const MaturityBadge: React.FC = ({ const config = getMaturityConfig(maturity); const levelClass = styles[maturity] ?? styles.speculative; return ( - + = ({ > {config.label} - + ); }; From b9bd27f59e779354c09b2029bbf11ce2cc4bdb6e Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 22:14:48 -0700 Subject: [PATCH 15/31] add maturity level --- src/pages/ideas/dev-example.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/ideas/dev-example.md b/src/pages/ideas/dev-example.md index 317b8d7..cd99852 100644 --- a/src/pages/ideas/dev-example.md +++ b/src/pages/ideas/dev-example.md @@ -16,6 +16,7 @@ templateKey: idea-post title: Investigate role of cell-cell junctions in collective cell migration (dev example) type: analysis of existing data +maturity: supported date: 2025-10-28T19:10:00.000Z authors: - Gokhan Dalgin From bf2bbed221e2232245118481d3efe191263fd69a Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 22:15:14 -0700 Subject: [PATCH 16/31] update docs based on changes --- .../design/plans/2026-06-04-maturity-label.md | 36 +++++++++---------- .../specs/2026-06-04-maturity-label-design.md | 22 ++++-------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/docs/design/plans/2026-06-04-maturity-label.md b/docs/design/plans/2026-06-04-maturity-label.md index 1a02260..b7ca2f9 100644 --- a/docs/design/plans/2026-06-04-maturity-label.md +++ b/docs/design/plans/2026-06-04-maturity-label.md @@ -12,18 +12,18 @@ ## File Map -| Action | Path | Responsibility | -|---|---|---| -| Create | `src/constants/maturityLevels.ts` | MATURITY_CONFIG record + getMaturityConfig pure function | -| Create | `src/constants/maturityLevels.test.ts` | Unit tests for MATURITY_CONFIG and getMaturityConfig | -| Create | `src/components/MaturityBadge.tsx` | Pill badge + Ant Design Tooltip wrapper | -| Create | `src/style/maturity-badge.module.css` | Teal light-to-dark color scale | -| Create | `gatsby/resolvers/test/resolvers.test.js` | Resolver default test | -| Modify | `gatsby/resolvers/resolvers.js` | Add maturity field to createIdeaPostResolver | -| Modify | `gatsby/schema/base.gql` | Add `maturity: String` to IdeaPost type | -| Modify | `static/admin/config.yml` | Add maturity select widget after type field | -| Modify | `src/components/IdeaRoll.tsx` | Add maturity to query; render MaturityBadge in tagEyebrow | -| Modify | `src/templates/idea-post.tsx` | Add maturity to query; render MaturityBadge in metaStrip | +| Action | Path | Responsibility | +| ------ | ----------------------------------------- | --------------------------------------------------------- | +| Create | `src/constants/maturityLevels.ts` | MATURITY_CONFIG record + getMaturityConfig pure function | +| Create | `src/constants/maturityLevels.test.ts` | Unit tests for MATURITY_CONFIG and getMaturityConfig | +| Create | `src/components/MaturityBadge.tsx` | Pill badge + Ant Design Tooltip wrapper | +| Create | `src/style/maturity-badge.module.css` | Teal light-to-dark color scale | +| Create | `gatsby/resolvers/test/resolvers.test.js` | Resolver default test | +| Modify | `gatsby/resolvers/resolvers.js` | Add maturity field to createIdeaPostResolver | +| Modify | `gatsby/schema/base.gql` | Add `maturity: String` to IdeaPost type | +| Modify | `static/admin/config.yml` | Add maturity select widget after type field | +| Modify | `src/components/IdeaRoll.tsx` | Add maturity to query; render MaturityBadge in tagEyebrow | +| Modify | `src/templates/idea-post.tsx` | Add maturity to query; render MaturityBadge in metaStrip | --- @@ -184,11 +184,11 @@ In `static/admin/config.yml`, locate the `type` widget block in the `ideas` coll options: [ { - label: "๐ŸŒฑ Speculative โ€” Untested, shared to invite discussion", + label: "๐ŸŒฑ Untested: shared to invite discussion and future investigation. Not a claim", value: "speculative", }, { - label: "๐ŸŒฟ Exploratory โ€” Early investigation, findings are preliminary", + label: "๐ŸŒฟ Exploratory โ€” Early investigation, findings are preliminary.", value: "exploratory", }, { @@ -236,13 +236,13 @@ describe("getMaturityConfig", () => { it("returns the correct hint for each level", () => { expect(getMaturityConfig("speculative").hint).toBe( - "Untested โ€” shared to invite discussion, not as a claim", + "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", + "Backed by data or analysis, but not yet exhaustive. Needs further work.", ); expect(getMaturityConfig("validated").hint).toBe( "Well-evidenced and reproducible", @@ -277,7 +277,7 @@ interface MaturityConfig { export const MATURITY_CONFIG: Record = { speculative: { label: "Speculative", - hint: "Untested โ€” shared to invite discussion, not as a claim", + hint: "Untested: shared to invite discussion and future investigation. Not a claim.", }, exploratory: { label: "Exploratory", @@ -285,7 +285,7 @@ export const MATURITY_CONFIG: Record = { }, supported: { label: "Supported", - hint: "Backed by data or analysis, but not yet exhaustive", + hint: "Backed by data or analysis, but not yet exhaustive. Needs further work.", }, validated: { label: "Validated", diff --git a/docs/design/specs/2026-06-04-maturity-label-design.md b/docs/design/specs/2026-06-04-maturity-label-design.md index c779301..5509a80 100644 --- a/docs/design/specs/2026-06-04-maturity-label-design.md +++ b/docs/design/specs/2026-06-04-maturity-label-design.md @@ -82,24 +82,16 @@ The emoji in the CMS dropdown helps authors scan options visually. The rendered ### Level config -| Value | Label | Tooltip hint | -|---|---|---| -| `speculative` | Speculative | Untested โ€” shared to invite discussion, not as a claim | -| `exploratory` | Exploratory | Early investigation โ€” findings are preliminary | -| `supported` | Supported | Backed by data or analysis, but not yet exhaustive | -| `validated` | Validated | Well-evidenced and reproducible | +| 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 progress from light to dark teal: - -| Level | Background | Text | Border | -|---|---|---|---| -| Speculative | `#e6f4f4` | `#4a9090` | `#b8dede` | -| Exploratory | `#b8dede` | `#2a7070` | `#7dbaba` | -| Supported | `#2a7070` | `#fff` | none | -| Validated | `#0d3d3d` | `#fff` | none | - +Four CSS classes (`.speculative`, `.exploratory`, `.supported`, `.validated`) on a shared pill shape. Colors progress from light to dark teal Tooltip uses Ant Design `` component. Unknown values fall back to Speculative rendering. ### Props From 50b85bcd124fb705a17c4415b9de1ab4fd6343cf Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 22:15:28 -0700 Subject: [PATCH 17/31] change hint text --- src/constants/maturityLevels.test.ts | 4 ++-- src/constants/maturityLevels.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/constants/maturityLevels.test.ts b/src/constants/maturityLevels.test.ts index 1125530..e6f0ce7 100644 --- a/src/constants/maturityLevels.test.ts +++ b/src/constants/maturityLevels.test.ts @@ -12,13 +12,13 @@ describe("getMaturityConfig", () => { it("returns the correct hint for each level", () => { expect(getMaturityConfig("speculative").hint).toBe( - "Untested โ€” shared to invite discussion, not as a claim", + "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", + "Backed by data or analysis, but not yet exhaustive. Needs further work.", ); expect(getMaturityConfig("validated").hint).toBe( "Well-evidenced and reproducible", diff --git a/src/constants/maturityLevels.ts b/src/constants/maturityLevels.ts index 2ba1e7d..e13b719 100644 --- a/src/constants/maturityLevels.ts +++ b/src/constants/maturityLevels.ts @@ -6,7 +6,7 @@ interface MaturityConfig { export const MATURITY_CONFIG: Record = { speculative: { label: "Speculative", - hint: "Untested โ€” shared to invite discussion, not as a claim", + hint: "Untested: shared to invite discussion and future investigation. Not a claim.", }, exploratory: { label: "Exploratory", @@ -14,7 +14,7 @@ export const MATURITY_CONFIG: Record = { }, supported: { label: "Supported", - hint: "Backed by data or analysis, but not yet exhaustive", + hint: "Backed by data or analysis, but not yet exhaustive. Needs further work.", }, validated: { label: "Validated", From 9b4dbdb511e0a40d0acebf69e6f4a60e8ed1e566 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 22:15:49 -0700 Subject: [PATCH 18/31] move badges to the right --- src/components/IdeaRoll.tsx | 39 ++++++++++++++++++---------------- src/style/idea-roll.module.css | 7 +++++- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/components/IdeaRoll.tsx b/src/components/IdeaRoll.tsx index 29fbd18..effa2de 100644 --- a/src/components/IdeaRoll.tsx +++ b/src/components/IdeaRoll.tsx @@ -11,6 +11,7 @@ const { eyebrowTag, listItem, tagEyebrow, + tagEyebrowWrapper, tagSeparator, title, } = require("../style/idea-roll.module.css"); @@ -59,24 +60,26 @@ const IdeaRoll = ({ count }: IdeaRollProps) => {
      {ideas.map((item) => (
    • -
      - {item.tags.map((tag, i) => ( - - {i > 0 && ( - - )} - - - ))} +
      +
      + {item.tags.map((tag, i) => ( + + {i > 0 && ( + + )} + + + ))} +
      diff --git a/src/style/idea-roll.module.css b/src/style/idea-roll.module.css index 5919b1e..13e7990 100644 --- a/src/style/idea-roll.module.css +++ b/src/style/idea-roll.module.css @@ -14,11 +14,17 @@ border-bottom: none; } +.tagEyebrowWrapper { + display: flex; + justify-content: space-between; +} + .tagEyebrow { display: flex; align-items: center; flex-wrap: wrap; margin-bottom: 6px; + gap: 6px; } /* Overrides antd Tag's scoped styles, which win without !important */ @@ -40,7 +46,6 @@ font-size: 10px; font-weight: 400; color: var(--border-color); - margin: 0 5px; user-select: none; } From 35c32a19a1b875a64a8ca9dd5d985a2194b31260 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 4 Jun 2026 22:15:57 -0700 Subject: [PATCH 19/31] change colors --- src/style/maturity-badge.module.css | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css index 7fe8e77..d2d9438 100644 --- a/src/style/maturity-badge.module.css +++ b/src/style/maturity-badge.module.css @@ -6,27 +6,30 @@ font-weight: 600; cursor: default; white-space: nowrap; - background: var(--surface-color); - border: 1px solid transparent; + border: 2px solid transparent; margin-left: 8px; } .speculative { color: #4a9090; border-color: #b8dede; + background: color-mix(in srgb, #b8dede 20%, transparent); } .exploratory { - color: #2a7070; - border-color: #7dbaba; + color: #367474; + border-color: #83b1b1; + background: color-mix(in srgb, #83b1b1 20%, transparent); } .supported { - color: #1a5555; - border-color: #2a7070; + color: #225959; + border-color: #4f8585; + background: color-mix(in srgb, #4f8585 30%, transparent); } .validated { color: #0d3d3d; - border-color: #0d3d3d; + border-color: #1a5858; + background: color-mix(in srgb, #1a5858 40%, transparent); } From 991e62e7e7465fb7c32fbdc95d5a54f63e861a66 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 11 Jun 2026 16:28:49 -0700 Subject: [PATCH 20/31] style: consolidate inlineDotted text-decoration into shorthand --- src/style/maturity-badge.module.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css index 01a12a6..ca46be7 100644 --- a/src/style/maturity-badge.module.css +++ b/src/style/maturity-badge.module.css @@ -37,9 +37,7 @@ .inlineDotted { background: transparent; border: none; - text-decoration: underline; - text-decoration-style: dotted; + text-decoration: underline dotted currentColor; text-underline-offset: 2px; - text-decoration-color: currentColor; cursor: help; } From 43b738e3d09cd64cb4d2ebd50c9fe71adc7dd37d Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 11 Jun 2026 16:29:32 -0700 Subject: [PATCH 21/31] feat: add variant prop to MaturityBadge for inline dotted rendering --- src/components/MaturityBadge.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/MaturityBadge.tsx b/src/components/MaturityBadge.tsx index 11b8c06..f8bf9b0 100644 --- a/src/components/MaturityBadge.tsx +++ b/src/components/MaturityBadge.tsx @@ -9,18 +9,21 @@ import * as popoverStyles from "../style/tag-popover.module.css"; interface MaturityBadgeProps { maturity: string; className?: string; + variant?: "badge" | "inline"; } export const MaturityBadge: React.FC = ({ className, maturity, + variant = "badge", }) => { const config = getMaturityConfig(maturity); const levelClass = styles[maturity] ?? styles.speculative; + const baseClass = variant === "inline" ? styles.inlineDotted : styles.badge; return ( From 642a4f26fc30f1f8fd02e579d5a57b158e2afa88 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 11 Jun 2026 16:31:29 -0700 Subject: [PATCH 22/31] style: explicitly inherit font sizing in inlineDotted variant --- src/style/maturity-badge.module.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css index ca46be7..17bbc0d 100644 --- a/src/style/maturity-badge.module.css +++ b/src/style/maturity-badge.module.css @@ -35,6 +35,8 @@ } .inlineDotted { + font-size: inherit; + font-weight: inherit; background: transparent; border: none; text-decoration: underline dotted currentColor; From 978030070b999c60f096da86f53091e49f627b62 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 11 Jun 2026 16:33:15 -0700 Subject: [PATCH 23/31] feat: show maturity badge inline with title in idea list --- src/components/IdeaRoll.tsx | 15 ++++++++++++--- src/style/idea-roll.module.css | 8 +++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/IdeaRoll.tsx b/src/components/IdeaRoll.tsx index 68d999f..476fa54 100644 --- a/src/components/IdeaRoll.tsx +++ b/src/components/IdeaRoll.tsx @@ -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 { @@ -15,6 +16,7 @@ const { textBlock, thumbnail, title, + titleRow, } = require("../style/idea-roll.module.css"); type IdeaNode = Queries.IdeaRollQuery["allIdeaPost"]["nodes"][number]; @@ -105,9 +107,16 @@ const IdeaRoll = ({ count }: IdeaRollProps) => { ))}
      )} - - {item.title} - +
      + + {item.title} + + {item.maturity && ( + + )} +
      by{" "} {item.authors diff --git a/src/style/idea-roll.module.css b/src/style/idea-roll.module.css index 6be71d6..ec4300b 100644 --- a/src/style/idea-roll.module.css +++ b/src/style/idea-roll.module.css @@ -57,6 +57,13 @@ user-select: none; } +.titleRow { + display: flex; + align-items: baseline; + flex-wrap: wrap; + margin-bottom: 6px; +} + .title { display: block; font-size: 19px; @@ -65,7 +72,6 @@ text-decoration: none; letter-spacing: -0.02em; line-height: 1.25; - margin-bottom: 6px; } .title:hover { From 360c671bd8564445a509291a92f68400a672505e Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 11 Jun 2026 16:35:47 -0700 Subject: [PATCH 24/31] style: use gap on titleRow for badge spacing; fix title display --- src/style/idea-roll.module.css | 3 ++- src/style/maturity-badge.module.css | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/style/idea-roll.module.css b/src/style/idea-roll.module.css index ec4300b..f3b0074 100644 --- a/src/style/idea-roll.module.css +++ b/src/style/idea-roll.module.css @@ -61,11 +61,12 @@ display: flex; align-items: baseline; flex-wrap: wrap; + gap: 8px; margin-bottom: 6px; } .title { - display: block; + display: inline; font-size: 19px; font-weight: 800; color: var(--primary-color); diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css index 17bbc0d..b22cb1c 100644 --- a/src/style/maturity-badge.module.css +++ b/src/style/maturity-badge.module.css @@ -7,7 +7,6 @@ cursor: default; white-space: nowrap; border: 2px solid transparent; - margin-left: 8px; } .speculative { From 906a2be1de37ad433e87628d06179eff41e8b20e Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 11 Jun 2026 16:36:21 -0700 Subject: [PATCH 25/31] feat: use inline maturity badge variant in post metadata strip --- src/templates/idea-post.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/idea-post.tsx b/src/templates/idea-post.tsx index b4dfa8d..c9d20f1 100644 --- a/src/templates/idea-post.tsx +++ b/src/templates/idea-post.tsx @@ -114,7 +114,7 @@ export const IdeaPostTemplate: React.FC< {maturity && (
      Maturity - +
      )} {program && program.length > 0 && ( From 388d3cb87bc33d85c3a3deedaaf28aa164b857c2 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 11 Jun 2026 17:13:12 -0700 Subject: [PATCH 26/31] update cursors --- src/style/idea-post.module.css | 1 + src/style/maturity-badge.module.css | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/style/idea-post.module.css b/src/style/idea-post.module.css index c1d47da..4df4165 100644 --- a/src/style/idea-post.module.css +++ b/src/style/idea-post.module.css @@ -119,6 +119,7 @@ color: var(--primary-color); background: transparent; text-decoration: none; + cursor: pointer; } /* โ”€โ”€ Body โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css index b22cb1c..a413db4 100644 --- a/src/style/maturity-badge.module.css +++ b/src/style/maturity-badge.module.css @@ -4,7 +4,7 @@ border-radius: 12px; font-size: 0.78em; font-weight: 600; - cursor: default; + cursor: pointer; white-space: nowrap; border: 2px solid transparent; } From 1f63fcda1db87ace4671cd07291223d90f0c7c6c Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 11 Jun 2026 17:13:23 -0700 Subject: [PATCH 27/31] adjust alignment --- src/style/idea-roll.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/style/idea-roll.module.css b/src/style/idea-roll.module.css index f3b0074..61334ba 100644 --- a/src/style/idea-roll.module.css +++ b/src/style/idea-roll.module.css @@ -59,7 +59,7 @@ .titleRow { display: flex; - align-items: baseline; + align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 6px; From d1f7978668631f411c97e8cea9cd7101f27ec45d Mon Sep 17 00:00:00 2001 From: meganrm Date: Wed, 12 Aug 2026 13:57:21 -0700 Subject: [PATCH 28/31] fix text color --- src/style/idea-roll.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/style/idea-roll.module.css b/src/style/idea-roll.module.css index 61334ba..f15cafc 100644 --- a/src/style/idea-roll.module.css +++ b/src/style/idea-roll.module.css @@ -69,7 +69,7 @@ display: inline; font-size: 19px; font-weight: 800; - color: var(--primary-color); + color: var(--text-primary-color); text-decoration: none; letter-spacing: -0.02em; line-height: 1.25; From 1524a2336f953098c5d470323098e8a10b45d75a Mon Sep 17 00:00:00 2001 From: meganrm Date: Mon, 17 Aug 2026 15:58:38 -0700 Subject: [PATCH 29/31] fix format --- gatsby/resolvers/test/resolvers.test.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/gatsby/resolvers/test/resolvers.test.js b/gatsby/resolvers/test/resolvers.test.js index 1d22630..5bc5e91 100644 --- a/gatsby/resolvers/test/resolvers.test.js +++ b/gatsby/resolvers/test/resolvers.test.js @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; + import { createIdeaPostResolver } from "../resolvers"; const mockReporter = { error: () => {} }; @@ -7,15 +8,27 @@ 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"); + 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"); + expect(resolver.maturity.resolve({ maturity: null })).toBe( + "speculative", + ); + expect(resolver.maturity.resolve({ maturity: undefined })).toBe( + "speculative", + ); }); }); From e50c911c37635702c93859ef0b9eafb81917e9db Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 20 Aug 2026 15:09:00 -0700 Subject: [PATCH 30/31] remove unused class --- src/style/idea-roll.module.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/style/idea-roll.module.css b/src/style/idea-roll.module.css index f15cafc..afaf53c 100644 --- a/src/style/idea-roll.module.css +++ b/src/style/idea-roll.module.css @@ -17,11 +17,6 @@ border-bottom: none; } -.tagEyebrowWrapper { - display: flex; - justify-content: space-between; -} - .textBlock { flex: 1; min-width: 0; From bb63205c3f0a7a6a65ad0f919fb85854739b6434 Mon Sep 17 00:00:00 2001 From: meganrm Date: Thu, 20 Aug 2026 15:24:29 -0700 Subject: [PATCH 31/31] put specs into a changelog --- CHANGELOG.md | 14 + CLAUDE.md | 1 + .../design/plans/2026-06-04-maturity-label.md | 661 ------------------ .../specs/2026-06-04-maturity-label-design.md | 164 ----- 4 files changed, 15 insertions(+), 825 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 docs/design/plans/2026-06-04-maturity-label.md delete mode 100644 docs/design/specs/2026-06-04-maturity-label-design.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a192ed8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +Notable feature and design changes to the Idea Board, newest first. This is a running log of *what shipped and why*, not a design-spec archive โ€” see `docs/design/specs/` for specs still under active discussion. + +## 2026-06-11 โ€” Maturity label + +Scientists were reluctant to share early-stage ideas for fear of being held accountable if they turned out wrong. Added a maturity label so authors can flag how evidenced an idea is, framing uncertainty as a positive signal rather than a warning. + +- Four fixed levels: **Speculative โ†’ Exploratory โ†’ Supported โ†’ Validated**, each with a tooltip explaining what the level means. +- New required `maturity` select field in the CMS (`static/admin/config.yml`); existing ideas without the field default to `speculative` via a Gatsby resolver. +- `MaturityBadge` component renders an ALLEN_BLUE opacity ramp (faint at Speculative, full-strength at Validated), with two variants: + - `badge` (pill) โ€” shown inline with the title in the idea list + - `inline` (dotted-underline text) โ€” shown in the idea detail metadata strip +- Out of scope (not done): bulk-updating maturity on existing ideas, filtering/sorting the index by maturity, automated maturity progression. diff --git a/CLAUDE.md b/CLAUDE.md index ddfa374..271be40 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,3 +73,4 @@ These run automatically โ€” do not skip with `--no-verify`: | `tdd` | Red-green-refactor loop for new features | | `code-review` | Review changes for quality and team standards | | `grill-me` | Stress-test a plan before implementation | +| `pre-pr` | Retire shipped plans/specs into `CHANGELOG.md` before opening a PR | diff --git a/docs/design/plans/2026-06-04-maturity-label.md b/docs/design/plans/2026-06-04-maturity-label.md deleted file mode 100644 index b7ca2f9..0000000 --- a/docs/design/plans/2026-06-04-maturity-label.md +++ /dev/null @@ -1,661 +0,0 @@ -# Maturity Label Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a `maturity` field to ideas that renders as a light-to-dark teal pill badge (with tooltip) in both the idea list eyebrow row and the idea detail metadata strip. - -**Architecture:** New `maturity` frontmatter field on idea `.md` files, exposed in GraphQL via a resolver that defaults to `"speculative"` for existing ideas. A shared `MaturityBadge` component reads from a `maturityLevels` constants file and renders an Ant Design `Tooltip`-wrapped pill. Both `IdeaRoll` and `idea-post` template wire in the badge. - -**Tech Stack:** Gatsby 5, React 18, TypeScript, Ant Design v5, Vitest, Decap CMS, CSS Modules - ---- - -## File Map - -| Action | Path | Responsibility | -| ------ | ----------------------------------------- | --------------------------------------------------------- | -| Create | `src/constants/maturityLevels.ts` | MATURITY_CONFIG record + getMaturityConfig pure function | -| Create | `src/constants/maturityLevels.test.ts` | Unit tests for MATURITY_CONFIG and getMaturityConfig | -| Create | `src/components/MaturityBadge.tsx` | Pill badge + Ant Design Tooltip wrapper | -| Create | `src/style/maturity-badge.module.css` | Teal light-to-dark color scale | -| Create | `gatsby/resolvers/test/resolvers.test.js` | Resolver default test | -| Modify | `gatsby/resolvers/resolvers.js` | Add maturity field to createIdeaPostResolver | -| Modify | `gatsby/schema/base.gql` | Add `maturity: String` to IdeaPost type | -| Modify | `static/admin/config.yml` | Add maturity select widget after type field | -| Modify | `src/components/IdeaRoll.tsx` | Add maturity to query; render MaturityBadge in tagEyebrow | -| Modify | `src/templates/idea-post.tsx` | Add maturity to query; render MaturityBadge in metaStrip | - ---- - -## Task 1: Add maturity resolver (TDD) - -**Files:** -- Create: `gatsby/resolvers/test/resolvers.test.js` -- Modify: `gatsby/resolvers/resolvers.js` - -- [ ] **Step 1.1: Write the failing test** - -Create `gatsby/resolvers/test/resolvers.test.js`: - -```js -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"); - }); -}); -``` - -- [ ] **Step 1.2: Run test to verify it fails** - -```bash -yarn test --reporter=verbose -``` - -Expected: FAIL with `Cannot read properties of undefined (reading 'resolve')` โ€” `resolver.maturity` doesn't exist yet. - -- [ ] **Step 1.3: Add maturity resolver** - -In `gatsby/resolvers/resolvers.js`, add the `maturity` entry to the object returned by `createIdeaPostResolver`, after `preliminaryFindings`: - -```js -maturity: { - resolve: (source) => source.maturity ?? "speculative", -}, -``` - -The full function should end with: - -```js - preliminaryFindings: { - resolve: (source) => { - const raw = source.preliminaryFindings; - if (!raw || typeof raw !== "object") { - return { summary: "", figures: [] }; - } - return { - summary: stringWithDefault(raw.summary, ""), - figures: resolveToArray(raw.figures), - }; - }, - }, - maturity: { - resolve: (source) => source.maturity ?? "speculative", - }, -}); -``` - -- [ ] **Step 1.4: Run test to verify it passes** - -```bash -yarn test --reporter=verbose -``` - -Expected: PASS โ€” 2 tests pass in `gatsby/resolvers/test/resolvers.test.js` - -- [ ] **Step 1.5: Commit** - -```bash -git add gatsby/resolvers/resolvers.js gatsby/resolvers/test/resolvers.test.js -git commit -m "feat: add maturity resolver with speculative default" -``` - ---- - -## Task 2: Update GraphQL schema - -**Files:** -- Modify: `gatsby/schema/base.gql` - -- [ ] **Step 2.1: Add maturity field to IdeaPost** - -In `gatsby/schema/base.gql`, add `maturity: String` to `IdeaPost`, in alphabetical order between `introduction` and `nextSteps`: - -```graphql -type IdeaPost implements Node { - authors: [Allenite!]! - date: Date @dateformat - description: String - draft: Boolean - introduction: String - maturity: String - nextSteps: String - preliminaryFindings: PreliminaryFindings - primaryContact: Allenite - program: [String!]! - publication: String - relatedIdeas: [IdeaPost!]! - resources: [Resource!]! - slug: String! - tags: [String!]! - title: String! - type: String -} -``` - -- [ ] **Step 2.2: Regenerate Gatsby types** - -```bash -yarn develop -``` - -Wait until the terminal shows `You can now view idea-board in the browser`, then stop with `Ctrl+C`. This writes updated generated types to `.cache/` so TypeScript knows about `maturity` in `Queries.IdeaPostByIDQuery` and `Queries.IdeaRollQuery`. - -- [ ] **Step 2.3: Commit** - -```bash -git add gatsby/schema/base.gql -git commit -m "feat: add maturity field to IdeaPost GraphQL schema" -``` - ---- - -## Task 3: Update CMS config - -**Files:** -- Modify: `static/admin/config.yml` - -- [ ] **Step 3.1: Add maturity widget after the type widget** - -In `static/admin/config.yml`, locate the `type` widget block in the `ideas` collection (starts with `label: "Type"`). Insert the `maturity` widget immediately after its closing `},`: - -```yaml - - { - label: "Maturity", - name: "maturity", - widget: "select", - required: true, - default: "speculative", - options: - [ - { - label: "๐ŸŒฑ Untested: shared to invite discussion and future investigation. Not a claim", - 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", - }, - ], - } -``` - -- [ ] **Step 3.2: Commit** - -```bash -git add static/admin/config.yml -git commit -m "feat: add maturity select field to CMS config" -``` - ---- - -## Task 4: Create maturityLevels constants (TDD) - -**Files:** -- Create: `src/constants/maturityLevels.ts` -- Create: `src/constants/maturityLevels.test.ts` - -- [ ] **Step 4.1: Write the failing test** - -Create `src/constants/maturityLevels.test.ts`: - -```ts -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"); - }); -}); -``` - -- [ ] **Step 4.2: Run test to verify it fails** - -```bash -yarn test --reporter=verbose -``` - -Expected: FAIL โ€” `Failed to resolve import "./maturityLevels"` - -- [ ] **Step 4.3: Create maturityLevels.ts** - -Create `src/constants/maturityLevels.ts`: - -```ts -interface MaturityConfig { - label: string; - hint: string; -} - -export const MATURITY_CONFIG: Record = { - 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; -} -``` - -- [ ] **Step 4.4: Run test to verify it passes** - -```bash -yarn test --reporter=verbose -``` - -Expected: PASS โ€” 3 tests pass in `src/constants/maturityLevels.test.ts` - -- [ ] **Step 4.5: Commit** - -```bash -git add src/constants/maturityLevels.ts src/constants/maturityLevels.test.ts -git commit -m "feat: add maturity level config and getMaturityConfig utility" -``` - ---- - -## Task 5: Create MaturityBadge component - -**Files:** -- Create: `src/style/maturity-badge.module.css` -- Create: `src/components/MaturityBadge.tsx` - -- [ ] **Step 5.1: Create CSS module** - -Create `src/style/maturity-badge.module.css`: - -```css -.badge { - display: inline-block; - padding: 2px 10px; - border-radius: 12px; - font-size: 0.78em; - font-weight: 600; - cursor: default; - white-space: nowrap; -} - -.speculative { - background: #e6f4f4; - color: #4a9090; - border: 1px solid #b8dede; -} - -.exploratory { - background: #b8dede; - color: #2a7070; - border: 1px solid #7dbaba; -} - -.supported { - background: #2a7070; - color: #fff; -} - -.validated { - background: #0d3d3d; - color: #fff; -} -``` - -- [ ] **Step 5.2: Create component** - -Create `src/components/MaturityBadge.tsx`: - -```tsx -import React from "react"; - -import { Tooltip } from "antd"; - -import { getMaturityConfig } from "../constants/maturityLevels"; - -const styles = require("../style/maturity-badge.module.css"); - -interface MaturityBadgeProps { - maturity: string; - className?: string; -} - -export const MaturityBadge: React.FC = ({ - maturity, - className, -}) => { - const config = getMaturityConfig(maturity); - const levelClass = styles[maturity] ?? styles.speculative; - return ( - - - {config.label} - - - ); -}; -``` - -- [ ] **Step 5.3: Run typecheck** - -```bash -yarn typeCheck -``` - -Expected: no errors - -- [ ] **Step 5.4: Commit** - -```bash -git add src/components/MaturityBadge.tsx src/style/maturity-badge.module.css -git commit -m "feat: add MaturityBadge component" -``` - ---- - -## Task 6: Wire up IdeaRoll - -**Files:** -- Modify: `src/components/IdeaRoll.tsx` - -- [ ] **Step 6.1: Add MaturityBadge import** - -Add to the imports at the top of `src/components/IdeaRoll.tsx`, after the `TagPopover` import: - -```tsx -import { MaturityBadge } from "./MaturityBadge"; -``` - -- [ ] **Step 6.2: Add maturity to GraphQL query** - -In the `useStaticQuery` call, add `maturity` to the node fields: - -```graphql -query IdeaRoll { - allIdeaPost(sort: { date: DESC }, filter: { draft: { ne: true } }) { - nodes { - id - slug - title - tags - maturity - authors { - name - } - resources { - type - name - } - } - } -} -``` - -- [ ] **Step 6.3: Update tagEyebrow render** - -The current code wraps the eyebrow div in `{item.tags.length > 0 && ...}`, which hides it for tagless ideas. Since maturity always renders, remove that condition so the eyebrow div always shows. Replace the entire tags block: - -```tsx -
      - {item.tags.map((tag, i) => ( - - {i > 0 && ( - - )} - - - ))} - -
      -``` - -- [ ] **Step 6.4: Run typecheck** - -```bash -yarn typeCheck -``` - -Expected: no errors. If TypeScript complains that `maturity` doesn't exist on the node type, Gatsby types weren't regenerated yet โ€” run `yarn develop` (Task 2, Step 2.2) first. - -- [ ] **Step 6.5: Commit** - -```bash -git add src/components/IdeaRoll.tsx -git commit -m "feat: show maturity badge in idea list" -``` - ---- - -## Task 7: Wire up idea-post template - -**Files:** -- Modify: `src/templates/idea-post.tsx` - -- [ ] **Step 7.1: Add MaturityBadge import** - -Add to the imports in `src/templates/idea-post.tsx`: - -```tsx -import { MaturityBadge } from "../components/MaturityBadge"; -``` - -- [ ] **Step 7.2: Add maturity to destructured props** - -In the `IdeaPostTemplate` component signature, add `maturity` to the destructured props (alphabetically between `introduction` and `nextSteps`): - -```tsx -export const IdeaPostTemplate: React.FC< - IdeaPostNode & { - onExpandDescription?: ( - content: string, - label: string, - sectionKey: string, - ) => void; - } -> = ({ - authors, - date, - introduction, - maturity, - nextSteps, - onExpandDescription, - preliminaryFindings, - primaryContact, - program, - publication, - relatedIdeas, - resources, - slug, - tags, - title, - type, -}) => { -``` - -- [ ] **Step 7.3: Render MaturityBadge in metaStrip** - -In the `metaStrip` div, add a new `metaGroup` after the existing `type` block: - -```tsx -{type && ( -
      - Type - {type} -
      -)} -{maturity && ( -
      - Maturity - -
      -)} -``` - -- [ ] **Step 7.4: Add maturity to page query** - -In the `pageQuery` at the bottom of `src/templates/idea-post.tsx`, add `maturity` to the `ideaPost` fields (alphabetically between `introduction` and `title`): - -```graphql -query IdeaPostByID($id: String!) { - ideaPost(id: { eq: $id }) { - slug - authors { - name - contactId - } - primaryContact { - name - contactId - } - publication - date(formatString: "MMMM DD, YYYY") - introduction - maturity - title - description - tags - program - type - preliminaryFindings { - summary - figures { - type - url - file { - childImageSharp { - gatsbyImageData(width: 600, quality: 90) - } - } - caption - } - } - nextSteps - resources { - ...ResourceFields - } - relatedIdeas { - title - slug - } - } -} -``` - -- [ ] **Step 7.5: Run typecheck** - -```bash -yarn typeCheck -``` - -Expected: no errors - -- [ ] **Step 7.6: Commit** - -```bash -git add src/templates/idea-post.tsx -git commit -m "feat: show maturity badge in idea detail metadata strip" -``` - ---- - -## Task 8: End-to-end verification - -- [ ] **Step 8.1: Run dev server** - -```bash -yarn develop -``` - -- [ ] **Step 8.2: Check idea list at http://localhost:8000** - -Verify: -- Every idea in the list shows a teal maturity badge in the eyebrow row -- Ideas without `maturity` in their frontmatter show a light teal "Speculative" badge -- Ideas with tags still show tags alongside the badge -- Ideas with no tags show only the maturity badge (eyebrow row is no longer hidden) -- Hovering over any badge shows the tooltip hint text - -- [ ] **Step 8.3: Check an idea detail page** - -Click into any idea and verify: -- The metadata strip shows a "Maturity" row with a teal badge -- The badge color matches the level (light = Speculative, dark = Validated) -- Hovering shows the correct tooltip hint - -- [ ] **Step 8.4: Run full test suite** - -```bash -yarn test -``` - -Expected: all tests pass, including the 2 new resolver tests and 3 new maturityLevels tests diff --git a/docs/design/specs/2026-06-04-maturity-label-design.md b/docs/design/specs/2026-06-04-maturity-label-design.md deleted file mode 100644 index c4bf93b..0000000 --- a/docs/design/specs/2026-06-04-maturity-label-design.md +++ /dev/null @@ -1,164 +0,0 @@ -# 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 `` 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 `` (default `"badge"` variant) inline after the title ``, 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 -- `` 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)