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/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..5bc5e91 --- /dev/null +++ b/gatsby/resolvers/test/resolvers.test.js @@ -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", + ); + }); +}); 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 diff --git a/src/components/IdeaRoll.tsx b/src/components/IdeaRoll.tsx index 324a6e9..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]; @@ -38,6 +40,7 @@ const IdeaRoll = ({ count }: IdeaRollProps) => { slug title tags + maturity authors { name } @@ -104,9 +107,16 @@ const IdeaRoll = ({ count }: IdeaRollProps) => { ))} )} - - {item.title} - +
+ + {item.title} + + {item.maturity && ( + + )} +
by{" "} {item.authors diff --git a/src/components/MaturityBadge.tsx b/src/components/MaturityBadge.tsx new file mode 100644 index 0000000..f8bf9b0 --- /dev/null +++ b/src/components/MaturityBadge.tsx @@ -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 = ({ + className, + maturity, + variant = "badge", +}) => { + const config = getMaturityConfig(maturity); + const levelClass = styles[maturity] ?? styles.speculative; + const baseClass = variant === "inline" ? styles.inlineDotted : styles.badge; + return ( + + + {config.label} + + + ); +}; diff --git a/src/constants/maturityLevels.test.ts b/src/constants/maturityLevels.test.ts new file mode 100644 index 0000000..e6f0ce7 --- /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 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"); + }); +}); diff --git a/src/constants/maturityLevels.ts b/src/constants/maturityLevels.ts new file mode 100644 index 0000000..e13b719 --- /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 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; +} 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 2f5d126..05ba7fe 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 diff --git a/src/pages/ideas/dev-example.md b/src/pages/ideas/dev-example.md index c58b00c..75b1c59 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 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/idea-roll.module.css b/src/style/idea-roll.module.css index 6366f1a..afaf53c 100644 --- a/src/style/idea-roll.module.css +++ b/src/style/idea-roll.module.css @@ -27,6 +27,7 @@ align-items: center; flex-wrap: wrap; margin-bottom: 6px; + gap: 6px; } /* Overrides antd Tag's scoped styles, which win without !important */ @@ -48,19 +49,25 @@ font-size: 10px; font-weight: 400; color: var(--border-color); - margin: 0 5px; user-select: none; } +.titleRow { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 6px; +} + .title { - display: block; + 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; - margin-bottom: 6px; } .title:hover { diff --git a/src/style/maturity-badge.module.css b/src/style/maturity-badge.module.css new file mode 100644 index 0000000..a413db4 --- /dev/null +++ b/src/style/maturity-badge.module.css @@ -0,0 +1,44 @@ +.badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 0.78em; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + border: 2px solid transparent; +} + +.speculative { + color: color-mix(in srgb, var(--ALLEN_BLUE) 45%, transparent); + border-color: color-mix(in srgb, var(--ALLEN_BLUE) 25%, transparent); + background: color-mix(in srgb, var(--ALLEN_BLUE) 8%, transparent); +} + +.exploratory { + color: color-mix(in srgb, var(--ALLEN_BLUE) 65%, transparent); + border-color: color-mix(in srgb, var(--ALLEN_BLUE) 40%, transparent); + background: color-mix(in srgb, var(--ALLEN_BLUE) 12%, transparent); +} + +.supported { + color: color-mix(in srgb, var(--ALLEN_BLUE) 85%, transparent); + border-color: color-mix(in srgb, var(--ALLEN_BLUE) 60%, transparent); + background: color-mix(in srgb, var(--ALLEN_BLUE) 16%, transparent); +} + +.validated { + color: var(--ALLEN_BLUE); + border-color: color-mix(in srgb, var(--ALLEN_BLUE) 80%, transparent); + background: color-mix(in srgb, var(--ALLEN_BLUE) 20%, transparent); +} + +.inlineDotted { + font-size: inherit; + font-weight: inherit; + background: transparent; + border: none; + text-decoration: underline dotted currentColor; + text-underline-offset: 2px; + cursor: help; +} diff --git a/src/templates/idea-post.tsx b/src/templates/idea-post.tsx index 1f29f4c..8c1e226 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"; @@ -54,6 +55,7 @@ export const IdeaPostTemplate: React.FC = ({ authors, date, introduction, + maturity, isPreview, nextSteps, onExpandDescription, @@ -111,6 +113,12 @@ export const IdeaPostTemplate: React.FC = ({ {type}
)} + {maturity && ( +
+ Maturity + +
+ )} {program && program.length > 0 && (
Program @@ -375,6 +383,7 @@ export const pageQuery = graphql` publication date(formatString: "MMMM DD, YYYY") introduction + maturity title description tags diff --git a/static/admin/config.yml b/static/admin/config.yml index 8c831e9..1002c90 100644 --- a/static/admin/config.yml +++ b/static/admin/config.yml @@ -46,6 +46,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", + }, ], } - {