From 126462bddfb497500bc4f61d592655880afb1f27 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:57:47 +0200 Subject: [PATCH 1/3] skills: add content-write-changelog, the skill that writes the changelog entry Merges the two changelog skills kept outside this repo (the release notes writer used with Loggy, and content-write-changelog plus content-publish-changelog from prisma/ignite) into one skill that lives next to the changelog it writes. It follows the structure of the two newest entries, adds a gathering reference and a checker script, and keeps internal names, private repository names, and positioning out of this public repo. Co-Authored-By: Claude Fable 5.1 --- .claude/skills/README.md | 17 +- .../skills/content-write-changelog/SKILL.md | 75 +++++++++ .../references/filtering.md | 90 +++++++++++ .../references/gathering.md | 63 ++++++++ .../references/structure.md | 148 ++++++++++++++++++ .../references/voice.md | 74 +++++++++ .../scripts/check-entry.mjs | 136 ++++++++++++++++ 7 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/content-write-changelog/SKILL.md create mode 100644 .claude/skills/content-write-changelog/references/filtering.md create mode 100644 .claude/skills/content-write-changelog/references/gathering.md create mode 100644 .claude/skills/content-write-changelog/references/structure.md create mode 100644 .claude/skills/content-write-changelog/references/voice.md create mode 100644 .claude/skills/content-write-changelog/scripts/check-entry.mjs diff --git a/.claude/skills/README.md b/.claude/skills/README.md index f0e9c004c9..58b9d42ad2 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -1,6 +1,6 @@ # Skills -Local skills for this repo. They help you write blog posts, create blog cover images, and write documentation in a consistent style. +Local skills for this repo. They help you write blog posts, create blog cover images, write documentation, and write the changelog in a consistent style. ## How skills work @@ -15,6 +15,7 @@ A skill is a folder with a `SKILL.md` (the instructions) and sometimes `referenc |-------|-----------|-----------------| | [`content-write-blog`](content-write-blog/SKILL.md) | Scaffold a new Prisma blog post (frontmatter + section stubs) | "Draft a blog post about connection pooling" | | [`content-create-hero-image`](content-create-hero-image/SKILL.md) | Generate a post's hero (SVG) and social/OG image (PNG) in the Eclipse house style | "Create a cover image for my Compute post" | +| [`content-write-changelog`](content-write-changelog/SKILL.md) | Write the entry for prisma.io/changelog from everything that shipped since the last one, and open the pull request | "Write this month's changelog" | | [`docs-writer`](docs-writer/README.md) | Write or rewrite developer docs (how-to, concept, reference) | "Write a how-to for deploying to Prisma Compute" | | [`docs-reader-review`](docs-reader-review/SKILL.md) | Check a written page reads for an ordinary user: banned-jargon, staccato, and AI-signs checks, then a fresh reviewer reads it cold and marks what it cannot follow | "Reader review this page" | | [`docs-reader-review/scripts/check-ai-signs.sh`](docs-reader-review/scripts/check-ai-signs.sh) | Runs in CI (`docs-prose.yml`) on every hand-written docs page a pull request adds or changes (the generated ORM and CLI error-reference pages are skipped; their text is fixed upstream); its word list is dated in `references/ai-writing-signs.md` and needs re-checking against Wikipedia's [Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing) about every six months | "Update the AI-signs word list" | @@ -51,6 +52,20 @@ Generates a `hero` (editable SVG) plus a pixel-exact `meta` (Open Graph PNG) for For the render to use the real brand fonts, the bundled `assets/fonts/` is used automatically. See [`content-create-hero-image/README.md`](content-create-hero-image/README.md) for tooling details. +## content-write-changelog + +Writes the entry for [prisma.io/changelog](https://www.prisma.io/changelog) and opens the pull request. It gathers what merged since the last entry, keeps only what a user can see and what is already released, and writes it outcome first with the most impactful change at the top. + +**How to use:** + +1. Ask for the changelog ("write this month's changelog"). The window starts at the newest entry in `apps/site/content/changelog/`. +2. It lists the merged pull requests across the Prisma repositories with the `gh` CLI, then reads the release notes and the docs for the candidates it keeps. You need access to the Prisma organization for the private repositories, or the entry covers the public ones only. +3. It writes `apps/site/content/changelog/{date}.mdx` and adds screenshots to `apps/site/public/changelog/`. +4. It runs `content-write-changelog/scripts/check-entry.mjs`, which checks the frontmatter, compiles the MDX, and checks every link and anchor against production. +5. It opens a pull request whose body lists everything it held back or excluded, with a reason for each. You review, edit, and merge. + +The skill reads the same positioning doc as `content-write-blog`, from the **prisma/ignite** repository. Its rules live in four short references: [gathering](content-write-changelog/references/gathering.md), [filtering](content-write-changelog/references/filtering.md), [voice](content-write-changelog/references/voice.md), and [structure](content-write-changelog/references/structure.md). + ## docs-writer Writes or rewrites developer-facing documentation a reader can follow without getting stuck. Covers how-to, concept, and reference pages. diff --git a/.claude/skills/content-write-changelog/SKILL.md b/.claude/skills/content-write-changelog/SKILL.md new file mode 100644 index 0000000000..6d58ee8cd0 --- /dev/null +++ b/.claude/skills/content-write-changelog/SKILL.md @@ -0,0 +1,75 @@ +--- +name: content-write-changelog +description: Use when the operator asks to write, generate, or publish a changelog entry for prisma.io/changelog, says "this week's changelog" or "this month's changelog", asks what shipped since the last entry, or asks to open the changelog pull request. +metadata: + author: Prisma + version: "2026.10.2" +--- + +# Write the changelog entry + +Turn everything that shipped since the last entry into one dated MDX file at `apps/site/content/changelog/{YYYY-MM-DD}.mdx`, plus a triage note that lists every candidate you left out and why. Then open a pull request for humans to review. This skill never merges. + +The reader is a working developer with no time. In a ten-second scan the entry must answer three questions: what can I do now, what stopped hurting, and do I have to act. + +## Pre-conditions + +Stop and tell the operator if one of these is missing. + +1. **A checkout of this repo**, synced with the default branch. Work on a new branch or worktree, never on a branch that carries someone's unfinished work. +2. **The `gh` CLI, signed in** with access to the Prisma organization. Much of the platform is built in private repositories. Without access to them the entry covers the public repositories only, and you must say so in the pull request. +3. **The positioning doc**, read before you write a product claim. The authoritative copy is `docs/prisma/positioning.md` in the `prisma/ignite` repository, the same source `content-write-blog` uses. There is no copy in this repo. If you cannot read it, ask the operator for the current product names and maturity labels. + +## Rules that always apply + +- **Outcome first, product named in the same sentence.** Every unit a skimmer can land on (title, opening paragraph, section opener, bullet, guide annotation) leads with what the reader can now do and names the product. Write "You can now enroll a coding agent in Prisma with a credential of its own", not "Agent enrollment is now available". +- **Publish what the user can see, wherever the code lives.** A Console workflow, a CLI command, a REST API route, or a docs page is publishable even when its code is private. Describe the effect and link a public docs page, or link nothing. Never link a private pull request and never name a private repository. +- **Only what is released.** A merged pull request is not a shipped feature. Check each product's release before you write about it, as described in `references/gathering.md`. +- **Every claim has a public source.** A release note, a docs page, a blog post, or a public pull request. Quote dates, limits, and prices from the source and never from memory. +- **Full product names on every mention**, as the positioning doc spells them. Check the name and the maturity of each product against the docs when you write, because both change between entries. +- **Most impactful first, at every level.** The title is the biggest change. The headline sections, the bullets in a section, the actions a reader must take, and the fixes are each ordered by how many readers they reach and how much they change. +- **Never pad.** A window with a few fixes gets a short entry with no headline sections. A window with nothing user-facing gets no entry, and you report that instead. +- **No em dashes, no emoji, no vague adjectives.** State the behavior the reader will observe. + +## Workflow + +1. **Set the window.** The window starts at the newest entry in `apps/site/content/changelog/` and ends today. Read that entry in full: its structure is the precedent, and the pull requests it links mark where the window starts. + Done when you can name the start date, the end date, and the last pull request already covered. +2. **Gather.** Collect the merged pull requests, the releases, the new docs pages, and the new blog posts for the window, following `references/gathering.md`. + Done when every source in that file has been read or reported as unreachable. +3. **Gate.** Give each candidate one verdict from `references/filtering.md`: publish, rewrite, flag, or exclude. When unsure, flag. + Done when every candidate has a verdict and every flag and exclusion has a one-line reason. +4. **Confirm.** For each candidate you keep, find the public source, check that it is released, and note the docs page that documents it. + Done when no kept item rests on a pull request title alone. +5. **Write.** Draft the entry in the order `references/structure.md` gives, in the voice `references/voice.md` describes. + Done when the frontmatter is valid, `slug` equals `date`, and the date is the day the entry lands. +6. **Check.** Run `node .claude/skills/content-write-changelog/scripts/check-entry.mjs apps/site/content/changelog/{YYYY-MM-DD}.mdx --links`, then `.claude/skills/docs-reader-review/scripts/check-ai-signs.sh` on the same file. The closing `---` before the Enterprise line is the one hit the second script is expected to report. + Done when both scripts report nothing else and you have gone through the checklist below. +7. **Open the pull request.** Create the branch `changelog/{YYYY-MM-DD}`, commit the entry and its images as `docs(site): add changelog entry {YYYY-MM-DD}`, push, and open the pull request with the body in `references/structure.md`. If an entry for that date already exists, ask the operator whether to replace it or pick another date. + Done when the pull request is open and its body carries the triage note. +8. **Report and stop.** Give the operator the pull request link, the items that need a human decision, and anything you could not verify. Do not merge and do not enable auto-merge. + +## Checklist before you open the pull request + +- [ ] The title leads with a user action or outcome and names the product. It has no `Prisma:` prefix, no version number, and no chain of features joined by semicolons. +- [ ] The opening paragraph states the biggest change in its first sentence, and names every date the reader must act on. +- [ ] There are at most three headline sections, each one changes what a reader can do, and each heading states an outcome or an action. +- [ ] The most impactful item comes first in the entry, in every section, and in every list. +- [ ] "What you need to do" exists whenever a reader has to act, with one bullet per audience. +- [ ] Every breaking change says what to change. Every deprecation names the old surface, the replacement, and the date. +- [ ] Every documented surface links its docs page, and every link and anchor was checked against production. +- [ ] Every technical identifier is in backticks. +- [ ] No private pull request links, no private repository names, no internal names, no issue-tracker IDs, no customer names, and no description of how a security problem worked. +- [ ] No CI, dependency, refactor, test, SEO, or analytics items. +- [ ] Every image has alt text that describes what the screenshot shows. +- [ ] Every blog and guide link carries one sentence taken from what the page says, not from its title. +- [ ] The triage note lists every flagged and excluded candidate, including the ones with no public pull request. + +## Reference + +- `references/gathering.md`: where the changes come from, and how to tell merged from released. +- `references/filtering.md`: the verdicts, and the rules for private repositories, unreleased work, and security fixes. +- `references/voice.md`: how to turn a pull request into a sentence a reader can use. +- `references/structure.md`: frontmatter, section order, components, images, and the pull request body. +- `scripts/check-entry.mjs`: checks the frontmatter, the MDX, the images, and the links. +- The two newest entries in `apps/site/content/changelog/` are the samples. Match their shape before you match this document. diff --git a/.claude/skills/content-write-changelog/references/filtering.md b/.claude/skills/content-write-changelog/references/filtering.md new file mode 100644 index 0000000000..90317ea107 --- /dev/null +++ b/.claude/skills/content-write-changelog/references/filtering.md @@ -0,0 +1,90 @@ +# Filtering: what reaches the public changelog + +Run this on every candidate, including the ones that sit under a product heading in someone else's summary. Give each candidate exactly one verdict. When you are unsure, flag it. Every flag and every exclusion goes into the triage note with a one-line reason, so nothing is dropped silently. + +## The baseline test + +A changelog records what changed for the user, not how the team worked. + +Include user-visible behavior changes, new capabilities, fixes a reader would notice, performance gains stated as an effect, breaking changes, deprecations, and retirements. + +Exclude dependency bumps with no user effect, refactors, CI and build changes, repository configuration, test-only changes, and changes to internal documentation. + +## The four verdicts + +### Publish, or rewrite + +Publish a shipped change to a public product as it is. Rewrite a change that carries a real benefit wrapped in internal detail: strip the detail and keep the effect. + +### Flag + +Pull the item from the entry and list it for a human decision when: + +- you cannot confirm that the capability is public, for example a feature that is rolling out, limited to eligible accounts, or switched on by a flag +- it touches pricing, plan limits, a launch date, or a maturity label, and no public page states it +- it reverts something an earlier entry announced +- it is a fix with no user-visible change you can name +- you are not sure + +### Exclude + +Never publish: + +- **Internal names.** Codenames, internal tools, internal agents, and anything that is not in the public product vocabulary. +- **Internal references.** Issue-tracker IDs, chat channels, dashboards, and internal links. +- **Internal process.** CI, builds, dependency bumps, repository configuration, SEO, analytics, consent, and tracking. +- **Implementation detail.** Which vendor, model, or runtime powers a feature, how the infrastructure is laid out, and how a rollout is staged. +- **Security mechanics.** How a vulnerability worked, what a credential looks like, and how tokens are checked. +- **People and customers.** Customer names, support-ticket details, and the names or email addresses of individuals. + +## The strip test + +Remove the internal detail from a line and look at what is left. + +- Nothing a reader can use: exclude. +- A real benefit: rewrite it as user value. +- You cannot tell: flag. + +## Changes from private repositories + +Much of the platform is built in private repositories. Whether the repository is public does not decide the verdict. Whether the user can see the change does. + +Publish a Console workflow, a CLI command, a REST API route, a platform behavior, or a docs page. Describe what the user sees, link a public docs page when one exists, and otherwise link nothing. Add the item to the triage note as "no public PR". + +Exclude work that stays internal even though the product is public: how builds run, how a rollout is staged, where a service is hosted, and observability plumbing. + +Never link a private pull request. The link returns 404 for readers and gives away the repository name. + +## Products that are not generally available + +A product in Early Access or in release candidates often has a public repository full of internal engineering. Apply the test to each line, not to the repository. + +Publish a change that alters what a user can do or see: a new capability in the schema, a CLI command or flag, an editor feature, a query API addition, or a support policy. Keep the wording to what the release notes say. + +Exclude parser and compiler rewrites, internal type machinery, project close-outs, and architecture records. + +State the product's maturity once per entry, in the wording the docs use today. A change to a product before its public launch is not publishable. + +## Guides and articles + +Blog posts and docs guides are written for users, so list them. Link the published page and never the pull request that added it. + +Skip a post about an internal tool. Skip site mechanics such as redirects, metadata, images, and copy tweaks. + +A guide that is the main reference for a feature covered above is linked there, not repeated in the list. + +## Security fixes + +State the outcome, not the exploit. "Connection strings are now redacted from error logs" is publishable. The code path that leaked them is not. + +A dependency update that resolves a published advisory can be one neutral line. Name the advisory only if the public pull request already does. + +When in doubt, flag the item for a security reviewer. + +## Incidents + +A fix that follows an incident is published as the fix. Leave out the incident, the dates, the number of customers affected, and the cause. + +## People and customers + +Replace a named customer with a neutral phrase. "Fixed a replication lag issue reported by Acme" becomes "Fixed a replication lag issue that affected some databases". diff --git a/.claude/skills/content-write-changelog/references/gathering.md b/.claude/skills/content-write-changelog/references/gathering.md new file mode 100644 index 0000000000..651aa1a613 --- /dev/null +++ b/.claude/skills/content-write-changelog/references/gathering.md @@ -0,0 +1,63 @@ +# Gathering: what shipped in the window + +A changelog entry is only as good as its sources. Read all of them before you write a line. + +## 1. Set the window from the last entry + +List `apps/site/content/changelog/` and open the newest file. The window starts on that entry's date. Because an entry is written on the day it lands, pull requests merged that same day may or may not be in it, so compare against the pull requests it links instead of trusting the date alone. + +## 2. Find the repositories + +The repositories that feed the changelog carry the `loggy-core` topic. Ask GitHub for the current list instead of keeping one in your head, because repositories get added and renamed: + +```bash +gh api 'search/repositories?q=org:prisma+topic:loggy-core&per_page=50' \ + --jq '.items[] | "\(.full_name)\t\(.private)"' +``` + +The second column tells you which repositories are private. Changes from those are published without a link, as `filtering.md` explains. If the search returns only public repositories, your `gh` session cannot see the private ones. Say so in the pull request, because the Console, the REST API, and most of Prisma Compute and Prisma Postgres will be missing. + +## 3. List the merged pull requests + +For each repository, list what merged in the window and keep the title, the merge date, and the body: + +```bash +gh pr list -R "$repo" --state merged --limit 1000 \ + --search "merged:2026-08-28..2026-10-02" \ + --json number,title,mergedAt,url,body +``` + +Start the range a few days before the last entry's date, then drop what that entry already covers. + +A month across every repository is close to a thousand pull requests. Do not read them all. Read the titles first, and set aside the ones that start with `chore`, `ci`, `test`, `refactor`, or `build`, along with dependency bumps and internal project close-outs. Read the body only for the candidates that are left. The body, not the title, tells you what the user sees and whether the change is behind a flag. + +## 4. Check what is released + +A merged pull request is not a shipped feature. Each product has its own check. + +- **Prisma ORM.** The release notes in the `docs/releases/` directory of the ORM repository are the authoritative source, one file per version. Use them for the wording of features, fixes, and breaking changes, and for the pull request each one links. A pull request merged after the last version bump is not released. Leave it for the next entry. `npm view prisma dist-tags` shows what `latest` and `prev` install today. +- **Prisma Studio.** Compare the merge dates with `gh release list -R prisma/studio`. A fix merged after the newest release has not reached users. +- **Console, REST API, Prisma MCP server, Prisma Compute, Prisma Postgres.** These deploy continuously, so a merged change is usually live. Read the body for the words "flag", "gate", "rollout", "internal", "eligible", and "behind". A feature that is on for some accounts only is flagged, not published. +- **This repo.** A docs page or blog post is live when its URL returns 200 on production. Check it, because a page can merge before the app that serves it is deployed. + +## 5. Find the public source for each item + +For every feature you keep, find the docs page that documents it. In `apps/docs/content/docs/`, the URL of a page is its file path: `postgres/migrate-from-ea-to-ga.mdx` is served at `/docs/postgres/migrate-from-ea-to-ga`, and a folder name in parentheses is dropped from the URL. + +A feature with a docs page gets a link to it. A feature with no docs page and no public pull request is described by its effect, gets no link, and goes into the triage note as "no public PR" so a reviewer can confirm it is live. + +Quote every date, limit, and price from the docs page. If a pull request and the docs disagree, the docs win, and the disagreement goes into the triage note. + +## 6. List the new guides and blog posts + +List the posts in `apps/blog/content/blog/` whose frontmatter `date` falls in the window, and the docs guides that were added. Take each one-sentence annotation from the post's `metaDescription` or its opening paragraph. A title alone is not enough to describe a post accurately. + +## 7. Collect the images + +A headline section about something visual carries one screenshot. + +- If the docs already publish a screenshot of the feature, copy it from `apps/docs/public/img/` instead of taking a new one. +- For a public page, capture the live page at a width of 1440 pixels, and crop out cookie banners and announcement bars. +- For a Console screen, ask the operator for a screenshot. It must come from a demo workspace and show no customer names, email addresses, or connection strings. + +Save images as `apps/site/public/changelog/{YYYY-MM-DD}-{short-name}.png`. diff --git a/.claude/skills/content-write-changelog/references/structure.md b/.claude/skills/content-write-changelog/references/structure.md new file mode 100644 index 0000000000..27de7f0aea --- /dev/null +++ b/.claude/skills/content-write-changelog/references/structure.md @@ -0,0 +1,148 @@ +# Structure: the MDX file and the pull request + +The entry is one file at `apps/site/content/changelog/{YYYY-MM-DD}.mdx`. The two newest entries in that directory are the working samples. When they and this document disagree, follow the entries and update this document in the same pull request. + +## Frontmatter + +```yaml +--- +title: "Let your coding agent set up Prisma and ask before it touches production" +date: "2026-10-02" +version: "2026-10-02" +slug: "2026-10-02" +headline: "Let your coding agent set up Prisma and ask before it touches production" +tags: + - "Prisma" + - "Prisma Compute" + - "Prisma ORM" +canonical: "/changelog#log2026-10-02" +metaDescription: "Coding agents can now work in Prisma with their own credential and ask before they change production. Also new: ..." +share: + active: true + content: "Look at this page: " +--- +``` + +- `date` is the day the entry lands, and `slug` and `version` repeat it. `version` is never a product version. +- `headline` repeats `title`. +- `metaDescription` is one or two sentences that name the biggest change and the next two. +- `tags` start with `"Prisma"`, then list each product the entry covers. + +The index page at `/changelog` derives a single category for the entry from its tags, in `apps/site/src/lib/changelog-meta.ts`. The first match wins, in this order: a tag containing "Studio", then "Compute", then "Postgres" or "Accelerate", then "ORM". So tag a product only when the entry has real content for it, or the entry lands under the wrong filter. + +## Body, in this order + +Leave out any block that has no content. Do not repeat the title as a heading, because the page renders it from the frontmatter. + +1. **Opening.** The first sentence is the biggest change, in bold, and it has to stand alone because the index page shows only the start of the entry. The other headline changes follow in a sentence each. If the reader must act by a date, a second short paragraph opens with a bold "Two dates need action." and names each date. + +2. **Up to three headline sections, the most impactful first.** Each is an `##` heading that states the outcome, either as a sentence (`## Coding agents get their own credential and ask before production changes`) or as an action the reader can take (`## Try Prisma ORM 8 on the schema you already have`). A heading that names only the feature, such as "Agent enrollment", tells a skimmer nothing. A change earns a headline section when it changes what a reader can do and can be explained in a few short paragraphs. A fix never does. Inside each section: + - what the reader can do now, then why it matters, then how to start + - one screenshot when the change is visual + - one code block when the change is something the reader types + - a row of one or two buttons that lead to the product and to the docs + + Order them by impact: how many readers the change reaches and how much it changes what they can do. Lead with the commercial products only when two changes are of similar weight. + +3. **`## What you need to do`.** Include it whenever a reader has to act. One bullet per audience, opening with a bold label that lets readers find themselves: `- **Prisma ORM 7:** there is *nothing to change*.` Each bullet states the action, the deadline, and the guide to follow. Put the nearest deadline and the largest loss first, so a bullet about data that will be deleted comes before one about a connection string. Say so when a group has nothing to do. + +4. **`## Breaking changes`.** One line saying which versions they apply to, then one bullet per change. The bullet opens with what breaks in bold, then says what to write instead, then links the pull request. When a release has more breaking changes than most apps will hit, list the common ones and link the full release notes. + +5. **`## Deprecations`.** One bullet per deprecation, with the product in bold, the old surface, the replacement, and the date. When the reader has to act, add an italic `*Action required:*` sentence. + +6. **One `##` section per product**, headed with the exact product name. The section opens with one bold sentence that states its biggest change. Bullets follow, most impactful first, each opening with the outcome. Group related bullets under a plain sentence instead of adding sub-headings. Order the sections by how much changed for users. + +7. **`## Fixes`.** Always visible and grouped by product under bold labels. Within a product, wrong results and data loss come first, then failures, then cosmetic fixes. Open the fixes that matter most with a bold sentence, and say what went wrong before when that helps a reader recognize the bug. + +8. **`## Guides and articles`.** One bullet per new post or guide. Link the published page, and follow it with a colon and one sentence about what the reader gets. + +9. **Around Prisma.** Events and community news, each under its own `##` heading, and only when there is something a reader can attend or use. + +10. **The closing line.** A thematic break, then this line and nothing else: + + ```mdx + --- + + *Need help applying these changes in production? [Prisma Enterprise Support](/enterprise) can help with schema design, performance, security, and compliance.* + ``` + +## Short windows + +A week with a few fixes gets the opening paragraph, the product sections that have content, and the fixes. Do not promote a small change to a headline section to fill space. + +## Links + +- Docs links are absolute: `https://www.prisma.io/docs/...`. Site pages are relative: `/pricing`, `/extensions`, `/enterprise`. +- Link the docs page wherever a documented feature is mentioned. Use only paths you have checked against production. +- Pull request references trail the sentence in parentheses. For the ORM repository write `([#30365](https://github.com/prisma/orm/pull/30365))`. For any other public repository add its name: `([prisma-engines#5851](url))`. Changes from private repositories get no reference. +- A button that leads to the Console carries UTM parameters, as in `?utm_source=changelog&utm_medium=cta&utm_campaign=agent-enrollment`. + +## Components + +Buttons go in a wrapper that opts out of prose styling. Use the filled button for the main action and the outline button for the second one. `ctaLocation` is `changelog-` followed by a short name for the section. + +```mdx +
+ Connect the Prisma MCP server + See every tool +
+``` + +Code blocks take a language and a title, and stay small enough to read at a glance: + +````mdx +```bash title="Terminal" +npx prisma@latest orm init --from-prisma7-schema prisma/schema.prisma +``` +```` + +Take every identifier in a snippet from the release notes or the docs. If you had to guess an import path or an option name, say so in the triage note. + +To show inline code that itself contains a backtick, wrap it in double backticks: ``` ``@default(sql`gen_random_uuid()`)`` ```. + +## Images + +Images live in `apps/site/public/changelog/` and are named `{YYYY-MM-DD}-{short-name}.png`. Reference them as `/changelog/{file}`. The alt text describes what the screenshot shows, in one sentence, for a reader who cannot see it. + +## The pull request + +Branch `changelog/{YYYY-MM-DD}`, commit `docs(site): add changelog entry {YYYY-MM-DD}`, and put one paragraph in the commit body that states the window and the headline changes. + +The pull request body leads with the conclusion and is short enough to skim: + +```markdown +## What + +Adds the changelog entry for {window} at `apps/site/content/changelog/{date}.mdx`. + +**The headline is {biggest change}.** {The other headline changes, in one sentence.} + +## Dates readers must act on + +- **{date}:** {what happens} + +## Review focus + +- {Naming or maturity wording a reviewer should confirm.} +- {Items sourced from private repositories that are worth a look in the product.} + +## Triage notes + +**Held back, needs confirmation that it is public** + +- {item}: {reason} + +**Held for the next entry, not released yet** + +- {item}: {reason} + +**Excluded** + +- {item or group}: {reason} + +## Checks + +- {What you ran and what it reported.} +``` + +Leave out "Dates readers must act on" when there are none. The triage notes describe held and excluded work in the same public-safe terms as the entry, because the pull request is public too. diff --git a/.claude/skills/content-write-changelog/references/voice.md b/.claude/skills/content-write-changelog/references/voice.md new file mode 100644 index 0000000000..f5501103cc --- /dev/null +++ b/.claude/skills/content-write-changelog/references/voice.md @@ -0,0 +1,74 @@ +# Voice: short, concrete, and useful to someone skimming + +The entry is read by a developer who is deciding whether an update affects them. Write so they can decide without reading twice. + +## Outcome first + +The first sentence of every item answers one question: what can I do now that I could not do before? After that, in this order, come why it matters, how it works, and the technical detail. Stop as soon as the reader can act. + +- **Write from the reader's side.** Describe what they experience, not how it was built. Second person works well when it is concrete: "You can now delete a project that still has active deployments." +- **One idea per sentence.** When a sentence contains "and", check whether it holds two changes. If it does, split it. Two changes never share a bullet. +- **Mechanics come after the benefit.** Config files, setup pull requests, and API internals do not appear before the reader knows what they get. +- **A snippet follows its explanation.** Code never sits between the outcome and the sentence that explains it. +- **Skip the history.** "Was gated, then tested, then released" becomes what is true today. Add one "before" sentence only when the contrast helps: "The choice was dropped before, and the project deployed in US East." +- **Question every sentence.** If a user would not care, delete it. +- **Prefer two short sentences to one long one.** "It used to map to `userProfile`. It now maps to `UserProfile`." reads faster than one sentence joined with "and". +- **Open a bullet with a verb when the reader can do something new.** "Change what is live", "Find out what went wrong", and "Skip duplicates on bulk inserts" tell a skimmer more than a noun phrase does. + +## Name the product + +The product name appears wherever a reader might land: the title, the opening paragraph, section openers, and the first bullet of a list. A reader who jumps to the middle of the entry should know which product a line is about. + +Use the names in the positioning doc, in full, every time. Check two things against the docs before you write, because they change between entries: + +- **The name.** Follow what the docs call the product today. For example, the docs say "Prisma ORM" and add a version number only when two versions are being contrasted, as in "Prisma ORM 8 reads the Prisma 7 schema you already have". +- **The maturity.** Early Access, release candidate, and generally available mean different things to a reader. State a product's maturity once per entry, in the docs' wording, and do not repeat it in headings. + +## Titles + +The title tells someone scanning the changelog index whether to open the entry. + +- Lead with what the reader can do, and name the product: "Let your coding agent set up Prisma and ask before it touches production". +- Say what the reader gets, not what the feature is made of. "Enroll your coding agent in Prisma with its own credential" names the mechanism. The version above names the two things the reader cares about. +- A launch is the exception, where the event is the news: "Prisma Compute is now generally available". The index page gives the featured treatment to titles that say "generally available", "now available", or "now in beta" or "preview", so use those phrases only for a real launch. +- One claim per title. When an entry covers several products, lead with the biggest change and let the opening paragraph carry the rest. +- No `Prisma:` prefix, no version number, and no verbs like "lands", "arrives", or "ships". + +## Words to cut + +- **Openers that delay the point:** "We're excited to", "Today we're announcing", "As always". +- **Filler:** "stay tuned", "under the hood", "and much more". +- **Intensifiers:** "very", "really", "truly", "simply". +- **Jargon:** "leverage", "robust", "best-in-class", "supercharge". +- **Claims of importance** with no change behind them: "This is huge", "A better experience". + +These adjectives need proof in the same sentence, or they go: `seamless`, `effortless`, `powerful`, `fast`, `simple`, `easier`, `cleaner`, `richer`, `clearer`. Replace them with what the reader will observe. Not "builds are easier to debug" but "a failed build sends an email and shows its logs in the Console". + +The full list of patterns that make text read as machine-written is in `.claude/skills/docs-reader-review/references/ai-writing-signs.md`, and `check-ai-signs.sh` in the same skill finds the ones a regular expression can catch. + +## Sentence rules + +- No em dashes. Use a comma, a period, or parentheses. +- No emoji. +- No rhetorical questions and no "It's not X, it's Y". +- Active voice and present tense: "Views now support `@unique`." +- Use a number only when it appears in the source. Never estimate one. +- Put every exact identifier in backticks: packages, import paths, config files, API fields, routes, commands, and error codes. Product surfaces such as the Console and the REST API stay plain text. +- A link says what the reader gets there: "The [enrollment guide](url) covers the policy rules." Never "Read more". + +## Emphasis + +Bold the one phrase in a paragraph that a skimmer must not miss, and bold the lead of a bullet when the bullet opens with the outcome. Use italics for the short qualifier that changes a decision, such as *never returned* or *nothing to change*. If everything is bold, nothing is. + +## From pull request to sentence + +Strip the mechanism and keep the effect. + +- "Use every key column in includes, nested writes and multi-table variants" becomes "`include()` across a composite foreign key returns the right rows. It matched on the first key column only, which returned related rows that belonged to other parents." +- "Schedule paid-to-paid downgrades for the end of the period" becomes "Downgrades between paid plans take effect at the end of the billing period. You keep your current plan until then." +- "Reduce included-result decoding overhead" has no effect a reader can observe without a number from the source, so it is excluded. +- A title that carries only an issue-tracker ID and an internal project name is excluded. + +## Read it once more as the reader + +Before you hand the entry over, read only the title, the opening paragraph, and the first sentence of each section. A reader who stops there should know what changed, which products it touches, and whether they have to do anything. diff --git a/.claude/skills/content-write-changelog/scripts/check-entry.mjs b/.claude/skills/content-write-changelog/scripts/check-entry.mjs new file mode 100644 index 0000000000..1439ee92a6 --- /dev/null +++ b/.claude/skills/content-write-changelog/scripts/check-entry.mjs @@ -0,0 +1,136 @@ +#!/usr/bin/env node +// Checks a changelog entry before it goes into a pull request. +// Usage: node check-entry.mjs apps/site/content/changelog/2026-10-02.mdx [--links] [--modules ] +// +// Always checked: required frontmatter, slug and version equal to date, the file name, em dashes +// and emoji, private pull request links, images on disk with alt text, and that the MDX compiles. +// With --links: every external link returns 200, and every #anchor exists on the page it points to. +// --modules names a directory whose node_modules holds @mdx-js/mdx, for a checkout or worktree +// that has no install of its own. Exit 1 on any finding. +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +const args = process.argv.slice(2); +const file = args.find((a) => a.endsWith(".mdx")); +const checkLinks = args.includes("--links"); +const modulesFlag = args.indexOf("--modules"); +if (!file) { + console.error("Usage: check-entry.mjs [--links] [--modules ]"); + process.exit(2); +} + +const path = resolve(file); +const siteDir = resolve(dirname(path), "../.."); +const repoRoot = resolve(siteDir, "../.."); +const source = readFileSync(path, "utf8"); +const findings = []; +const fail = (message) => findings.push(message); + +const match = source.match(/^---\n([\s\S]*?)\n---\n/); +if (!match) { + console.error("No frontmatter found."); + process.exit(1); +} +const frontmatter = match[1]; +const body = source.slice(match[0].length); +const field = (name) => frontmatter.match(new RegExp(`^${name}:\\s*"?(.*?)"?\\s*$`, "m"))?.[1]; + +for (const name of ["title", "date", "version", "slug", "headline", "canonical", "metaDescription"]) { + if (!field(name)) fail(`frontmatter: "${name}" is missing`); +} +if (!/^tags:\s*\n(\s+-\s+.+\n?)+/m.test(frontmatter)) fail("frontmatter: tags are missing"); +const date = field("date"); +if (date) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) fail(`frontmatter: date "${date}" is not YYYY-MM-DD`); + if (field("slug") !== date) fail("frontmatter: slug must equal date"); + if (field("version") !== date) fail("frontmatter: version must equal date, not a product version"); + if (basename(path) !== `${date}.mdx`) fail(`file name must be ${date}.mdx`); + if (field("canonical") !== `/changelog#log${date}`) fail(`frontmatter: canonical must be /changelog#log${date}`); +} +if (field("title") !== field("headline")) fail("frontmatter: headline must repeat title"); +if (/^Prisma:|\bv?\d+\.\d+\.\d+\b/.test(field("title") ?? "")) fail("title: no Prisma: prefix and no version number"); + +const lines = source.split("\n"); +lines.forEach((line, i) => { + if (/\u2014|\u2013/.test(line)) fail(`line ${i + 1}: dash character, use a comma, a period, or parentheses`); + if (/\p{Extended_Pictographic}/u.test(line)) fail(`line ${i + 1}: emoji`); +}); + +const prose = body.replace(/```[\s\S]*?```/g, ""); +if (/^##\s/m.test(prose.trimStart().split("\n")[0])) fail("body: open with a paragraph, not a heading"); + +const links = [...prose.matchAll(/(? m[1] ?? m[2]); +const privateRepos = new Set(); +for (const link of links) { + const pr = link.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/); + if (pr) privateRepos.add(pr[1]); +} + +for (const image of prose.matchAll(/!\[([^\]]*)\]\(([^)\s]+)\)/g)) { + const [, alt, src] = image; + if (alt.trim().length < 20) fail(`image ${src}: alt text is missing or too short to describe the image`); + if (src.startsWith("/") && !existsSync(join(siteDir, "public", src))) fail(`image ${src}: not found in apps/site/public`); +} + +function findMdx() { + const roots = [modulesFlag >= 0 ? resolve(args[modulesFlag + 1]) : null, repoRoot].filter(Boolean); + for (const root of roots) { + const direct = join(root, "node_modules/@mdx-js/mdx/index.js"); + if (existsSync(direct)) return direct; + const store = join(root, "node_modules/.pnpm"); + if (!existsSync(store)) continue; + const dir = readdirSync(store).find((name) => name.startsWith("@mdx-js+mdx@")); + if (dir) return join(store, dir, "node_modules/@mdx-js/mdx/index.js"); + } + return null; +} + +const mdxPath = findMdx(); +if (!mdxPath) { + fail("mdx: @mdx-js/mdx not found, run pnpm install or pass --modules "); +} else { + const { compile } = await import(pathToFileURL(mdxPath).href); + try { + await compile(body); + } catch (error) { + fail(`mdx: does not compile: ${error.message}`); + } +} + +async function status(url, method = "GET") { + try { + const response = await fetch(url, { method, redirect: "follow", signal: AbortSignal.timeout(20000) }); + return response; + } catch { + return null; + } +} + +if (checkLinks) { + for (const repo of privateRepos) { + const response = await status(`https://github.com/${repo}`); + if (!response || response.status !== 200) fail(`link: ${repo} is not a public repository, remove its pull request links`); + } + const unique = [...new Set(links)].filter((link) => !/github\.com\/[^/]+\/[^/]+\/pull\//.test(link)); + for (const link of unique) { + const url = link.startsWith("/") ? `https://www.prisma.io${link}` : link; + const [page, anchor] = url.split("#"); + const response = await status(page); + if (!response || response.status !== 200) { + fail(`link: ${url} returned ${response ? response.status : "no response"}`); + continue; + } + if (anchor && !/^log\d{4}/.test(anchor)) { + const html = await response.text(); + if (!html.includes(`id="${anchor}"`)) fail(`link: ${url} has no element with id "${anchor}"`); + } + } +} + +if (findings.length) { + console.error(`${basename(path)}: ${findings.length} finding${findings.length === 1 ? "" : "s"}`); + for (const finding of findings) console.error(` - ${finding}`); + process.exit(1); +} +console.log(`${basename(path)}: ok${checkLinks ? ", links checked" : ""}`); From e6b624e9414631213532b2f14d3c33e744625f20 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:12:13 +0200 Subject: [PATCH 2/3] skills: make content-write-changelog explain changes, and fix review findings The first entry written with the skill read like a spec: clipped sentences, bold labels on every bullet, and vocabulary from the release notes. The voice reference now builds on docs-reader-review (explain, do not state; the signs of AI writing; the banned terms), and the workflow adds its three checker scripts and a cold-reader round before the pull request. Also from review: paginate the repository search, take the window from step 1 in the pull request command, reject impossible dates, check pull request links, and say what counts as a source for a change built in a private repository. Co-Authored-By: Claude Fable 5.1 --- .claude/skills/README.md | 5 +- .../skills/content-write-changelog/SKILL.md | 23 ++-- .../references/gathering.md | 8 +- .../references/structure.md | 14 +-- .../references/voice.md | 102 ++++++++++-------- .../scripts/check-entry.mjs | 18 +++- 6 files changed, 101 insertions(+), 69 deletions(-) diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 58b9d42ad2..4b85104fae 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -54,7 +54,7 @@ For the render to use the real brand fonts, the bundled `assets/fonts/` is used ## content-write-changelog -Writes the entry for [prisma.io/changelog](https://www.prisma.io/changelog) and opens the pull request. It gathers what merged since the last entry, keeps only what a user can see and what is already released, and writes it outcome first with the most impactful change at the top. +Writes the entry for [prisma.io/changelog](https://www.prisma.io/changelog) and opens the pull request. It gathers what merged since the last entry, keeps only what a user can see and what is already released, and explains each change from the reader's side, with the most impactful change at the top. **How to use:** @@ -62,7 +62,8 @@ Writes the entry for [prisma.io/changelog](https://www.prisma.io/changelog) and 2. It lists the merged pull requests across the Prisma repositories with the `gh` CLI, then reads the release notes and the docs for the candidates it keeps. You need access to the Prisma organization for the private repositories, or the entry covers the public ones only. 3. It writes `apps/site/content/changelog/{date}.mdx` and adds screenshots to `apps/site/public/changelog/`. 4. It runs `content-write-changelog/scripts/check-entry.mjs`, which checks the frontmatter, compiles the MDX, and checks every link and anchor against production. -5. It opens a pull request whose body lists everything it held back or excluded, with a reason for each. You review, edit, and merge. +5. It runs the `docs-reader-review` checks and has a fresh reviewer read the entry cold, so that the entry explains each change instead of listing it the way a spec would. +6. It opens a pull request whose body lists everything it held back or excluded, with a reason for each. You review, edit, and merge. The skill reads the same positioning doc as `content-write-blog`, from the **prisma/ignite** repository. Its rules live in four short references: [gathering](content-write-changelog/references/gathering.md), [filtering](content-write-changelog/references/filtering.md), [voice](content-write-changelog/references/voice.md), and [structure](content-write-changelog/references/structure.md). diff --git a/.claude/skills/content-write-changelog/SKILL.md b/.claude/skills/content-write-changelog/SKILL.md index 6d58ee8cd0..fa296cf5b5 100644 --- a/.claude/skills/content-write-changelog/SKILL.md +++ b/.claude/skills/content-write-changelog/SKILL.md @@ -22,14 +22,15 @@ Stop and tell the operator if one of these is missing. ## Rules that always apply -- **Outcome first, product named in the same sentence.** Every unit a skimmer can land on (title, opening paragraph, section opener, bullet, guide annotation) leads with what the reader can now do and names the product. Write "You can now enroll a coding agent in Prisma with a credential of its own", not "Agent enrollment is now available". +- **Outcome first, product named in the same sentence.** The title, the opening paragraph, and the first sentence of each section lead with what the reader can now do and name the product. Write "You can now enroll a coding agent in Prisma with a credential of its own", not "Agent enrollment is now available". +- **Explain, do not state.** A changelog written from pull request titles reads like a spec. Say what each change means for the reader, in sentences a person would say aloud, with the "so" and "because" left in. `references/voice.md` and the `docs-reader-review` skill describe how. - **Publish what the user can see, wherever the code lives.** A Console workflow, a CLI command, a REST API route, or a docs page is publishable even when its code is private. Describe the effect and link a public docs page, or link nothing. Never link a private pull request and never name a private repository. - **Only what is released.** A merged pull request is not a shipped feature. Check each product's release before you write about it, as described in `references/gathering.md`. -- **Every claim has a public source.** A release note, a docs page, a blog post, or a public pull request. Quote dates, limits, and prices from the source and never from memory. +- **Every claim has a source you have read.** Use a public one wherever it exists: a release note, a docs page, a blog post, or a public pull request. A change built in a private repository often has none yet. In that case the merged pull request is your source, the entry describes the effect without a link, and the item goes into the triage note as "no public PR" so that a reviewer confirms it in the product before the entry is merged. Quote dates, limits, and prices from the source and never from memory. - **Full product names on every mention**, as the positioning doc spells them. Check the name and the maturity of each product against the docs when you write, because both change between entries. - **Most impactful first, at every level.** The title is the biggest change. The headline sections, the bullets in a section, the actions a reader must take, and the fixes are each ordered by how many readers they reach and how much they change. - **Never pad.** A window with a few fixes gets a short entry with no headline sections. A window with nothing user-facing gets no entry, and you report that instead. -- **No em dashes, no emoji, no vague adjectives.** State the behavior the reader will observe. +- **No em dashes, no emoji, no vague adjectives, and no vocabulary from the source code.** State the behavior the reader will observe, in words the reader already has. ## Workflow @@ -41,13 +42,15 @@ Stop and tell the operator if one of these is missing. Done when every candidate has a verdict and every flag and exclusion has a one-line reason. 4. **Confirm.** For each candidate you keep, find the public source, check that it is released, and note the docs page that documents it. Done when no kept item rests on a pull request title alone. -5. **Write.** Draft the entry in the order `references/structure.md` gives, in the voice `references/voice.md` describes. +5. **Write.** Read `references/voice.md` and the three `docs-reader-review` references it names, then draft the entry in the order `references/structure.md` gives. Done when the frontmatter is valid, `slug` equals `date`, and the date is the day the entry lands. -6. **Check.** Run `node .claude/skills/content-write-changelog/scripts/check-entry.mjs apps/site/content/changelog/{YYYY-MM-DD}.mdx --links`, then `.claude/skills/docs-reader-review/scripts/check-ai-signs.sh` on the same file. The closing `---` before the Enterprise line is the one hit the second script is expected to report. - Done when both scripts report nothing else and you have gone through the checklist below. -7. **Open the pull request.** Create the branch `changelog/{YYYY-MM-DD}`, commit the entry and its images as `docs(site): add changelog entry {YYYY-MM-DD}`, push, and open the pull request with the body in `references/structure.md`. If an entry for that date already exists, ask the operator whether to replace it or pick another date. +6. **Check.** Run `node .claude/skills/content-write-changelog/scripts/check-entry.mjs apps/site/content/changelog/{YYYY-MM-DD}.mdx --links`. Then run the three scripts in `.claude/skills/docs-reader-review/scripts/` on the same file: `check-plain.sh`, `check-staccato.py`, and `check-ai-signs.sh`. The closing `---` before the Enterprise line is the one hit `check-ai-signs.sh` is expected to report. + Done when the scripts report nothing else. +7. **Have it read cold.** Give the entry to a fresh reviewer, as the `docs-reader-review` skill describes, and rewrite every sentence it could not restate. Repeat with a new reviewer until a round reports no sentence it could not restate and no word it had to guess. Then check every rewritten sentence against its source again, because rewriting drifts meaning. + Done when a round comes back clean, the facts have been checked again, and you have gone through the checklist below. +8. **Open the pull request.** Create the branch `changelog/{YYYY-MM-DD}`, commit the entry and its images as `docs(site): add changelog entry {YYYY-MM-DD}`, push, and open the pull request with the body in `references/structure.md`. If an entry for that date already exists, ask the operator whether to replace it or pick another date. Done when the pull request is open and its body carries the triage note. -8. **Report and stop.** Give the operator the pull request link, the items that need a human decision, and anything you could not verify. Do not merge and do not enable auto-merge. +9. **Report and stop.** Give the operator the pull request link, the items that need a human decision, and anything you could not verify. Do not merge and do not enable auto-merge. ## Checklist before you open the pull request @@ -58,7 +61,9 @@ Stop and tell the operator if one of these is missing. - [ ] "What you need to do" exists whenever a reader has to act, with one bullet per audience. - [ ] Every breaking change says what to change. Every deprecation names the old surface, the replacement, and the date. - [ ] Every documented surface links its docs page, and every link and anchor was checked against production. -- [ ] Every technical identifier is in backticks. +- [ ] Every technical identifier is in backticks, and every term a reader of the previous version would not know is replaced or explained where it first appears. +- [ ] Headline sections and short product sections are paragraphs. Lists are used only for items the reader scans. +- [ ] Bold appears on the first sentence, on interface labels, on dates that need action, and on the first sentence of items in a scanned list, and nowhere else. - [ ] No private pull request links, no private repository names, no internal names, no issue-tracker IDs, no customer names, and no description of how a security problem worked. - [ ] No CI, dependency, refactor, test, SEO, or analytics items. - [ ] Every image has alt text that describes what the screenshot shows. diff --git a/.claude/skills/content-write-changelog/references/gathering.md b/.claude/skills/content-write-changelog/references/gathering.md index 651aa1a613..4ab7fece31 100644 --- a/.claude/skills/content-write-changelog/references/gathering.md +++ b/.claude/skills/content-write-changelog/references/gathering.md @@ -11,7 +11,7 @@ List `apps/site/content/changelog/` and open the newest file. The window starts The repositories that feed the changelog carry the `loggy-core` topic. Ask GitHub for the current list instead of keeping one in your head, because repositories get added and renamed: ```bash -gh api 'search/repositories?q=org:prisma+topic:loggy-core&per_page=50' \ +gh api --paginate 'search/repositories?q=org:prisma+topic:loggy-core&per_page=100' \ --jq '.items[] | "\(.full_name)\t\(.private)"' ``` @@ -19,15 +19,15 @@ The second column tells you which repositories are private. Changes from those a ## 3. List the merged pull requests -For each repository, list what merged in the window and keep the title, the merge date, and the body: +For each repository, list what merged in the window and keep the title, the merge date, and the body. `SINCE` and `UNTIL` are the dates from step 1, written as `YYYY-MM-DD`: ```bash gh pr list -R "$repo" --state merged --limit 1000 \ - --search "merged:2026-08-28..2026-10-02" \ + --search "merged:$SINCE..$UNTIL" \ --json number,title,mergedAt,url,body ``` -Start the range a few days before the last entry's date, then drop what that entry already covers. +Set `SINCE` a few days before the last entry's date, then drop what that entry already covers. If a repository returns exactly 1000 pull requests, the list was cut off, so split the window in two and run the command for each half. A month across every repository is close to a thousand pull requests. Do not read them all. Read the titles first, and set aside the ones that start with `chore`, `ci`, `test`, `refactor`, or `build`, along with dependency bumps and internal project close-outs. Read the body only for the candidates that are left. The body, not the title, tells you what the user sees and whether the change is behind a flag. diff --git a/.claude/skills/content-write-changelog/references/structure.md b/.claude/skills/content-write-changelog/references/structure.md index 27de7f0aea..b09dbe9ec8 100644 --- a/.claude/skills/content-write-changelog/references/structure.md +++ b/.claude/skills/content-write-changelog/references/structure.md @@ -34,25 +34,25 @@ The index page at `/changelog` derives a single category for the entry from its Leave out any block that has no content. Do not repeat the title as a heading, because the page renders it from the frontmatter. -1. **Opening.** The first sentence is the biggest change, in bold, and it has to stand alone because the index page shows only the start of the entry. The other headline changes follow in a sentence each. If the reader must act by a date, a second short paragraph opens with a bold "Two dates need action." and names each date. +1. **Opening.** The first sentence is the biggest change, in bold, and it has to stand alone because the index page shows only the start of the entry. The other headline changes follow in a sentence each. If the reader must act by a date, a second short paragraph names each date in bold and points to "What you need to do". 2. **Up to three headline sections, the most impactful first.** Each is an `##` heading that states the outcome, either as a sentence (`## Coding agents get their own credential and ask before production changes`) or as an action the reader can take (`## Try Prisma ORM 8 on the schema you already have`). A heading that names only the feature, such as "Agent enrollment", tells a skimmer nothing. A change earns a headline section when it changes what a reader can do and can be explained in a few short paragraphs. A fix never does. Inside each section: - - what the reader can do now, then why it matters, then how to start + - paragraphs, not bullets: what the reader does today and what was wrong with it, then what is new, then how to start - one screenshot when the change is visual - one code block when the change is something the reader types - a row of one or two buttons that lead to the product and to the docs Order them by impact: how many readers the change reaches and how much it changes what they can do. Lead with the commercial products only when two changes are of similar weight. -3. **`## What you need to do`.** Include it whenever a reader has to act. One bullet per audience, opening with a bold label that lets readers find themselves: `- **Prisma ORM 7:** there is *nothing to change*.` Each bullet states the action, the deadline, and the guide to follow. Put the nearest deadline and the largest loss first, so a bullet about data that will be deleted comes before one about a connection string. Say so when a group has nothing to do. +3. **`## What you need to do`.** Include it whenever a reader has to act. One bullet per audience, opening with a bold condition that lets readers find themselves: `- **If you are on Prisma ORM 7**, you do not need to change anything.` Each bullet states the action, the deadline in bold, the reason, and the guide to follow. Put the nearest deadline and the largest loss first, so a bullet about data that will be deleted comes before one about a connection string. Say so when a group has nothing to do. -4. **`## Breaking changes`.** One line saying which versions they apply to, then one bullet per change. The bullet opens with what breaks in bold, then says what to write instead, then links the pull request. When a release has more breaking changes than most apps will hit, list the common ones and link the full release notes. +4. **`## Breaking changes`.** One line saying which versions they apply to, then one bullet per change. The bullet opens with what changed in bold, then explains what the reader will see and what to write instead, then links the pull request. When a release has more breaking changes than most apps will hit, list the common ones and link the full release notes. -5. **`## Deprecations`.** One bullet per deprecation, with the product in bold, the old surface, the replacement, and the date. When the reader has to act, add an italic `*Action required:*` sentence. +5. **`## Deprecations`.** One bullet per deprecation, in full sentences: the product, what is deprecated, what replaces it, the date, and whether the old form still works. When the action is already under "What you need to do", link the guide and do not repeat the steps. -6. **One `##` section per product**, headed with the exact product name. The section opens with one bold sentence that states its biggest change. Bullets follow, most impactful first, each opening with the outcome. Group related bullets under a plain sentence instead of adding sub-headings. Order the sections by how much changed for users. +6. **One `##` section per product**, headed with the exact product name. The section opens with its biggest change, explained in a short paragraph. Related smaller changes follow as further paragraphs, or as a list when there are several of the same kind, introduced by a sentence that says what they have in common. Put the most impactful first. Order the sections by how much changed for users. -7. **`## Fixes`.** Always visible and grouped by product under bold labels. Within a product, wrong results and data loss come first, then failures, then cosmetic fixes. Open the fixes that matter most with a bold sentence, and say what went wrong before when that helps a reader recognize the bug. +7. **`## Fixes`.** Always visible and grouped by product under bold labels. Within a product, wrong results and data loss come first, then failures, then cosmetic fixes. Each fix is a full sentence, and it says what went wrong before when that helps a reader recognize the bug. 8. **`## Guides and articles`.** One bullet per new post or guide. Link the published page, and follow it with a colon and one sentence about what the reader gets. diff --git a/.claude/skills/content-write-changelog/references/voice.md b/.claude/skills/content-write-changelog/references/voice.md index f5501103cc..fb354c0624 100644 --- a/.claude/skills/content-write-changelog/references/voice.md +++ b/.claude/skills/content-write-changelog/references/voice.md @@ -1,74 +1,90 @@ -# Voice: short, concrete, and useful to someone skimming +# Voice: a person explaining what changed -The entry is read by a developer who is deciding whether an update affects them. Write so they can decide without reading twice. +The entry is read by a developer who is deciding whether an update affects them. Write the way you would explain the change to a colleague at the next desk. A changelog built from pull request titles reads like a spec: every sentence is true, and the reader still has to work out what it means for them. The job of this pass is to do that work for the reader. -## Outcome first +The house rules for prose are in the `docs-reader-review` skill, and they apply here in full: -The first sentence of every item answers one question: what can I do now that I could not do before? After that, in this order, come why it matters, how it works, and the technical detail. Stop as soon as the reader can act. +- `.claude/skills/docs-reader-review/references/explain-not-state.md` describes the habit of stating facts without explaining them, and how to fix it. +- `.claude/skills/docs-reader-review/references/ai-writing-signs.md` lists the words and sentence shapes that make text read as machine-written. +- `.claude/skills/docs-reader-review/references/banned-terms.md` lists the source-code vocabulary that readers do not have. -- **Write from the reader's side.** Describe what they experience, not how it was built. Second person works well when it is concrete: "You can now delete a project that still has active deployments." -- **One idea per sentence.** When a sentence contains "and", check whether it holds two changes. If it does, split it. Two changes never share a bullet. -- **Mechanics come after the benefit.** Config files, setup pull requests, and API internals do not appear before the reader knows what they get. -- **A snippet follows its explanation.** Code never sits between the outcome and the sentence that explains it. -- **Skip the history.** "Was gated, then tested, then released" becomes what is true today. Add one "before" sentence only when the contrast helps: "The choice was dropped before, and the project deployed in US East." -- **Question every sentence.** If a user would not care, delete it. -- **Prefer two short sentences to one long one.** "It used to map to `userProfile`. It now maps to `UserProfile`." reads faster than one sentence joined with "and". -- **Open a bullet with a verb when the reader can do something new.** "Change what is live", "Find out what went wrong", and "Skip duplicates on bulk inserts" tell a skimmer more than a noun phrase does. +Read all three before you write. The rest of this file is what is specific to the changelog. + +## Explain, do not state + +For each change, say what it means for the reader before you say how it works. Three habits do most of the work. + +**Start from the reader's situation.** Open a headline section with what the reader does today and what was wrong with it, and only then say what is new. "When a coding agent works in Prisma today, it uses your CLI session. As far as Prisma can tell, the agent is you" tells a reader why enrollment matters. "Agents can now be enrolled with a credential and a policy" does not. + +**Walk through it as a scenario.** Put the reader in the sentence: "When a deploy goes wrong, you can ask your AI tool to read the deployment's runtime logs, and then ask it to roll the app back." A list of tool names leaves the reader to imagine that for themselves. + +**Keep the connectives.** When two facts are cause and effect, or condition and result, join them with "so", "because", "when", "if", or "which". "Because the credential belongs to the agent, you can pause or revoke it without affecting your own access" is one thought. Split into two short sentences, it becomes two facts the reader has to connect. A run of clipped sentences is the most common sign of a changelog written from a diff. + +Say what was true before when it helps a reader recognize their own problem: "Your choice was dropped before, and the project deployed in US East." + +## Prose for reasoning, lists for scanning + +Use a paragraph when you are explaining why something matters or how the pieces fit together. Headline sections, and product sections with a handful of related changes, are paragraphs. + +Use a list when the items are the same kind of thing and the reader will scan for the one that applies to them: the audiences under "What you need to do", breaking changes, a product's new features, fixes, and guides. In a list, each item is still one or more full sentences that explain the change. + +Introduce a list by what its items have in common ("Connecting a repository takes fewer steps as well:"), never by how many there are ("Three smaller changes:"). + +## Words the reader does not have + +A reader on the previous version does not know the new version's vocabulary. Replace a term from the source code or the release notes with what it means, or define it in the sentence that first uses it. + +- "a migration snapshot" becomes "the copy of your schema that a migration stores" +- "buffered queries" becomes "a query whose results you have not finished reading" +- "a Composer project's topology" becomes "how the project's services connect to each other" +- "the upgrade recipe" becomes "the upgrade guide linked from each release" + +Identifiers in backticks are not prose and stay as they are. Run `check-plain.sh` from `docs-reader-review` to catch the terms on the banned list. ## Name the product -The product name appears wherever a reader might land: the title, the opening paragraph, section openers, and the first bullet of a list. A reader who jumps to the middle of the entry should know which product a line is about. +The product name appears wherever a reader might land: the title, the opening paragraph, and the first sentence of each section. A reader who jumps to the middle of the entry should know which product a paragraph is about. -Use the names in the positioning doc, in full, every time. Check two things against the docs before you write, because they change between entries: +Use the names in the positioning doc, in full. Check two things against the docs before you write, because they change between entries: -- **The name.** Follow what the docs call the product today. For example, the docs say "Prisma ORM" and add a version number only when two versions are being contrasted, as in "Prisma ORM 8 reads the Prisma 7 schema you already have". +- **The name.** Follow what the docs call the product today. For example, the docs say "Prisma ORM" and add a version number only when two versions are being contrasted, as in "Prisma ORM 8 can now read your Prisma 7 `schema.prisma` file". - **The maturity.** Early Access, release candidate, and generally available mean different things to a reader. State a product's maturity once per entry, in the docs' wording, and do not repeat it in headings. -## Titles +## Titles and headings The title tells someone scanning the changelog index whether to open the entry. - Lead with what the reader can do, and name the product: "Let your coding agent set up Prisma and ask before it touches production". -- Say what the reader gets, not what the feature is made of. "Enroll your coding agent in Prisma with its own credential" names the mechanism. The version above names the two things the reader cares about. +- Say what the reader gets, and leave out what the feature is made of. "Enroll your coding agent in Prisma with its own credential" names the mechanism. The version above names the two things the reader cares about. - A launch is the exception, where the event is the news: "Prisma Compute is now generally available". The index page gives the featured treatment to titles that say "generally available", "now available", or "now in beta" or "preview", so use those phrases only for a real launch. -- One claim per title. When an entry covers several products, lead with the biggest change and let the opening paragraph carry the rest. +- One claim per title. When an entry covers several products, lead with the biggest change and let the opening carry the rest. - No `Prisma:` prefix, no version number, and no verbs like "lands", "arrives", or "ships". -## Words to cut - -- **Openers that delay the point:** "We're excited to", "Today we're announcing", "As always". -- **Filler:** "stay tuned", "under the hood", "and much more". -- **Intensifiers:** "very", "really", "truly", "simply". -- **Jargon:** "leverage", "robust", "best-in-class", "supercharge". -- **Claims of importance** with no change behind them: "This is huge", "A better experience". +Headline section headings follow the same rules: an outcome or an action the reader can take, in sentence case. -These adjectives need proof in the same sentence, or they go: `seamless`, `effortless`, `powerful`, `fast`, `simple`, `easier`, `cleaner`, `richer`, `clearer`. Replace them with what the reader will observe. Not "builds are easier to debug" but "a failed build sends an email and shows its logs in the Console". +## Emphasis -The full list of patterns that make text read as machine-written is in `.claude/skills/docs-reader-review/references/ai-writing-signs.md`, and `check-ai-signs.sh` in the same skill finds the ones a regular expression can catch. +Bold is for three things: the first sentence of the entry, interface labels the reader will click (**Save changes**), and dates the reader must not miss. In a list the reader scans, the first sentence of an item can be bold when it names the change. Do not bold a phrase in every paragraph, and do not use italics to stress a word. If the sentence needs stress to be understood, rewrite the sentence. ## Sentence rules -- No em dashes. Use a comma, a period, or parentheses. -- No emoji. -- No rhetorical questions and no "It's not X, it's Y". -- Active voice and present tense: "Views now support `@unique`." - Use a number only when it appears in the source. Never estimate one. -- Put every exact identifier in backticks: packages, import paths, config files, API fields, routes, commands, and error codes. Product surfaces such as the Console and the REST API stay plain text. -- A link says what the reader gets there: "The [enrollment guide](url) covers the policy rules." Never "Read more". - -## Emphasis - -Bold the one phrase in a paragraph that a skimmer must not miss, and bold the lead of a bullet when the bullet opens with the outcome. Use italics for the short qualifier that changes a decision, such as *never returned* or *nothing to change*. If everything is bold, nothing is. +- Put every exact identifier in backticks: packages, import paths, config files, API fields, routes, commands, and error codes. Product areas such as the Console and the REST API stay plain text. +- A link says what the reader gets there: "The [enrollment guide](url) explains the policy in detail." Never "Read more". +- Use the count of items there actually are. Lists of exactly three adjectives or three examples, again and again, read as generated. +- Avoid framing a change as a contrast the reader was not thinking about ("not just X, but Y"). Say what it does. ## From pull request to sentence -Strip the mechanism and keep the effect. +Take the mechanism out, keep the effect, and add what it means for the reader. -- "Use every key column in includes, nested writes and multi-table variants" becomes "`include()` across a composite foreign key returns the right rows. It matched on the first key column only, which returned related rows that belonged to other parents." -- "Schedule paid-to-paid downgrades for the end of the period" becomes "Downgrades between paid plans take effect at the end of the billing period. You keep your current plan until then." +- "Use every key column in includes, nested writes and multi-table variants" becomes "Loading related records with `include()` across a composite foreign key now returns the right rows. It used to match on the first key column only, so it could return rows that belonged to other parents." +- "Schedule paid-to-paid downgrades for the end of the period" becomes "When you downgrade from one paid plan to another, the change now takes effect at the end of the billing period, and you keep your current plan until then." +- "Accept and return logicalId on services, databases, and buckets" becomes "Services, databases, and buckets now have an optional `logicalId`, an identifier you choose so that your own tooling can tell which resource is which." - "Reduce included-result decoding overhead" has no effect a reader can observe without a number from the source, so it is excluded. -- A title that carries only an issue-tracker ID and an internal project name is excluded. -## Read it once more as the reader +## The reader review + +The checker scripts find words and sentence shapes. They cannot tell whether a paragraph explains anything. Before the entry goes into a pull request, give it to a fresh reviewer who has not seen the sources, using `.claude/skills/docs-reader-review/references/reader-persona.md` as its instructions. Describe the reader as someone who has used the previous version of the product for two years and is skimming to learn what changed and whether they have to act. -Before you hand the entry over, read only the title, the opening paragraph, and the first sentence of each section. A reader who stops there should know what changed, which products it touches, and whether they have to do anything. +Rewrite every sentence the reviewer could not restate, then check the rewritten sentences against the sources again. A rewrite that reads well and says something the source does not is worse than the sentence it replaced. diff --git a/.claude/skills/content-write-changelog/scripts/check-entry.mjs b/.claude/skills/content-write-changelog/scripts/check-entry.mjs index 1439ee92a6..5368bbcf5a 100644 --- a/.claude/skills/content-write-changelog/scripts/check-entry.mjs +++ b/.claude/skills/content-write-changelog/scripts/check-entry.mjs @@ -4,7 +4,8 @@ // // Always checked: required frontmatter, slug and version equal to date, the file name, em dashes // and emoji, private pull request links, images on disk with alt text, and that the MDX compiles. -// With --links: every external link returns 200, and every #anchor exists on the page it points to. +// With --links: every link returns 200, pull request links included, and every #anchor exists on +// the page it points to. // --modules names a directory whose node_modules holds @mdx-js/mdx, for a checkout or worktree // that has no install of its own. Exit 1 on any finding. import { existsSync, readdirSync, readFileSync } from "node:fs"; @@ -42,7 +43,9 @@ for (const name of ["title", "date", "version", "slug", "headline", "canonical", if (!/^tags:\s*\n(\s+-\s+.+\n?)+/m.test(frontmatter)) fail("frontmatter: tags are missing"); const date = field("date"); if (date) { - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) fail(`frontmatter: date "${date}" is not YYYY-MM-DD`); + const parsed = new Date(`${date}T00:00:00Z`); + const real = /^\d{4}-\d{2}-\d{2}$/.test(date) && !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === date; + if (!real) fail(`frontmatter: date "${date}" is not a real YYYY-MM-DD date`); if (field("slug") !== date) fail("frontmatter: slug must equal date"); if (field("version") !== date) fail("frontmatter: version must equal date, not a product version"); if (basename(path) !== `${date}.mdx`) fail(`file name must be ${date}.mdx`); @@ -108,11 +111,18 @@ async function status(url, method = "GET") { } if (checkLinks) { + const privateFound = new Set(); for (const repo of privateRepos) { const response = await status(`https://github.com/${repo}`); - if (!response || response.status !== 200) fail(`link: ${repo} is not a public repository, remove its pull request links`); + if (!response || response.status !== 200) { + privateFound.add(repo); + fail(`link: ${repo} is not a public repository, remove its pull request links`); + } } - const unique = [...new Set(links)].filter((link) => !/github\.com\/[^/]+\/[^/]+\/pull\//.test(link)); + const unique = [...new Set(links)].filter((link) => { + const repo = link.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\//)?.[1]; + return !repo || !privateFound.has(repo); + }); for (const link of unique) { const url = link.startsWith("/") ? `https://www.prisma.io${link}` : link; const [page, anchor] = url.split("#"); From 6148d41556836b08f12395c508288b8f2e6f89dd Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:26:51 +0200 Subject: [PATCH 3/3] skills: check changelog links in batches, with one retry A slow or rate-limited response is not a broken link, and forty pull request links checked one at a time took minutes. Co-Authored-By: Claude Fable 5.1 --- .../scripts/check-entry.mjs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.claude/skills/content-write-changelog/scripts/check-entry.mjs b/.claude/skills/content-write-changelog/scripts/check-entry.mjs index 5368bbcf5a..ed908bb2e8 100644 --- a/.claude/skills/content-write-changelog/scripts/check-entry.mjs +++ b/.claude/skills/content-write-changelog/scripts/check-entry.mjs @@ -101,13 +101,22 @@ if (!mdxPath) { } } -async function status(url, method = "GET") { - try { - const response = await fetch(url, { method, redirect: "follow", signal: AbortSignal.timeout(20000) }); - return response; - } catch { - return null; +// One retry, because a slow or rate-limited response is not a broken link. +async function status(url) { + for (const attempt of [1, 2]) { + try { + const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(20000) }); + if (attempt === 2 || (response.status !== 429 && response.status < 500)) return response; + } catch { + if (attempt === 2) return null; + } + await new Promise((done) => setTimeout(done, 3000)); } + return null; +} + +async function inBatches(items, size, check) { + for (let i = 0; i < items.length; i += size) await Promise.all(items.slice(i, i + size).map(check)); } if (checkLinks) { @@ -123,19 +132,19 @@ if (checkLinks) { const repo = link.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\//)?.[1]; return !repo || !privateFound.has(repo); }); - for (const link of unique) { + await inBatches(unique, 6, async (link) => { const url = link.startsWith("/") ? `https://www.prisma.io${link}` : link; const [page, anchor] = url.split("#"); const response = await status(page); if (!response || response.status !== 200) { fail(`link: ${url} returned ${response ? response.status : "no response"}`); - continue; + return; } if (anchor && !/^log\d{4}/.test(anchor)) { const html = await response.text(); if (!html.includes(`id="${anchor}"`)) fail(`link: ${url} has no element with id "${anchor}"`); } - } + }); } if (findings.length) {