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
20 changes: 11 additions & 9 deletions .claude/skills/ci-preflight/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Use before pushing a branch, opening or updating a PR, or whenever

## Overview

`.github/workflows/ci.yml` is 15 blocking steps across two jobs. Every one of them can run
`.github/workflows/ci.yml` is 17 blocking steps across two jobs. Every one of them can run
locally, so a red CI run is always avoidable — `bun test` passing is not evidence, and it is
the single most common reason work gets handed over broken.

Expand All @@ -19,10 +19,10 @@ node .claude/skills/ci-preflight/run-ci.mjs

~2.5 minutes warm on a green tree. Full output per step goes to
`node_modules/.cache/ci-preflight/<step>.log`; only a summary and a tail of each failure
reach stdout, so a red run costs a few hundred tokens rather than the ~40k that fourteen
reach stdout, so a red run costs a few hundred tokens rather than the ~40k that sixteen
raw `bun run` calls would.

Do not hand-run the fourteen commands instead. Two things go wrong when you do:
Do not hand-run the sixteen commands instead. Two things go wrong when you do:

- **Order is load-bearing.** `test`, `api`, `lint:publish` and every `verify:*` gate resolve
`@dexpace/core` by package name, which lands in `packages/core/dist/`. Run any of them
Expand All @@ -35,7 +35,7 @@ Do not hand-run the fourteen commands instead. Two things go wrong when you do:
1. **Run it.** Add `--skip-install` only if you have not touched `package.json` since the
last install. **Before you push, add `--clean`** — a warm tree is blind to a whole class of
defect CI hits on its first step. (The pinned Bun needs no flag; it is the default.)
2. **All green** → say so plainly: CI is all good, naming the count (`all 15 steps passed`).
2. **All green** → say so plainly: CI is all good, naming the count (`all 17 steps passed`).
Nothing else to do.
3. **Anything red** → report the findings to the user *first*: which gates failed, what each
one means, and the fix you intend. One line per finding, not a transcript dump.
Expand Down Expand Up @@ -107,16 +107,18 @@ Both of these will make you report a passing gate that CI rejects.
| `lint` | Formatting **and** type-aware rules; formatting is an error, not a warning. | `bun run fix` first — it clears every prettier finding. Hand-fix what survives: 70-line function cap, `max-depth` 3, `max-params` 3, explicit return types on exported members. Every `eslint-disable` needs a `-- reason`. |
| `build` | Emit failed. **Blocks the eleven gates below it**, which the runner reports `SKIP`. | Fix this before reading anything else; the skipped gates are unknown, not passing. |
| `test` | A failing test, *or* the silent coverage floor (see above). | If the tail says `0 fail`, it is coverage — find the file that dropped below 0.8 in the printed table and test it. Otherwise fix the test or the code. |
| `test:scripts` | A gate's own logic broke, or the knowledge corpus shifted under an assertion that pins its shape. | `node --test scripts/<name>.test.mjs` for detail. If it is `knowledge.test.mjs`'s ID-less-topic count, a corpus edit gave a previously ID-less topic its first requirement ID — confirm that was intended, then move the number in the test, `CLAUDE.md` and `knowledge-lookup/SKILL.md` together. Otherwise fix the gate; never relax the assertion to match a degraded gate. |
| `api` | The committed `etc/<pkg>.api.md` no longer matches the built surface, or an export lacks TSDoc. | Intended export change: `cd packages/<pkg> && bun run api:local`, then commit the regenerated report. `(undocumented)` in the diff means the export needs a `@public` block, plus `@throws` naming each catchable error class. **Unintended** change: revert the export, don't bless the report. |
| `lint:publish` | `publint` + `attw` on every built package's `exports` map, `types`/`main` fields, and declaration resolution. | Fix the manifest. `cjs-resolves-to-esm` is already ignored by design (ESM-only); every other rule is real. |
| `verify:dual-consumption` | A built package is no longer importable and runnable by plain `node` through its package name. | Usually a broken `exports` map or a subpath that ships no JS. |
| `verify:consumer-types` | The built `.d.ts` does not compile on the declared `lib` with `types: []` — i.e. a dev-only global (`@types/bun`) leaked into the public surface. | Remove the dependency on the dev global, or declare it. This gate exists because exactly that defect passed all four gates above it. |
| `verify:seam-1` | A package gained a runtime dependency outside the allow-list, or dropped its committed empty `dependencies` object (an omitted field is a violation too). | Remove the dependency — SEAM-1 is the constraint, not the gate. `@dexpace/core` is a **peer** of the satellites, never a dependency. |
| `verify:sse-37` | Core's SSE code reached for serde or a codec package. | Remove the import; SSE-37/38 forbid the coupling. |
| `verify:runtime-floor` | `engines.node` and the `target`/`lib` a package compiles to have drifted apart. | Move both together, deliberately — never raise one to silence this. |
| `verify:test-partition` | One of the five strings that keep `tests/conformance/` (Bun) and `tests/node-conformance/` (`node --test`) apart has drifted — see CLAUDE.md's hard rule. Every way this breaks is silent: Bun runs `node:test` files and reports them **passing**, Bun ignores an unrecognized `[test]` key with no warning, and `node --test` over a glob matching nothing exits 0. | The assertion names the file and the string. Fix all five together — `bunfig.toml`, `package.json`, `eslint.config.js`, `run-ci.mjs`, `tests/node-conformance/README.md` — never one alone. Unlike the gates around it this one reads files only, so it still reports through a red `build` rather than going `SKIP`. |
| `verify:reproducible-build` | Two clean builds of an identical source tree disagreed (NFR-12) — either in an emitted `dist/` file or in an `npm pack` tarball. | The assertion names what differed. A wall-clock or random value reaching a build-time codegen step is the usual cause; `packages/core/scripts/gen-version.mjs` is the only such step today, and injecting a `Date.now()` there is this gate's own negative test (it fails naming `packages/core/dist/generated/version.js` and `npm-pack:dexpace-core-0.0.0.tgz`). If the log instead ends in `tsc` errors, the **build inside the gate** failed and there is no difference to read — fix that first. The gate sweeps every `dist/` and rebuilds twice itself, so it is last in the job and leaves the tree freshly built. |
| `audit` | A high-severity advisory in production dependencies. | `bun audit --prod` for detail. Note the tree is tiny (zero runtime deps by design), so a hit here is usually a transitive dev-dep misclassification worth reading carefully. |
| `test:node` | Bun-vs-Node runtime divergence, almost always in `packages/core/src/io/` — Web Streams, `AbortSignal`, `Uint8Array` chunking. | Fix against Node's semantics. A phase touching a runtime-divergent surface should be *adding* cases here; see `test/node-conformance/README.md`. |
| `test:node` | Bun-vs-Node runtime divergence, almost always in `packages/core/src/io/` — Web Streams, `AbortSignal`, `Uint8Array` chunking. | Fix against Node's semantics. A phase touching a runtime-divergent surface should be *adding* cases here; see `tests/node-conformance/README.md`. |

**A `timeout` verdict is not the same finding as a red one.** Every step is capped at
`STEP_TIMEOUT_MS` (10 minutes) in `run-ci.mjs`, and a step that hits the cap is reported `timeout`
Expand All @@ -136,7 +138,7 @@ your report when they matter:
not prove the floor. `--node-floor` runs the floor leg via `mise`/`fnm`/`nvm` (downloading
the toolchain once); the runner prints a note when the active major is not 20.

**Run it whenever the change adds or edits a file under `test/node-conformance/`**, touches
**Run it whenever the change adds or edits a file under `tests/node-conformance/`**, touches
`io/`, reaches for a new built-in, or moves the floor. This gap is not theoretical: Phase
8a's `transport.test.mjs` passed on Node 26 and failed 20 of 22 cases on 20.3.0, because an
async *root-level* `before` hook does not complete before subtests inside a `describe` when
Expand All @@ -148,9 +150,9 @@ your report when they matter:
CI also runs `node-conformance` only after the `ci` job succeeds — so locally, a `test:node`
failure alongside other failures is the same signal, just surfaced earlier.

Not in CI at all, so the runner does not include them: `bun run test:scripts` (tests the
gates themselves — run it by hand after touching `scripts/`), and changesets (a
consumer-facing change still needs `bun run changeset`).
Not in CI at all, so the runner does not include it: changesets (a consumer-facing change
still needs `bun run changeset`). `test:scripts` used to be on this list; Phase 10 wired it
into the `ci` job (open-items H13), so the runner covers it now.

## Runner flags

Expand Down
32 changes: 28 additions & 4 deletions .claude/skills/ci-preflight/run-ci.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Runs every blocking step of `.github/workflows/ci.yml` against the working tree, in CI's own
// order, and reports all failures at once rather than stopping at the first.
//
// Two things make this more than a shell alias for fourteen `bun run` calls:
// Two things make this more than a shell alias for sixteen `bun run` calls:
//
// * Ordering is load-bearing. `bun test`, `api`, `lint:publish` and every `verify:*` gate resolve
// `@dexpace/core` by package name, which lands in `packages/core/dist/`. Run them before
Expand Down Expand Up @@ -62,6 +62,17 @@ const STEPS = [
' test. Find the file that dropped below it in the table above.'
: null,
},
{
// Tier `static`, not `gate`. `tier` has exactly one consumer -- the build-failed SKIP rule at
// the bottom of this file, which tests for `gate` -- so `static` means "runs even when `build`
// is red". Correct here and for verify:test-partition below: both read files and resolve no
// workspace package by name, so a failed build does not make either meaningless the way it does
// the gates around them.
id: 'test:scripts',
ci: 'Gate self-tests (scripts/*.test.mjs)',
cmd: 'bun run test:scripts',
tier: 'static',
},
{
id: 'api',
ci: 'API surface check',
Expand Down Expand Up @@ -105,6 +116,14 @@ const STEPS = [
cmd: 'bun run verify:runtime-floor',
tier: 'gate',
},
{
// Tier `static` — see `test:scripts` above.
id: 'verify:test-partition',
ci: 'Test-partition check (tests/ vs tests/node-conformance/)',
cmd: 'bun run verify:test-partition',
tier: 'static',
fix: 'the assertion names the string that drifted — change all five together, never one alone',
},
{
// Last among the gates, matching ci.yml: it sweeps every dist/ and rebuilds twice, so running it
// earlier would pull the tree out from under any step that resolves a workspace package by name.
Expand Down Expand Up @@ -320,19 +339,24 @@ function report(results, skipped, opts) {
return 1;
}

// The three globs below duplicate `package.json`'s `test:node`, deliberately: this leg runs the
// suite under a pinned Node, so it cannot go through `bun run`. They are three of the five strings
// that hold the `tests/` partition (CLAUDE.md's hard rule) and are checked by
// `scripts/verify-test-partition.mjs` -- a stale glob here makes `node --test` match nothing and
// exit 0, which reads as a clean floor run over zero cases.
function runNodeFloor(opts, childEnv) {
const managers = [
[
'mise',
`mise x node@${NODE_FLOOR} -- node --test test/node-conformance/*.test.mjs`,
`mise x node@${NODE_FLOOR} -- node --test tests/node-conformance/*.test.mjs`,
],
[
'fnm',
`fnm exec --using=${NODE_FLOOR} node --test test/node-conformance/*.test.mjs`,
`fnm exec --using=${NODE_FLOOR} node --test tests/node-conformance/*.test.mjs`,
],
[
'nvm',
`bash -lc 'nvm exec ${NODE_FLOOR} node --test test/node-conformance/*.test.mjs'`,
`bash -lc 'nvm exec ${NODE_FLOOR} node --test tests/node-conformance/*.test.mjs'`,
],
];
const found = managers.find(
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/knowledge-lookup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Use when starting a numbered task from a docs/superpowers/plans/ fi

## Overview

`docs/knowledge/` is 39 topic files and ~1470 harvested entries — 512 KB, past what belongs
`docs/knowledge/` is 39 topic files and ~1470 harvested entries, past what belongs
in context. `bun run knowledge` filters it. A requirement-ID query runs ~120–580 tokens
(median ~230) against a topic file of ~1800–5200 (median ~2300): roughly 9× smaller, and
much more than that when the ID you want lives in a file you'd never have guessed.
Expand Down Expand Up @@ -39,7 +39,7 @@ Different filters AND together; multiple values inside one filter OR. So

## Check the result is real before trusting it

**A `--req` hit is not proof the corpus knows anything.** 256 of the 641 cited IDs resolve
**A `--req` hit is not proof the corpus knows anything.** 255 of the 645 canonical IDs resolve
*only* to an appendix-B conformance roll-up — one sentence naming three to five IDs and
stating none of them. It exits 0, so nothing else will warn you.

Expand Down Expand Up @@ -70,7 +70,7 @@ bun run knowledge --list-topics # 39 topics, entry and ID cou
bun run knowledge --topic pipeline --section rules --brief cursor fork
```

**16 of the 39 topics carry no requirement ID at all** — every styleguide-derived one,
**15 of the 39 topics carry no requirement ID at all** — every styleguide-derived one,
including `data-modeling`, `error-handling`, `assertions`, `testing`, `api-design`. ID-first
cannot reach them. `--list-topics` shows which; don't work from a memorised list.

Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ jobs:
- name: Test (with coverage)
run: bun run test --coverage

# The gates' own tests (`scripts/*.test.mjs`), on `node --test`. Deliberately outside
# `bun run test`: bunfig scopes discovery and the 80% coverage floor to `packages`, and that
# floor is a statement about `packages/core`, not about repo tooling. What this protects is a
# gate's logic silently degrading — a bad glob, a swallowed assertion — which no other step
# would notice, since a degraded gate still exits 0. Closes open-items H13, whose trigger had
# already fired: `knowledge.test.mjs` was failing on `main` and nothing ran it.
- name: Gate self-tests (scripts/*.test.mjs)
run: bun run test:scripts

- name: API surface check
run: bun run api

Expand Down Expand Up @@ -62,6 +71,12 @@ jobs:
- name: Runtime-floor consistency check
run: bun run verify:runtime-floor

# Reads the five files that must agree on `tests/node-conformance/`. The rule and the reasons
# it exists live in CLAUDE.md, "HARD RULE — the `tests/` partition"; what matters here is only
# that every way it breaks is silent, so nothing else in this workflow would catch it.
- name: Test-partition check (tests/ vs tests/node-conformance/)
run: bun run verify:test-partition

# NFR-12. Deliberately last in this job: it sweeps every dist/ and rebuilds
# the workspace twice, so it would otherwise pull the rug from under any
# step above that resolves a workspace package through its dist/.
Expand Down
Loading
Loading