@wy-ai-labs/gates
v0.1.0
Published
Gate culture as a tool: manifest-driven gate runner (tiers static/core/full/live, cycle detection, env skips, exit-2 environment-blocked convention) plus shared gate checks and bins — lock hygiene, ESM paths, path deps, architecture direction, deny-list,
Downloads
156
Maintainers
Readme
@wy-ai-labs/gates
Gate culture as a tool — a manifest-driven gate runner (tiers, transitive expansion, cycle rejection, env skips, the exit-2 "environment blocked" convention) plus shared gate checks and bins that report every finding and never let a gate disappear silently.
Part of the wy-ai-labs parts monorepo (packages/gates) · contract v1 · no Python twin
Install
npm install --save-dev @wy-ai-labs/gates # Node ≥22.11, ESM only (import — no require), zero runtime dependencies30-second usage
- Put a
gates.jsonat the repository root (copy the one below and delete what you do not have). - Wire the tiers into
package.json:
{
"scripts": {
"test": "node --test \"test/**/*.test.mjs\"",
"gates:core": "wy-gates run core",
"gates:full": "wy-gates run full --no-stop",
"gates:live": "wy-gates run live",
"gates:inventory": "wy-gates inventory"
}
}- Run them:
npx wy-gates run core # before every commit / PR: stops at the first failure
npx wy-gates run full --no-stop # nightly: runs everything, lists every failure
npx wy-gates list # what each tier expands to
npx wy-gates inventory # Markdown tables for docs/gates-inventory.md
npx wy-check-lock --expected-node 22 # any check is also a standalone bin (exit 0 / 1 / 2)The same thing from JavaScript — pure functions, no process.exit, no output unless you ask for it:
import { runManifest, createConsoleReporter, checkLock, checkEsmPaths } from '@wy-ai-labs/gates';
const summary = await runManifest('gates.json', { tier: 'core', reporter: createConsoleReporter() });
// summary = { ok, tier, results: [{ id, group, command, status: 'pass'|'fail'|'skip', ms, exitCode, reason? }], durationMs, counts, stoppedAt }
process.exitCode = summary.ok ? 0 : 1;
const lock = checkLock({ cwd: process.cwd(), expectedNode: 22 }); // { ok, problems: string[], summary }
const paths = checkEsmPaths({ cwd: process.cwd(), exclude: ['test/**'] });gates.json — copy-paste starting point
{
"version": 1,
"tiers": {
"static": ["lock", "esm-paths", "no-path-deps", "architecture", "denylist"],
"core": ["static", "unit"],
"full": ["core", "error-passthrough", "render"],
"live": ["full", "e2e-live"]
},
"groups": {
"lock": { "commands": ["wy-check-lock --expected-node 22"], "description": "package-lock hygiene: lockfileVersion 3, no libc fields" },
"esm-paths": { "commands": ["wy-check-esm-paths --exclude \"test/**\""], "description": "dynamic import() of a raw filesystem path (Windows crash class)" },
"no-path-deps": { "commands": ["wy-check-no-path-deps"], "description": "registry dependency specs only — no file:/link:/workspace:/git/URL" },
"architecture": { "commands": ["wy-check-architecture --config gates/architecture.json"], "description": "layer direction + entry-file line budgets" },
"denylist": { "commands": ["wy-check-denylist --list gates/denylist.txt --list gates/denylist.local.txt --redact"], "description": "public-safety deny-list: 0 hits" },
"unit": { "commands": ["npm test"], "description": "unit + contract tests, offline" },
"error-passthrough": { "commands": ["wy-check-error-passthrough --exclude \"test/**\""], "description": "error text is never cut below 500 chars at capture" },
"build": { "commands": ["npm run build"] },
"render": { "commands": ["node scripts/check-render.mjs"], "requires": ["build"],
"skipIf": { "envMissing": ["CHROME_PATH"] }, "allowEnvironmentSkip": true, "timeoutMs": 600000,
"description": "pixel render check — needs a browser, skips (never fails) without one" },
"e2e-live": { "commands": ["node scripts/e2e-live.mjs"], "skipIf": { "envMissing": ["LLM_BASE_URL"] }, "allowEnvironmentSkip": true,
"description": "real provider round-trip — manual only, never in CI" }
},
"when": { "live": "manual, before a release" }
}| Manifest field | Meaning |
|---|---|
| tiers.<name> | ordered list of tier or group names; tiers expand transitively (core = everything in static + unit), duplicates run once, a cycle is rejected at load by name (tier cycle: full -> core -> full) |
| groups.<name>.commands | shell command lines, run in order in the manifest's directory with node_modules/.bin on PATH; an entry may be { "command", "name", "env" } |
| requires | groups pulled in before this one (e.g. render requires build) — cycles rejected |
| skipIf.envMissing | skip the whole group with reason env X missing when any listed variable is unset or empty — the runner is never invoked |
| allowEnvironmentSkip | treat exit code 2 from a command as skip — environment blocked (exit 2) instead of fail (the source's ENVIRONMENT BLOCKED convention); default false |
| timeoutMs / env | per-command timeout (a timeout is a fail with error: "timeout after N ms") and extra env for the group |
| when | optional "runs when" text per tier for the inventory (defaults: static/core → every PR, full → nightly, live → manual) |
Every child process also receives WY_GATES_TIER, WY_GATES_GROUP and WY_GATES_ID. Unknown keys (steps, command, skipIf.platform, …) are rejected by name — a typo never degrades into a silently empty gate.
Checks and bins
Every check is a function returning { ok, problems: string[], summary } (cwd-relative paths, every finding, sorted) and a bin exiting 0 OK · 1 problems (listed on stderr) · 2 usage / config error; all bins take --cwd, --json, --quiet, --help.
| Function / bin | Catches | Options |
|---|---|---|
| checkLock / wy-check-lock | package-lock.json missing, lockfileVersion ≠ 3, "libc" fields (npm versions disagree about them → two machines rewrite the lock in turns, dirty tree, blocked git pull); optional --expected-node 22 when run on another Node major | lockFile, expectedNode |
| checkEsmPaths / wy-check-esm-paths | import(path.join(...)), import(__dirname + …), import("C:\\…"), import(require.resolve(…)) without pathToFileURL() — fine on POSIX, ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows | include (default **/*.mjs\|js\|cjs), exclude, hardcodedSlash (opt-in dir + "/x" heuristic) |
| checkNoPathDeps / wy-check-no-path-deps | file: / link: / workspace: / git / github: / URL / ../ specs in any dependency block of package.json and packages/*/package.json | packagesDir, blocks |
| checkArchitecture / wy-check-architecture --config f.json | imports against the allowed layer directions (src ↛ server, both → shared), forbidden bare imports per layer (shared ↛ node:), entry files over their line budget | config { layers, allow, entryBudget, exclude, extensions, aliases, forbiddenImports } |
| checkDenylist / wy-check-denylist --list f [--list …] | regex deny-list hits over the tree (file:line: match [list:line]), same walk/skip rules as the monorepo's scan-text.mjs; no built-in list — bring your own | lists, patterns, redact (hide the matched text in CI logs), quiet, skipDirs, maxBytes |
| checkErrorPassthrough / wy-check-error-passthrough | .slice(0, N) / .substring(0, N) / .substr(0, N) with N < 500 on values that look like error text (err, error, message, text, body, stderr, detail) — the "do not lose the original error" gate; tails (.slice(-N)) and comment lines are fine, // gates: allow-truncate is the reviewable opt-out | include, exclude, minKeep, identifiers, allow |
| checkPattern / wy-check-pattern --config f.json | any forbidden pattern over included files (line by line) and any required pattern in a named file (whole text) — the shape of a repository's "check-no-.mjs" scripts: no ".env editing" instructions in user docs, no hard-coded account-specific model id, shutdown handlers still wired | include, exclude, forbidden: [{ pattern, flags, message }], required: [{ file, pattern, flags, message }] |
wy-check-architecture config example:
{
"layers": [{ "name": "src", "dirs": ["src"] }, { "name": "server", "dirs": ["server"] }, { "name": "shared", "dirs": ["shared"] }],
"allow": { "src": ["shared"], "server": ["shared"] },
"forbiddenImports": { "shared": ["node:", "express"] },
"entryBudget": { "files": ["server/index.ts", "src/main.ts"], "maxLines": 200 },
"exclude": ["**/*.test.*"],
"aliases": { "@/": "src/" }
}docs/gates-inventory.md — template
Keep one inventory per repository. The policy paragraph is hand-written; the tables are pasted from wy-gates inventory whenever the manifest changes.
# Gate inventory — <repository>
Gates are the safety net that unit tests cannot be: environment, platform, lifecycle and log regressions.
Policy (do not revert):
- `npm test` stays unit/contract tests only, seconds, offline. Heavy gates never move back into `pretest`.
- **static / core** run on every PR (`gates:core`, before every commit) · **full** runs nightly and before a release
(`gates:full --no-stop`) · **live** (real providers, devices, networks) runs manually only, never in CI.
- **Gates are never deleted — only quarantined or demoted.** A gate that became slow, flaky or environment-bound moves
to a heavier tier or gets a `skipIf` / `requires` guard; the bug class it once caught stays covered. Note the real
bug each gate caught (commit, issue, version) next to it — that history is what justifies its cost.
- A new gate is added to the manifest *and* to this file: cheap + production-bug-catching → `core`; heavy or
environment-dependent → `full`; live → `live`.
<!-- everything below is generated: `npx wy-gates inventory` -->
## Gate inventory
…Contract
Public API, invariants and error model are fixed in CONTRACT.md; the tests in test/ are the contract suite (one test per numbered invariant). Three invariants to know before depending on this part:
- Cycles and unknown references fail at load, by name — a tier that includes itself (transitively), a
requiresloop, a tier entry naming nothing, or an unknown manifest key throws before any command runs (manifest_cycle/manifest_unknown_ref/invalid_argumentwith the offender in the message). - Exit codes are the status and skips are never silent — exit 0 →
pass; exit 2 →skiponly when the group saysallowEnvironmentSkip: true; anything else →fail; everyskipcarries areasonand an env-missing skip never invokes the runner.stopOnFail(default) reports the rest asskip — stopped after <id> failedso the summary is always complete. - Checks report every finding and throw only for bad input —
{ ok, problems }lists everything (cwd-relative, sorted, deterministic);invalid_argument/not_foundare raised for options and config only, never for what is in the tree; no check embeds a hostname, model id, org name or built-in deny-list.
An incompatible change bumps CONTRACT_VERSION and the major version together (CONTRACT.md → Compatibility).
Mined from
Extracted from three private source repositories — CodeReviewWar (modules scripts/run-test-manifest.mjs, scripts/test-manifest.json, scripts/check-architecture.mjs, docs/gates-inventory.md), LLM_GATEWAY (scripts/check-lock.mjs, check-esm-paths.mjs, check-error-passthrough.mjs, check-no-env-edit.mjs, check-shutdown.mjs, docs/gates-inventory.md) and AgentRAGKnowledge (scripts/check-lock.mjs, check-shutdown.mjs, check-no-default-model.mjs), developed 2026-07-13 – 2026-08-12 — generalized and re-tested for publication. What was kept, generalized and stripped: PROVENANCE.md.
Used by
| Client | Role of this part there |
|---|---|
| llm-gateway | gates.json with core (lock, esm-paths, error-passthrough, unit) and full (+ packaging / render checks); docs/gates-inventory.md generated by wy-gates inventory |
| code-review-war | tiered manifest (static → PR, full → nightly, browser gates skipIf CHROME_PATH + exit-2 skips), wy-check-architecture for the src / server / shared direction and entry-file budgets |
| every other wy-ai-labs client and the parts monorepo itself | wy-check-no-path-deps, wy-check-denylist, wy-check-pattern as the shared public-safety and principle gates |
Dependencies & budget
- Runtime dependencies: 0 (budget 0) — enforced by
scripts/check-budget.mjs. - Allowed: other
@wy-ai-labs/*parts, pinned as caret ranges from the registry. Never a client, neverfile:/link:/ git URLs /../. - No private host, model id, org name or deny-list inside the part: the manifest, the architecture config and the deny-lists are the caller's files; defaults are conventions only (tier names, the 500-char error rule, exit code 2).
Gates
| Command | What | When |
|---|---|---|
| npm test | contract suite (node:test), 68 tests, offline, temp-dir fixtures, ~25 s (real child processes for the bins) | every save |
| npm run gates:core | dependency budget + independence + engines.node + tests | before every commit / PR |
| npm run gates:full | everything above (no environment-dependent gates in this part) | nightly / before release |
CI only calls these scripts (wy-ai-labs/.github → node-gates.yml). Releases: Keep a Changelog + npm publish --provenance.
License
MIT © 2026 waneekim
한국어 요약
- @wy-ai-labs/gates — 게이트 문화의 도구화: 티어(static/core/full/live)를 가진 매니페스트 기반 게이트 러너(전이적 확장, 순환 거부, env 기반 스킵, 종료코드 2 = "환경 차단" 규약)와 공용 게이트 체크·bin(lock 위생, ESM 경로, 경로 의존성, 아키텍처 방향, deny-list, 에러 전문 보존, 금지/필수 패턴). 공개 API·불변식·오류 모델은 CONTRACT.md에 고정되어 있고, 테스트가 곧 계약 스위트입니다.
- 설치
npm install --save-dev @wy-ai-labs/gates(Node ≥22, ESM).gates.json을 두고wy-gates run core(커밋·PR 전),wy-gates run full --no-stop(야간),wy-gates inventory(docs/gates-inventory.md표). 런타임 의존성 0, 사설 호스트·모델명·내장 deny-list 없음. - 원칙: 게이트는 삭제하지 않는다 — 격리·강등만. 비공개 저장소
CodeReviewWar·LLM_GATEWAY·AgentRAGKnowledge에서 추출·일반화했습니다(PROVENANCE.md). 커밋 전npm run gates:core.
