Skip to content

Commit ca2a806

Browse files
ORESoftwareclaude
andcommitted
chore: land uncommitted working-tree changes (4 paths)
Captured from a dirty checkout so the work is on GitHub rather than only on one laptop. Built through a temporary index: the working tree, HEAD and the index in ores/json-native-parser were not modified, and nothing was stashed, reset or discarded. Base: 824dd8c Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LQF5QdYe9exL6HgFBSbapi
1 parent 824dd8c commit ca2a806

19 files changed

Lines changed: 2122 additions & 1 deletion

‎.github/workflows/ores-lint.yml‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Managed by .ores-lint/ - regenerated by the rollout script.
2+
name: ores-lint
3+
4+
on:
5+
pull_request:
6+
workflow_dispatch:
7+
# Add `push:` here once this repo's lint debt is paid down.
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Detect project types
16+
id: detect
17+
run: |
18+
{ [ -f package.json ] || git ls-files '*.js' '*.mjs' '*.cjs' '*.ts' '*.tsx' | grep -q .; } \
19+
&& echo "js=true" >> "$GITHUB_OUTPUT" || echo "js=false" >> "$GITHUB_OUTPUT"
20+
{ [ -f Cargo.toml ] || git ls-files '*.rs' | grep -q .; } \
21+
&& echo "rs=true" >> "$GITHUB_OUTPUT" || echo "rs=false" >> "$GITHUB_OUTPUT"
22+
{ [ -f pubspec.yaml ] || git ls-files '*.dart' | grep -q .; } \
23+
&& echo "dart=true" >> "$GITHUB_OUTPUT" || echo "dart=false" >> "$GITHUB_OUTPUT"
24+
{ [ -f gleam.toml ] || git ls-files '*.gleam' | grep -q .; } \
25+
&& echo "gleam=true" >> "$GITHUB_OUTPUT" || echo "gleam=false" >> "$GITHUB_OUTPUT"
26+
27+
# Node is the runner for ESLint and for the cross-language require-send
28+
# scanner, so it is installed whenever any supported language is present.
29+
- uses: actions/setup-node@v4
30+
if: steps.detect.outputs.js == 'true' || steps.detect.outputs.rs == 'true' || steps.detect.outputs.dart == 'true' || steps.detect.outputs.gleam == 'true'
31+
with:
32+
node-version: '22'
33+
34+
# ESLint is a global tool here, not a repo dependency - so this job never
35+
# runs `npm install` and never needs the repo's node_modules.
36+
- name: Install eslint globally
37+
if: steps.detect.outputs.js == 'true'
38+
run: npm i -g eslint typescript-eslint
39+
40+
- name: Install clippy
41+
if: steps.detect.outputs.rs == 'true'
42+
run: rustup component add clippy || true
43+
44+
# Dart/Gleam SDKs are optional. When absent, dart.sh / gleam.sh skip
45+
# with an actionable message; require-send.mjs still runs via Node.
46+
- uses: dart-lang/setup-dart@v1
47+
if: steps.detect.outputs.dart == 'true'
48+
continue-on-error: true
49+
50+
- name: Run ores-lint
51+
run: sh .ores-lint/lint.sh
52+
env:
53+
# Warn-only. Set to 1 here to make lint findings fail this repo's CI.
54+
ORES_LINT_STRICT: '0'

‎.ores-lint/README.md‎

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# ores-lint
2+
3+
A vendored, dependency-free lint baseline for every JavaScript/TypeScript,
4+
Rust, Dart/Flutter and Gleam repo in the org fleet. Everything it needs is in
5+
this directory — there is nothing to install from a registry and nothing to
6+
keep in version sync.
7+
8+
Only universally accepted linters are used as hosts:
9+
10+
| language | host linter | custom house rules |
11+
|---|---|---|
12+
| TypeScript / JS | ESLint 9+ (flat config) | `ores/require-send`, `ores/semi` |
13+
| Rust | clippy | `implicit_return` house style; `#[must_use]` on log `Event`; require-send scanner |
14+
| Dart / Flutter | `dart analyze` | require-send scanner |
15+
| Gleam | `gleam format --check` + `gleam check` | require-send scanner |
16+
17+
There is no ESLint-quality plugin host for Gleam, and Dart's `custom_lint` /
18+
Rust's `dylint` would pull registry packages into every repo. The one house
19+
rule that those hosts cannot express — **logger chains must end in `send()`** —
20+
is implemented as a vendored ESLint rule for TS and a small Node scanner for
21+
the other three languages.
22+
23+
## Running it
24+
25+
```sh
26+
sh .ores-lint/lint.sh # lints whatever this repo contains
27+
sh .ores-lint/selftest.sh # verifies the toolkit still works after a toolchain upgrade
28+
```
29+
30+
For JS repos it is also wired into `npm run lint:ores`, and runs automatically
31+
before `npm run build` (`prebuild`) and before `npm publish` (`prepublishOnly`).
32+
33+
## What it enforces
34+
35+
**JavaScript / TypeScript** — via ESLint (flat config):
36+
37+
| rule | why |
38+
|---|---|
39+
| `semi` | house style: semicolons are required, missing ones warn |
40+
| `ores/require-send` | a logging chain that reaches `.info()`/`.warn()`/… but never calls `.send()` or `.send(boolean)` builds an event that is never delivered |
41+
| correctness set | `eqeqeq`, `no-unreachable`, `no-dupe-keys`, `use-isnan`, `valid-typeof`, `no-async-promise-executor`, and similar low-false-positive checks |
42+
43+
**Rust** — via clippy, plus rustc `#[must_use]` on ores-otel `Event`:
44+
45+
| lint | why |
46+
|---|---|
47+
| `clippy::implicit_return` | house style: prefer an explicit `return` at tail position |
48+
| `clippy::correctness`, `clippy::suspicious` | real defects |
49+
| `unwrap_used`, `expect_used`, `panic_in_result_fn`, `todo`, `dbg_macro` | things that should not reach a publish |
50+
51+
**Dart / Flutter** — via `dart analyze` (or `flutter analyze`). If the repo has
52+
no `analysis_options.yaml`, rollout drops a baseline of analyzer-shipped
53+
correctness lints. Existing files are never overwritten. Prefer
54+
`package:lints` / `package:flutter_lints` when the package already depends on
55+
them — those are the Dart equivalents of `eslint:recommended`.
56+
57+
**Gleam** — via `gleam format --check` and `gleam check`. Unused values already
58+
catch many forgotten `send` calls; the scanner catches the rest (assigned
59+
events, unfinished pipes).
60+
61+
**require-send (all four languages).** The ores-otel logger builds an event
62+
through method chaining (TS/Rust/Dart) or pipes (Gleam). Delivery is
63+
`send()`, `send(boolean)`, or `send_with_store(...)`. Forgetting that call
64+
means the event is built and then dropped — unless shutdown recovers it, which
65+
is a fallback, not the API. Tests that deliberately build unsent events are
66+
skipped unless `ORES_LINT_REQUIRE_SEND_INCLUDE_TESTS=1`.
67+
68+
## Overriding a finding on one line
69+
70+
This is expected. Some tests, shutdown-recovery fixtures, and rare control
71+
flow should not call `send()`. Use the comment that matches the host linter,
72+
or the unified ores-lint form which works in every language:
73+
74+
```
75+
// ores-lint-disable-next-line require-send
76+
// ores-lint-disable-line require-send
77+
// ores-lint-disable-file require-send
78+
```
79+
80+
Language-native equivalents, also honoured:
81+
82+
| language | one-line override |
83+
|---|---|
84+
| TypeScript | `// eslint-disable-next-line ores/require-send` |
85+
| Rust (`#[must_use]`) | `#[allow(unused_must_use)]` on the next statement |
86+
| Dart analyzer | `// ignore: name_of_lint` (analyzer rules only; require-send uses the ores-lint form) |
87+
| Gleam | the ores-lint form above |
88+
89+
Do not disable the rule for a whole file unless the file is a generated
90+
fixture. Prefer the next-line form.
91+
92+
## The two things worth knowing
93+
94+
95+
**1. Implicit-return findings are capped.** `clippy::implicit_return` fires once
96+
per implicit return, which on a real crate is hundreds of identical lines. The
97+
lint stays fully enabled so nothing is missed, but `rust.sh` collapses it into a
98+
single warning showing at most 5 concrete locations plus `... and N more`. The
99+
same cap applies to every ESLint rule via `eslint/formatter.mjs`. Change it with
100+
`ORES_LINT_MAX_EXAMPLES`.
101+
102+
**2. `clippy::needless_return` had to be disabled.** It ships enabled in
103+
clippy's default `style` group and warns on exactly the explicit returns this
104+
house style asks for. Enabling `implicit_return` without allowing
105+
`needless_return` makes the two lints contradict each other on every function in
106+
the crate. `selftest.sh` asserts this stays true.
107+
108+
## Scope: sub-projects and repo boundaries
109+
110+
The linter does **not** assume the repo root is the only project.
111+
112+
- **Rust** — `rust.sh` finds every crate in the repo, including ones under
113+
`apps/` or `clients/`. Crates that are workspace members of an already-linted
114+
root are skipped (via `cargo metadata --no-deps`) so nothing is linted twice,
115+
and findings from every crate are aggregated into **one** capped report.
116+
- **JS/TS** — a flat config at the repo root makes `eslint .` reach nested
117+
packages, so the config goes in even when the JS lives in a subdirectory.
118+
119+
**Nested git repositories are a hard boundary.** A repo checked out inside
120+
another repo gets its own ores-lint install; the parent must not lint it, or the
121+
same findings get reported twice under the wrong repo name and the same
122+
`package.json` gets wired with two conflicting relative paths. `rollout.mjs`
123+
records those boundaries in `.ores-lint/nested-repos.json`, and both halves of
124+
the linter read it.
125+
126+
To exclude a repo entirely — vendored upstream forks, for instance — drop an
127+
empty `.ores-lint-ignore` file at its root.
128+
129+
## Legacy config migration
130+
131+
ESLint 9+ reads flat config **only**. Three older mechanisms are silently
132+
ignored, which means any repo still relying on them has not been linted at all:
133+
134+
| legacy mechanism | status |
135+
|---|---|
136+
| `.eslintrc*` | ignored entirely; rules are dead |
137+
| `eslintConfig` key in `package.json` | ignored entirely |
138+
| `.eslintignore` | ignored, with a warning |
139+
140+
`audit.mjs` reports every repo in each category. `.eslintignore` is ported
141+
automatically into flat-config `ignores` by `base.mjs` (gitignore semantics
142+
preserved), so its intent keeps applying. The other two need a human decision
143+
and are migrated per repo — porting the rules that still make sense, and saying
144+
in a comment which ones were dropped and why.
145+
146+
## Warn-only, by design
147+
148+
`lint.sh` exits 0 no matter what it finds. It is wired into build and publish
149+
hooks across hundreds of repos, so it is built to be incapable of breaking one
150+
unless a human opts in.
151+
152+
To make findings blocking for a single repo, create `.ores-lint/local.sh`:
153+
154+
```sh
155+
ORES_LINT_STRICT=1
156+
```
157+
158+
`local.sh` is yours — the rollout script never overwrites it. Everything else in
159+
this directory is managed and will be replaced on the next rollout.
160+
161+
## Knobs
162+
163+
| variable | default | meaning |
164+
|---|---|---|
165+
| `ORES_LINT_MAX_EXAMPLES` | `5` | example locations shown per rule |
166+
| `ORES_LINT_STRICT` | `0` | `1` makes any finding exit non-zero |
167+
| `ORES_LINT_SKIP_JS` / `ORES_LINT_SKIP_RUST` / `ORES_LINT_SKIP_DART` / `ORES_LINT_SKIP_GLEAM` | `0` | skip one language host |
168+
| `ORES_LINT_SKIP_REQUIRE_SEND` | `0` | skip the cross-language send() scanner |
169+
| `ORES_LINT_REQUIRE_SEND_INCLUDE_TESTS` | `0` | `1` also scans `test/` / `*_test.dart` / etc. |
170+
| `ORES_LINT_RUST_ALL_TARGETS` | `0` | `1` also lints tests/benches/examples |
171+
| `ORES_LINT_RUST_EXTRA` | — | extra flags appended to the clippy invocation |
172+
173+
## Graceful degradation
174+
175+
Nothing here is allowed to fail loudly for an environmental reason. ESLint not
176+
installed, too old, clippy not installed, dart/flutter not installed, gleam not
177+
installed, no TypeScript parser available, crate deps not fetchable — each is
178+
reported as an actionable skip, not an error.
179+
Repo-specific ESLint config that already existed is never overwritten.
180+
The same is true of `analysis_options.yaml`.
181+
182+
CI follows the same model: the workflow runs `npm i -g eslint typescript-eslint`
183+
and never runs `npm install` for the repo itself, so linting a PR does not
184+
require the repo's dependency tree to resolve.
185+
186+
## Per-repo customisation
187+
188+
`eslint.config.mjs` at the repo root takes options:
189+
190+
```js
191+
export default await oresConfig({
192+
requireSend: { loggerNames: ['myLogger'], terminalMethods: ['send', 'flush'] },
193+
rules: { 'no-console': 'warn' },
194+
ignores: ['**/generated/**'],
195+
});
196+
```
197+
198+
Once you edit that file the rollout script leaves it alone.
199+
200+
---
201+
202+
## Fleet operations (from the `codes` directory)
203+
204+
```sh
205+
node .ores-lint-toolkit/audit.mjs # report the fleet's lint posture
206+
node .ores-lint-toolkit/audit.mjs --json out.json # ...as machine-readable data
207+
node .ores-lint-toolkit/rollout.mjs --dry-run # preview
208+
node .ores-lint-toolkit/rollout.mjs # install / re-install everywhere
209+
node .ores-lint-toolkit/rollout.mjs --only ores-otel
210+
node .ores-lint-toolkit/rollout.mjs --shard 0/8 # one slice of a fleet-wide run
211+
node .ores-lint-toolkit/verify.mjs # assert every repo is correctly installed
212+
```
213+
214+
A full rollout over ~900 repos takes a few minutes. `--shard k/n` splits it into
215+
bounded chunks, which matters when the runner has a per-command time limit.
216+
217+
Re-run the rollout after editing anything in `.ores-lint-toolkit/` — it is
218+
idempotent and propagates the change to every repo.

‎.ores-lint/VERSION‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.3.0

‎.ores-lint/config.sh‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/sh
2+
# ores-lint shared configuration. Sourced by lint.sh, js.sh and rust.sh.
3+
# Every value can be overridden from the environment, or per repo in local.sh.
4+
5+
# Maximum number of concrete example locations shown for any one rule.
6+
: "${ORES_LINT_MAX_EXAMPLES:=5}"
7+
8+
# Warn-only by default: lint.sh exits 0 no matter what it finds.
9+
# Flip to 1 (per repo, or in CI) once a repo's debt is paid down.
10+
: "${ORES_LINT_STRICT:=0}"
11+
12+
: "${ORES_LINT_SKIP_RUST:=0}"
13+
: "${ORES_LINT_SKIP_JS:=0}"
14+
: "${ORES_LINT_SKIP_DART:=0}"
15+
: "${ORES_LINT_SKIP_GLEAM:=0}"
16+
: "${ORES_LINT_SKIP_REQUIRE_SEND:=0}"
17+
: "${ORES_LINT_REQUIRE_SEND_INCLUDE_TESTS:=0}"
18+
19+
# How deep to search for nested sub-projects (crates and packages). Repos here
20+
# routinely hold crates under apps/ and clients/ that a root-only lint misses.
21+
: "${ORES_LINT_DEPTH:=5}"
22+
23+
# Include tests/benches/examples in the Rust pass. Off by default so the
24+
# pre-publish signal is about shipped code.
25+
: "${ORES_LINT_RUST_ALL_TARGETS:=0}"
26+
27+
# Minimum ESLint major version. Flat config needs 9+. ESLint is expected to be
28+
# installed GLOBALLY, once - see required-tools.json. Nothing is ever installed
29+
# into a repo's node_modules.
30+
: "${ORES_LINT_ESLINT_MIN_MAJOR:=9}"
31+
32+
# The exact clippy diagnostic text for `clippy::implicit_return`. selftest.sh
33+
# verifies this still matches, so a future clippy rewording surfaces as a test
34+
# failure rather than as a silently empty report.
35+
: "${ORES_LINT_IMPLICIT_RETURN_MSG:=missing \`return\` statement}"
36+
37+
export ORES_LINT_MAX_EXAMPLES ORES_LINT_STRICT ORES_LINT_SKIP_RUST ORES_LINT_SKIP_JS
38+
export ORES_LINT_SKIP_DART ORES_LINT_SKIP_GLEAM ORES_LINT_SKIP_REQUIRE_SEND
39+
export ORES_LINT_REQUIRE_SEND_INCLUDE_TESTS
40+
export ORES_LINT_DEPTH ORES_LINT_RUST_ALL_TARGETS ORES_LINT_ESLINT_MIN_MAJOR
41+
export ORES_LINT_IMPLICIT_RETURN_MSG
42+
43+
# Repo-local overrides, never overwritten by the rollout script. Sourced last so
44+
# anything set here wins.
45+
ORES_LINT_CFG_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
46+
[ -f "$ORES_LINT_CFG_DIR/local.sh" ] && . "$ORES_LINT_CFG_DIR/local.sh"

0 commit comments

Comments
 (0)