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
7 changes: 7 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/src/ @SandroMaglione
/scripts/ @SandroMaglione
/.github/ @SandroMaglione
/package.json @SandroMaglione
/pnpm-lock.yaml @SandroMaglione
/tsconfig.build.json @SandroMaglione
/.changeset/config.json @SandroMaglione
25 changes: 25 additions & 0 deletions .github/ISSUE_TEMPLATE/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: Change proposal
about: Discuss a problem or API change before implementation
title: ""
labels: ""
assignees: ""
---

## Problem or use case

What are you trying to do, and what is difficult or impossible with the current API?

## Proposed direction

Describe the behavior or public API you would like to add or change.

## Example

Show a small example of how the proposed API would be used.

## Additional context

Include alternatives, constraints, or related issues.

Please wait for the proposal to be discussed before opening a pull request.
19 changes: 19 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Summary

-

## Changeset

- [ ] Added or updated for a library or package-metadata change
- [ ] Not required because this PR does not change `src/` or `package.json`

## Validation

- [ ] `pnpm check`
- [ ] Relevant example checks, when examples changed
- [ ] Reviewed the automated type-performance report, when the public TypeScript API or inference changed

<!--
The type-performance workflow posts and updates the base-versus-PR comparison automatically.
Do not copy a manually measured table into this description.
-->
65 changes: 44 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
push:
branches: [main]

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

Expand All @@ -21,46 +25,65 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm check

pokemon-example:
changeset-policy:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
defaults:
run:
working-directory: examples/pokemon
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
with:
package_json_file: examples/pokemon/package.json
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
examples/pokemon/pnpm-lock.yaml
- run: pnpm --dir ../.. install --frozen-lockfile
- run: pnpm --dir ../.. build
- run: pnpm install --frozen-lockfile
- run: pnpm check
fetch-depth: 0
- run: >-
node scripts/check-changeset.mjs
--base ${{ github.event.pull_request.base.sha }}
--head ${{ github.event.pull_request.head.sha }}

discover-examples:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.examples.outputs.matrix }}
steps:
- uses: actions/checkout@v7
- id: examples
run: echo "matrix=$(node scripts/list-examples.mjs)" >> "$GITHUB_OUTPUT"

platformer-example:
example:
needs: discover-examples
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.discover-examples.outputs.matrix) }}
name: example (${{ matrix.example }})
runs-on: ubuntu-latest
defaults:
run:
working-directory: examples/platformer
working-directory: ${{ matrix.directory }}
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
with:
package_json_file: examples/platformer/package.json
package_json_file: ${{ matrix.directory }}/package.json
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
examples/platformer/pnpm-lock.yaml
${{ matrix.directory }}/pnpm-lock.yaml
- run: pnpm --dir ../.. install --frozen-lockfile
- run: pnpm --dir ../.. build
- run: pnpm install --frozen-lockfile
- run: pnpm check

examples:
if: always()
needs: [discover-examples, example]
name: examples
runs-on: ubuntu-latest
steps:
- name: Require every example check to pass
env:
DISCOVERY_RESULT: ${{ needs.discover-examples.result }}
EXAMPLE_RESULT: ${{ needs.example.result }}
run: |
test "$DISCOVERY_RESULT" = "success"
test "$EXAMPLE_RESULT" = "success"
102 changes: 102 additions & 0 deletions .github/workflows/type-performance-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Type performance comment

on:
workflow_run:
workflows: [Type performance]
types: [completed]

concurrency:
group: type-performance-comment-${{ github.event.workflow_run.head_repository.id }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true

permissions:
actions: read
contents: read
issues: write
pull-requests: read

jobs:
comment:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Check out trusted reporting code
uses: actions/checkout@v7

- name: Download performance report
uses: actions/download-artifact@v8
with:
name: type-performance-report
path: report
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Render report from validated benchmark data
run: >-
node scripts/compare-type-performance.mjs
report/before.json
report/after.json
> "$RUNNER_TEMP/report.md"

- name: Create or update pull request comment
uses: actions/github-script@v9
env:
REPORT_PATH: ${{ runner.temp }}/report.md
with:
script: |
const fs = require("node:fs")
const marker = "<!-- effect-machine-type-performance -->"
const report = fs.readFileSync(process.env.REPORT_PATH, "utf8")

if (Buffer.byteLength(report, "utf8") > 60_000) {
core.setFailed("Type-performance report exceeds the safe comment size")
return
}

const run = context.payload.workflow_run
let pull = run.pull_requests?.[0]

if (pull === undefined) {
try {
const associated = await github.rest.repos.listPullRequestsAssociatedWithCommit({
...context.repo,
commit_sha: run.head_sha
})
pull = associated.data.find((candidate) =>
candidate.base.repo.full_name === `${context.repo.owner}/${context.repo.repo}`
)
} catch (error) {
core.warning(`Unable to look up a pull request for ${run.head_sha}: ${error.message}`)
}
}

if (pull === undefined) {
core.notice("No pull request is associated with this workflow run")
return
}

const body = `${marker}\n${report}`
const comments = await github.paginate(github.rest.issues.listComments, {
...context.repo,
issue_number: pull.number,
per_page: 100
})
const previous = comments.find((comment) =>
comment.user?.type === "Bot" && comment.body?.startsWith(marker)
)

if (previous === undefined) {
await github.rest.issues.createComment({
...context.repo,
issue_number: pull.number,
body
})
} else {
await github.rest.issues.updateComment({
...context.repo,
comment_id: previous.id,
body
})
}
68 changes: 68 additions & 0 deletions .github/workflows/type-performance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Type performance

on:
pull_request:

concurrency:
group: type-performance-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
type-performance:
name: type-performance
runs-on: ubuntu-latest
steps:
- name: Check out base
uses: actions/checkout@v7
with:
path: base
ref: ${{ github.event.pull_request.base.sha }}

- name: Check out pull request
uses: actions/checkout@v7
with:
path: head
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}

- uses: pnpm/action-setup@v6
with:
package_json_file: head/package.json

- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
base/pnpm-lock.yaml
head/pnpm-lock.yaml

- name: Install and build base
run: |
pnpm --dir base install --frozen-lockfile
pnpm --dir base build

- name: Install and build pull request
run: |
pnpm --dir head install --frozen-lockfile
pnpm --dir head build

- name: Measure base and pull request
run: |
node head/scripts/type-performance.mjs --root base --json --allow-missing > "$RUNNER_TEMP/before.json"
node head/scripts/type-performance.mjs --root head --json > "$RUNNER_TEMP/after.json"
node head/scripts/compare-type-performance.mjs "$RUNNER_TEMP/before.json" "$RUNNER_TEMP/after.json" > "$RUNNER_TEMP/report.md"
cat "$RUNNER_TEMP/report.md" >> "$GITHUB_STEP_SUMMARY"

- name: Upload report for the comment workflow
uses: actions/upload-artifact@v7
with:
name: type-performance-report
path: |
${{ runner.temp }}/before.json
${{ runner.temp }}/after.json
if-no-files-found: error
retention-days: 7
24 changes: 16 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,29 @@ The goal is eventually to merge this inside the core of the `effect` library, so

Make architectural decisions for the long term. Do not accept a stopgap that only works for now and is meant to be replaced later.

## Feature verification workflow
## Verification

Before implementing a feature, run:
Run:

```sh
pnpm perf:types
pnpm check
```

Record the type-performance results as the baseline for the feature.

After implementing the feature, run:
For changes that can affect the public TypeScript API or its inference, also run:

```sh
pnpm typecheck
pnpm perf:types
```

Compare the final type-performance results with the baseline. When reporting the completed work, include the before and after results and call out the additional type-instantiation cost of the feature, including regressions or improvements.
When an example changes, run its own check from the example directory:

```sh
pnpm check
```

Every package directly below `examples/` must have a `check` script and a committed lockfile.

## Pull request conventions

- Add or update a changeset for changes under `src/` or changes to `package.json`.
- Fill in the pull request template, including the validation performed and the changeset decision.
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Contributing

This project generally does not accept unsolicited pull requests. Open an issue first describing the problem or use case and, for API changes, the public API you want to add or change.

Wait for the proposal to be discussed and accepted before starting an implementation or opening a pull request. Pull requests without prior agreement may be closed.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,9 @@ Individual commands are available for `build`, `test`, `test:types`,
packs the package, imports all public entrypoints, and compiles a strict
TypeScript consumer with `skipLibCheck: false`.

Read [CONTRIBUTING.md](./CONTRIBUTING.md) before proposing a change. Pull
requests receive an automated base-versus-head type-instantiation report.

## Examples

The [platformer statechart example](./examples/platformer) is a playable SVG
Expand All @@ -510,6 +513,11 @@ invoked child statecharts, typed emissions, and Atom reactivity. It uses a local
`file:` dependency on this package while retaining an isolated dependency graph,
lockfile, build, and CI job.

The [playground](./examples/playground) collects focused interactive examples
for traffic lights, turnstiles, media players, microwaves, and worker-backed
machines. CI discovers every direct package under `examples/` and runs its
`check` script automatically.

## Releases

Add a changeset with `pnpm changeset`. CI validates frozen installation and the
Expand Down
Loading