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
2 changes: 1 addition & 1 deletion .changeset/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ Add a changeset for every meaningful change:
pnpm changeset
```

During pre-alpha, pick **patch** that keeps the package on the `0.0.x` ladder until its first
During pre-alpha, pick **patch**: that keeps the package on the `0.0.x` ladder until its first
alpha. See the cosyte version ladder in the meta-repo's `documentation/conventions.md`.
28 changes: 28 additions & 0 deletions .changeset/thick-jokes-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@cosyte/transform": patch
---

No runtime impact: punctuation only, plus a new repository-internal check that keeps it that way.

The Cosyte brand voice does not use the em dash (U+2014). This package carried 659 of them across
75 of its 98 tracked files, including the README, every documentation page that publishes to the
documentation site, and the source doc comments that compile into the shipped type declarations and
render in an editor on hover. All 609 that were in scope are rewritten with a period, a colon, a
comma or parentheses, chosen by what each sentence meant rather than by one blanket substitution.

No exported name, type, issue code, fatal code or documented behaviour changed. Two strings changed
punctuation and nothing else, and both belong to developer tooling rather than to the published
package: the PHI scanner's clean-run line, and one diagnostic from the check that verifies this
repository's own contributor instructions.

Two files still carry the character and each is an exemption with a written reason. The changelog's
dated archive below its "Released before this file was generated" heading is a frozen record whose
entries are byte identical to the tarballs they shipped in, and rewriting it would destroy the
evidence a changelog exists to hold. The vendored third-party tarball under `vendor/` holds the
character's bytes by coincidence inside a compressed stream, which no edit can remove.

The check that enforces the rule lands in the same change as the sweep, on purpose: a check arriving
before its sweep turns the build red on arrival, and a sweep arriving before its check lets the
character grow back. It reads bytes directly rather than shelling out, refuses to report a clean
result whenever it cannot prove it read its subject, and holds its own source to the same rule by
assembling every banned spelling at runtime instead of writing one down.
12 changes: 12 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# The vendored `@cosyte/hl7` and `@cosyte/fhir` tarballs are third-party archives that this
# repository consumes for dev and test (ADR 0001; refresh them with `pnpm vendor:refresh`).
# Declaring them keeps git from ever treating them as text, and it is the declaration that
# `scripts/check-no-emdash.mjs` partitions on: a DEFLATE stream can hold the em dash's UTF-8
# bytes by coincidence, `vendor/cosyte-hl7-0.0.0.tgz` does, and there is no edit that removes
# a byte from someone else's compressed stream.
#
# THIS FILE IS NOT A SILENCER. The em-dash gate REFUSES any `binary` declaration outside
# `vendor/`, so widening the exclusion means editing that script deliberately rather than
# adding a line here. Tracked FILENAMES are scanned whatever this file says, because a
# declaration about a file's bytes says nothing about its name.
vendor/*.tgz binary
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
# dual ESM/CJS smoke, actionlint) on the standard Node matrix. Repo-specific extra jobs (e.g. an
# examples smoke, a starter-kit build) are added on top of this caller, never instead of it.
# The template ships a PHI scanner (scripts/phi-scan.ts), so the shared pipeline's optional
# PHI-scan step is switched on. NOTE: the scaffolded scanner is a STARTER it detects only
# PHI-scan step is switched on. NOTE: the scaffolded scanner is a STARTER: it detects only
# cross-cutting SSN/email shapes until you add structured, field-level detection for this
# standard's PHI (see the STARTER banner in scripts/phi-scan.ts).
ci:
Expand Down
158 changes: 158 additions & 0 deletions .github/workflows/no-emdash.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: Em-dash gate

# The brand rule (founder directive 2026-07-24, stated canonically in the knowledgebase
# brand voice document) bans U+2014 outright across every cosyte surface, and names commit
# messages explicitly.
#
# THIS GATE DID NOT ARRIVE ON A CLEAN TREE, AND THAT ORDERING IS THE POINT. The change that
# landed this workflow also swept 609 occurrences out of 73 of the 98 tracked files,
# including `README.md`, the `docs-content/` pages that publish to the documentation site,
# and the `src/` JSDoc that compiles into `dist/index.d.ts` and renders on a consumer's
# hover. A gate landing before its sweep reds `main` on arrival; a sweep landing before its
# gate grows the character back on the next session. Both are one commit.
#
# WHY A SEPARATE WORKFLOW, and not a job in ci.yml. Same two reasons this repo's
# `no-internal-refs.yml` gives, and the second is the deciding one here:
#
# The reusable pipeline is not an option. This repo's `ci` job calls
# cosyte/.github/.github/workflows/ci.yml@main, which runs a FIXED ladder (typecheck,
# lint, format:check, phi-scan, test, coverage, build, attw, dual ESM/CJS smoke) and no
# arbitrary repo script. It does NOT run `pnpm check:no-emdash`.
#
# AND THE RULESET IS THE SECOND HALF OF THAT. `ci / verify` is a REQUIRED context here
# (ruleset `ci-required-checks`, id `19914044`), and a required job gates all of its
# STEPS, so folding a new step into `verify` would silently change what that required
# context covers. A separate workflow with its own job id is a new context, requirable on
# its own terms, and reversible without touching the four that already bind.
#
# The trigger differs. The messages job below needs the `edited` activity type, which is
# not a default: without it, a title or body edited after the last push is never
# re-checked, and this repo squash-merges, so the PR title and body ARE the commit
# message that lands on `main`. But ci.yml's triggers drive the full Node 22 + 24 matrix.
# Re-running all of that every time someone fixes a typo in a PR description is a large
# cost for no extra safety.
#
# ▶ TWO JOBS, DELIBERATELY, AND THE SPLIT IS THE WHOLE DESIGN. A sibling shipped this as
# ONE job and had to exempt the lot, which un-required the tracked-file half as well: the
# half that protects the published surface. That sibling names the split as its deferred
# fix. It is done here from the start, and it is the shape `website` already uses.
#
# `no-emdash` scans TRACKED FILES ONLY. Nothing outside this repository can put an em
# dash into one. Dependabot certainly writes tracked files (`package.json` and
# `pnpm-lock.yaml` are its whole job), but it writes version specifiers and lockfile
# records, never prose. So this job is safe to make a required context, and it SHOULD
# become one.
#
# `no-emdash-messages` scans the PR TITLE, BODY and COMMIT MESSAGES, and it must NEVER be
# required. Dependabot composes a PR body by pasting the dependency's UPSTREAM RELEASE
# NOTES into it, em dashes included. Requiring this context would block a dependency bump
# on prose nobody in this org wrote and nobody here can edit without rewriting the PR by
# hand. That is the same refusal this ecosystem already made for a CI `pnpm audit`: a
# gate that fails on someone else's clock stops being a signal and becomes a tax.
# HONOUR THIS EXEMPTION AND ITS WRITTEN REASON; DO NOT REMOVE IT.
#
# And do not "fix" the Dependabot case by requiring it with an actor `if:`. An
# `if: github.actor != 'dependabot[bot]'` on a REQUIRED context leaves the check
# permanently PENDING on exactly those PRs, which is worse than red: nothing says why.
#
# ▶ NEITHER CONTEXT IS REQUIRED YET, AND THAT IS CORRECT ORDERING RATHER THAN AN OVERSIGHT.
# A context may not be required before its workflow has completed on `main`: every PR
# would sit pending and unmergeable with nothing saying why. Once this workflow has run
# on `main`, add `no-emdash` (the tracked-file job, NOT the messages one) to the
# `ci-required-checks` ruleset (id `19914044`, the ONE ruleset this repo has: fold into
# it, never add a second), pinned to `integration_id: 15368` the way the existing
# contexts are. Read the real context name off a live check run first, never off this
# file's `name:`: this repo's `Public-surface gate` workflow emits the context
# `no-internal-refs`, and requiring a context nothing emits leaves every PR PENDING and
# unmergeable rather than red. The workflow here is titled "Em-dash gate" and the
# contexts are the bare job ids `no-emdash` and `no-emdash-messages`.
#
# A required job gates all of its STEPS, so splitting a step out of one silently
# un-requires it. Rename a job id and any ruleset entry naming it detaches without error.
# Change both together or neither.
#
# Run it locally with `pnpm check:no-emdash`.

on:
push:
branches: [main]
pull_request:
branches: [main]
# `edited` is deliberate and load-bearing; see the note above. It costs the
# tracked-file job a re-run on a description edit, which is a checkout and one Node
# process, and it is what keeps the messages job honest.
types: [opened, synchronize, reopened, edited]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
# The job id is the check-run context name a branch ruleset requires. This is the half
# that is safe to require; see the note above.
no-emdash:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# The gate is zero-dependency Node, so there is nothing to install and no
# lockfile cache to warm. It needs a Node new enough for `import.meta.dirname`,
# and this repo's own engines floor is >= 22.
node-version: 22

# No `|| true` and no `continue-on-error`: a check that cannot fail is
# documentation. The script refuses rather than reporting a clean tree whenever it
# cannot prove it read its subject.
- name: Check tracked files and their names
run: node scripts/check-no-emdash.mjs

# THE HALF THAT MUST NEVER BE A REQUIRED CONTEXT. See the Dependabot note above.
no-emdash-messages:
runs-on: ubuntu-latest
# Pull requests only: on a push to `main` the message is already written, and this job
# exists to stop one landing.
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Full history: the step below reads the PR's commit range, and the default
# shallow clone does not contain the base commit.
fetch-depth: 0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22

# The rule names commit messages explicitly, and a squash merge composes its message
# from the PR title, the PR body, and the branch commits, so all three are checked.
#
# This is also the half a local pre-commit scan structurally cannot cover, and two
# slices elsewhere in this ecosystem have lost a review pass to exactly that: a NEW
# file is untracked so a scan of the index does not see it, and no local hook sees a
# PR body at all.
- name: Check PR title, body, and commit messages
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Collect first, scan second, and set the shell options here rather than trust
# the runner's default: piping the collection straight into the scanner would
# hide a failed `git log` (a rewritten base commit, or a future edit that drops
# fetch-depth) behind a clean title and body, and the gate would print OK over
# commits it never read. The title and body arrive through env and are never
# interpolated into the script, so a PR description cannot inject shell.
set -euo pipefail
{
printf '%s\n' "$PR_TITLE"
printf '%s\n' "$PR_BODY"
git log --format=%B "$BASE_SHA..$HEAD_SHA"
} > "$RUNNER_TEMP/pr-messages.txt"
node scripts/check-no-emdash.mjs --stdin "the PR title, body, or commit messages" \
< "$RUNNER_TEMP/pr-messages.txt"
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ on:
jobs:
release:
# Required. The repo default for GITHUB_TOKEN is `contents: read`, and a called workflow can only
# downgrade the caller's token, never escalate it so without this the shared pipeline's
# downgrade the caller's token, never escalate it, so without this the shared pipeline's
# `contents/id-token/pull-requests: write` is an escalation and GitHub rejects the whole workflow
# at startup (1s, no jobs, no logs).
permissions:
Expand Down
Loading
Loading