Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .claude/skills/records/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
name: records
description: Commit records and review pages for the ReScript compiler repository. Use when committing a change, when an agent reports its work, and when someone asks to review a commit, a pull request or a branch. Every commit message is a structured record that answers the questions ReScript reviewers ask; review pages are rendered from records with scripts/commitRecords.js, never written by hand.
---

# Commit records and review pages

A **record** is a commit message whose body has named prose sections and
whose last block is git trailers. Its fields are the questions reviewers of
this repository ask on pull requests, answered before they are asked. A
review page is rendered from records: one card per commit, and for a range a
summary, a changelog draft and the files more than one commit changes.

## The record

```
<subject: present tense, what the compiler or tool now does>

<one paragraph: the change at the level of the language or the tool>

Example (rescript):
<the ReScript source that shows the change, at most 15 lines>

Before (js):
<generated JavaScript before, for that source>

After (js):
<generated JavaScript after>

Source: <how the Before/After JavaScript was produced: compiler and revision,
or the snapshot file in the diff>

Breaking: <what existing code or tooling stops working, and the migration>

Changelog entry: <the exact bullet of the changes/<name>.<category>.md fragment>

Review: <a question a reviewer raised or will raise, and its answer; repeat>

Area: syntax printer frontend typechecker lambda codegen runtime rewatch
analysis reanalyze gentype tools playground scripts ci docs tests
Changelog: breaking | feature | fix | docs | polish | internal | none
Output: unchanged | changed <which programs>, <evidence>
Formatting: unchanged | changed <which source shapes>
Formats: none | <bumped magic numbers: ast cmi cmt cmj>
PPX: unchanged | <effect on the frozen v0 PPX wire>
Editor: unchanged | <effect on completion, hover, hints; fixtures>
Errors: <ERROR_VARIANTS.md rows added, removed or relinked>
Tests: +added/path ~updated/path -removed/path | none, because <reason>
Measured: <quantity before -> after, and what produced it>; ...
PR: #<number>
Signed-off-by: <Name> <email>
```

- **Prose sections** are a line `Name:` or `Name (lang):` followed by text or
a block indented by 4 spaces. `lang` is `rescript`, `js`, `ts`, `sh`,
`rust`, `ocaml` or `text`; the renderer highlights ReScript and JavaScript.
`Before` and `After` sections of one language render side by side with
changed lines marked. Use `Before (rescript)` / `After (rescript)` for a
formatter change and `Diagnostic (text)` for compiler output. Any other
name is allowed and renders as prose or code.
- **The final block is git trailers**: `Key: value` lines, continuation lines
indented by one space, nothing after them. `git interpret-trailers --parse`
must read every key. `Signed-off-by` (DCO) is required on every commit.
- **Required keys**: `Area`, `Changelog`, `Output`, `Tests`. `Formatting`, `Formats`,
`PPX` and `Editor` are required whenever the change touches the syntax
library, the parsetree, the typed tree or Lambda. Omit the others when they
do not apply.
- **Wording** follows the writing rules: present facts, positive statements,
claims as strong as their evidence, every number labelled by what produced
it. A JavaScript `Before`/`After` pair says where it came from in a
`Source:` paragraph (compiler run, or a snapshot file in the diff).
- **Lists in trailers**: `Measured` separates items with `; `
(outside parentheses); each item renders on its own line. `Output` and the
other state trailers start with their state word (`unchanged`, `changed`,
`none`), followed by the detail.

## What each field states

| Field | What the record states |
| --- | --- |
| `Area` | The layers the change touches. They decide who reviews it and which suites apply; a parsetree or typed-tree change also reaches editor analysis. |
| `Changelog` + `Changelog entry` | Whether the change is user-facing, and its category: `breaking` when existing code or tooling must adapt, `internal` when nothing user-visible changes. The entry is one short line ending in the PR link, identical to the fragment's bullet. |
| `Output` + `Before`/`After (js)` | The generated JavaScript for the example before and after, and which existing programs compile differently. `unchanged` holds when no committed generated JS changes, or when a rebuild of the test suites is byte-identical. |
| `Formatting` + `Before`/`After (rescript)` | Whether `rescript format` rewrites existing code, and whether formatted output parses back to the same tree with its comments. |
| `Formats` | The magic numbers bumped in `compiler/ext/config.ml`. Any change to the marshaled parsetree, typed tree (`.cmi`, `.cmt`) or `.cmj` layout, constructor order included, bumps the matching number. |
| `PPX` | The effect on the frozen v0 PPX wire: `parsetree0.ml` is unchanged, and a change to the v0 mapping comes with an ast-mapping fixture. |
| `Editor` | Completion, hover and inlay hints on new or changed nodes, and the analysis fixtures that cover them. |
| `Errors` | The `tests/ERROR_VARIANTS.md` rows added, removed or relinked for changed error variants or fixtures. |
| `Tests` | The tests added, changed or removed, each observing the effect the record claims, so that it fails without the change. `none, because …` gives the reason when no test changes. |
| `Measured` | Performance before and after, with the input and the command that produced each number. |
| `Breaking` | What existing code or tooling stops working, and the migration. |
| `Review` | Questions a reviewer raised or will raise, with their answers. |
| `PR` | The pull request; the renderer links it. |
| `Signed-off-by` | The DCO sign-off, with the commit author's name and email, as the last trailer. |

## Rendering

```sh
node scripts/commitRecords.js --out review.html <rev>...
node scripts/commitRecords.js --out review.html <base>..<branch>...
node scripts/commitRecords.js --fragment --out part.html <rev>...
node scripts/commitRecords.js --out review.html --pr 1234 --title "<PR title>" <base>..<branch>
```

- `--pr` and `--title` head the page with the pull request's number (linked)
and title; without them the page is headed "Semantic Diff".

- A revision renders one card. A range renders a summary row, a changelog
draft grouped by category, its commits as cards, and the files more than
one commit (or more than one range) changes.
- Each card checks the record against its diff: a changelog category with no
new `changes/` fragment (or a `Changelog entry` that differs from the
fragment's bullet), modified generated `.mjs`, `.cjs` and `.res.js` files under
`Output: unchanged` (new ones from new tests are counted, not flagged), a
magic number bumped in `compiler/ext/config.ml` that
`Formats` does not name, a parsetree change with no ast-mapping fixture, a
new `super_errors` fixture with no `ERROR_VARIANTS.md` change, a missing
`Tests` trailer, `Tests: none` on a diff that changes tests. A failed
check is shown on the card. The DCO app checks `Signed-off-by`.
- The page is one self-contained HTML file: no script, no external data
(fonts fall back to system faces), light and dark themes, phone width.
`--fragment` omits the document skeleton for embedding.
- A note on `refs/notes/records` replaces the commit message's record. Use it
for commits that exist already (history, a squash merge) and for review
outcomes added later; a new commit carries its record in its message.

## When to use

- Every commit an agent makes carries a record in its message.
- An agent's report lists its commits and the rendered page; the records are
the description of the work. A pull request description is a rendering of
its range.
- Before review or merge: render `master..<branch>` and hand over the page.
- On GitHub, `.github/workflows/semantic_diff.yml` renders each pull
request's range when it is opened or pushed, and comments the page's link
(`Semantic diff: https://cdn.rescript-lang.org/semantic-diff/pr-<N>/…`).
108 changes: 108 additions & 0 deletions .github/workflows/semantic_diff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Semantic Diff

# Renders the commit records of each pull request (scripts/commitRecords.js)
# as a review page on the CDN and links it from the pull request.
#
# A pull request from a branch of this repository runs on pull_request, with
# its own workflow and renderer. A pull request from a fork runs on
# pull_request_target, which GitHub takes from the default branch and runs
# with the repository's secrets: it executes only the default branch's code,
# and the fork's commits are fetched as git data and never checked out.

on:
pull_request:
types: [opened, synchronize, reopened, closed]
pull_request_target:
types: [opened, synchronize, reopened, closed]

concurrency:
group: semantic-diff-${{ github.event_name }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
render:
if: >-
github.event.action != 'closed' &&
github.actor != 'dependabot[bot]' &&
(github.event_name == 'pull_request') ==
(github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: write
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
steps:
- name: Checkout renderer
uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.event.pull_request.base.sha }}
fetch-depth: 0
filter: blob:none
persist-credentials: false

- name: Render semantic diff
id: render
run: |
git fetch --no-tags --filter=blob:none origin "refs/pull/${PR_NUMBER}/head:refs/heads/pr-${PR_NUMBER}"
base=$(git merge-base "$BASE_SHA" "pr-${PR_NUMBER}")
mkdir -p semantic-diff
node scripts/commitRecords.js --out semantic-diff/index.html --pr "$PR_NUMBER" --title "$PR_TITLE" "${base}..pr-${PR_NUMBER}"
echo "head=$(git rev-parse --short=9 "pr-${PR_NUMBER}")" >> "$GITHUB_OUTPUT"

- name: Setup Rclone
uses: cometkim/rclone-actions/setup-rclone@main

- name: Configure Rclone remote
uses: cometkim/rclone-actions/configure-remote/s3-provider@main
with:
name: rescript
provider: Cloudflare
endpoint: https://${{ vars.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
access-key-id: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
acl: private

- name: Upload semantic diff
run: rclone copyto --s3-no-check-bucket semantic-diff/index.html "rescript:cdn-assets/semantic-diff/pr-${PR_NUMBER}/index.html"

# The query string names the rendered commit, so each push gets a URL
# that no cache has seen.
- name: Comment semantic diff URL
uses: thollander/actions-comment-pull-request@v3
with:
pr-number: ${{ github.event.pull_request.number }}
comment-tag: semantic-diff
# Each push replaces the comment, so the newest link is the last
# comment on the pull request.
mode: recreate
message: |
Semantic diff: https://cdn.rescript-lang.org/semantic-diff/pr-${{ github.event.pull_request.number }}/index.html?v=${{ steps.render.outputs.head }}

cleanup:
if: >-
github.event.action == 'closed' &&
github.actor != 'dependabot[bot]' &&
(github.event_name == 'pull_request') ==
(github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-24.04
permissions: {}
steps:
- name: Setup Rclone
uses: cometkim/rclone-actions/setup-rclone@main

- name: Configure Rclone remote
uses: cometkim/rclone-actions/configure-remote/s3-provider@main
with:
name: rescript
provider: Cloudflare
endpoint: https://${{ vars.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
access-key-id: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
acl: private

- name: Delete semantic diff
continue-on-error: true
run: rclone purge --s3-no-check-bucket "rescript:cdn-assets/semantic-diff/pr-${{ github.event.pull_request.number }}"
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ relevant guidance from each.
need to.
- Include appropriate tests with all changes
- Build must pass before committing
- Write each commit message as a record in the format of
[`.claude/skills/records/SKILL.md`](.claude/skills/records/SKILL.md), and
render review pages from records with `node scripts/commitRecords.js`

### Stacked pull requests

Expand Down
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ format: | $(YARN_INSTALL_STAMP)
checkformat: | $(YARN_INSTALL_STAMP)
./scripts/format_check.sh

# Semantic diff: renders the commit records of SEMANTIC_DIFF_BASE..HEAD
# (scripts/commitRecords.js) to _build/semantic-diff.html and opens it

SEMANTIC_DIFF_BASE ?= master

semantic-diff:
mkdir -p _build
node scripts/commitRecords.js --out _build/semantic-diff.html --title "$$(git branch --show-current)" $(SEMANTIC_DIFF_BASE)..HEAD
@if command -v open >/dev/null; then open _build/semantic-diff.html; \
elif command -v xdg-open >/dev/null; then xdg-open _build/semantic-diff.html; fi

# Coverage (bisect_ppx)
#
# Requires the pending `bisect_ppx` OCaml 5.5 compatibility patch in your switch:
Expand Down Expand Up @@ -371,4 +382,4 @@ clean: clean-lib clean-compiler clean-rewatch clean-coverage

.DEFAULT_GOAL := build

.PHONY: yarn-install build rewatch compiler lib artifacts bench test test-analysis test-reanalyze benchmark-reanalyze test-tools test-syntax test-syntax-roundtrip test-gentype test-rewatch test-all playground playground-compiler playground-test playground-cmijs playground-release format checkformat clean-rewatch clean-compiler clean-lib clean-gentype clean-tests clean
.PHONY: yarn-install build rewatch compiler lib artifacts bench test test-analysis test-reanalyze benchmark-reanalyze test-tools test-syntax test-syntax-roundtrip test-gentype test-rewatch test-all playground playground-compiler playground-test playground-cmijs playground-release format checkformat semantic-diff clean-rewatch clean-compiler clean-lib clean-gentype clean-tests clean
1 change: 1 addition & 0 deletions changes/commit-records.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add commit records, a commit message format that answers recurring review questions, and a semantic diff page rendered from them and linked on each pull request. https://github.com/rescript-lang/rescript/pull/8870
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"changelog:preview": "node scripts/changelog.js preview",
"changelog:release": "node scripts/changelog.js release",
"changelog:open": "node scripts/changelog.js open",
"test-changelog": "node --test scripts/changelog.test.js"
"test-changelog": "node --test scripts/changelog.test.js",
"test-commit-records": "node --test scripts/commitRecords.test.js"
},
"files": [
"CHANGELOG.md",
Expand Down
Loading
Loading