SKILL.md
review
Non-deterministic architectural check that complements the deterministic gate (bun run check — typecheck, lint, format, unit tests, skill lint). Determinism catches typos; this catches drift.
When to invoke
- After finishing any non-trivial change, before claiming "done."
- On user
/review.
Inputs
- Diff scope:
- git diff (uncommitted) AND - git diff main...HEAD if on a feature branch. - If outside a git repo, review uncommitted changes only.
CLAUDE.md.
Read both before forming any judgment. All other rules required for this review are embedded in this skill below — do not depend on any external doc.
Project layering (memorize)
A bun workspace with two packages. packages/client (@metabase/client) is the private Metabase API client, built by tsdown to a dist/ its exports map names; packages/cli (@metabase/cli) is the publishable CLI built on it, and inlines the client at build time. The e2e tier lives at the repo root under tests/e2e/ and drives the built binary; unit tests sit beside their source under packages/<p>/src/**/*.test.ts.
packages/client/src/
index.ts public barrel: client factory, error taxonomy, Credential, parseJson
client.ts createClient — composes the resource namespaces onto one transport
resources/ one file per Metabase resource; every /api/ path and transport call
http/ the HTTP boundary — transport.ts, errors.ts, network-error.ts,
response-shape.ts, sanitize.ts, retry.ts, oauth.ts
testing/ the shipped test doubles — fake-client.ts, fetch-capture.ts
auth/ OAuth login flow: credential.ts, pkce.ts, callback-server.ts, oauth-*.ts
version/ tag.ts, probe.ts, capabilities.ts, preflight-error.ts
domain/ Metabase API resource Zod schemas + inferred types
errors.ts MetabaseError taxonomy, toMetabaseError, isFileNotFoundError, errorMessage
list.ts ListResult<T> = { data, total } — what a one-response list method returns
url.ts normalizeUrl, displayUrl, assertEndpointOrigin — the only home for new URL()
json.ts parseJson — the only home for JSON.parse
poll.ts pollUntil — the wait-loop home (http/retry.ts drives its own backoff wait)
paginate.ts, signal.ts, predicates.ts, json-pointer.ts
packages/cli/src/
cli.ts entrypoint; only file allowed to call process.exit
main.ts root citty command, lazy subCommands
commands/ CLI shell — defineMetabaseCommand, flags, prompts, orchestration
core/ pure logic, no CLI deps
core/auth/ credential storage + verification
core/config.ts flag → env → stored resolver
core/env.ts readEnv — every MB_* env name, no raw process.env reads elsewhere
core/user-agent.ts USER_AGENT, the CLI's wire identity
core/npm-registry.ts the upgrade check's dist-tags lookup — the CLI's only fetch()
core/system-ca.ts trustSystemCa — the only node:tls in the repo, a CLI prerogative
output/ presentation; takes typed values; renders text or JSON
runtime/ platform glue: stdin, csv, yaml, citty helpers
Allowed import direction — the package boundary first, then within each package:
- Nothing in the client may import from the CLI — not by an
@metabase/clispecifier, not by a relative path that escapes the client's own src tree. The reverse is expected: the CLI consumes the client as@metabase/client/<path>. - The client's dependency budget is
zod+semver+node:builtins. Root devDependencies hoist, so the resolver will not reject a third dependency — a production client file importing anything else is FAIL even though it type-checks. domain/imports onlyzodand siblingdomain/files. Nothing from elsewhere in either package.resources/is imported byclient.tsand by siblingresources/files, and by nothing else. Adomain/,http/,auth/orversion/file importing a resource is FAIL — the direction runs one way, and the permitted importers are an allowlist, so a directory added later is not exempt for having arrived after the rule.commands/may importcore/,output/,runtime/, and any@metabase/client/*path.core/may importruntime/and@metabase/client/*. Nevercommands/, neveroutput/.output/may importruntime/,core/exceptcore/auth/, and@metabase/client/except@metabase/client/http/— presentation consumes the error taxonomy from@metabase/client/errors, not the HTTP boundary. Nevercommands/.runtime/may import@metabase/client/(typed errors are the project's pattern:runtime/body.tsusesparseJsonandConfigError,runtime/yaml.tsusesValidationError). It must not import fromcommands/,core/, oroutput/.
Domain-file contract
Every Metabase API resource in packages/client/src/domain/<resource-singular>.ts exports exactly two things per resource — the client's agent-facing contract:
<Resource>—z.object({ ... }).loose()for the full API shape, with the type inferred viaz.infer..loose()is the default so Metabase API additions don't break the parse — tighten over time, don't preempt. (Zod 4 —.passthrough()is deprecated; use.loose().)<Resource>Compact—<Resource>.pick({ ... }).strip()projection with its inferred type. The trailing.strip()is mandatory:.pick()on a.loose()parent inherits the loose catchall and silently passes every API field through at parse time, which means the "compact" projection isn't actually compact. A diff that lands a Compact missing.strip()is FAIL — quote<Resource>.pick({...})without trailing.strip().
Presentation is CLI-owned, never the client's: the <resource>View (ResourceView<T> with compactPick: <Resource>Compact and tableColumns: ColumnDef<T>[]) lives in packages/cli/src/output/views/<r>.ts, with any format: cell helper colocated there. A ResourceView, a ColumnDef, a tableColumns list, or a terminal-presentation format: helper appearing in packages/client/src/domain/ is FAIL. Inlining a column list inside a command is FAIL.
Naming: <Resource> is PascalCase (Card); the CLI-side <resource>View is camelCase (cardView). <Resource> is the resource name, not the file name — a file may host multiple resources (e.g. domain/user.ts → CurrentUser + CurrentUserCompact); the pair contract holds per-resource.
Adding a resource is additive: a new packages/client/src/domain/<r>.ts for the pair, packages/client/src/resources/<r>.ts for the methods, and packages/cli/src/output/views/<r>.ts for the view — consumed by commands as client.<r>.<method>(…), @metabase/client/domain/<r> and ../../output/views/<r>. A schema declared in packages/cli instead — or a command importing one by a relative path into the client's src tree — is FAIL. Editing existing commands to wire the schema is FAIL — commands import what they need. Do not require a fixture + parse-test pair — that is a tautology against Zod itself. The schema is contract-tested by the e2e tier when a command consuming it runs against the live API.
A new or modified API resource without both exports, or that omits .loose() on the full schema, or that omits .strip() after .pick() on the Compact, or that hand-rolls Array<Record<string, unknown>> / inline as { ... } casts in commands instead of consuming z.infer<typeof Schema>, is a structural failure.
Record<string, unknown> is forbidden in command files. The single exemption is packages/cli/src/output/projection.ts — the canonical structurally-untyped envelope for dot-path --fields projection over already-validated payloads.
Resource-method contract
packages/client/src/resources/<r>.ts exports <r>Resource(transport) and holds every /api/ path the client sends. packages/client/src/client.ts composes each namespace onto the client, so a resource file with no line in client.ts is unreachable and FAIL. Eight conventions bind every method:
client.<resource>.<method>(...), namespace named after the API resource (mb.card, singular). The resource is what Metabase calls the thing, not what it calls the route: the file and the namespace aresnippeteven though the endpoint is/api/native-query-snippet, because the domain schema, the CLI noun and the Metabase concept all readsnippet.- Path parameters positional, then params, then options:
update(id, params, options?). - Params use Metabase's own field names verbatim —
f,modelid,includeinactive. No mapping layer, no renaming. - Transport concerns (
signal,timeoutMs,retries) live in the trailingoptions, never in params. - Wire envelope schemas are module-private to
resources/; they never appear indomain/. - Methods return domain values, never wire envelopes. Non-paginated lists return
ListResult<T>={ data, total }. - A string path parameter always goes through
encodeURIComponent. - Every method carries the endpoint's description as a doc comment.
A method violating any of the eight is FAIL — quote the signature or the path template. Convention 7 is the one with a silent failure mode: an un-escaped string interpolated into a path template lets a / or ? in a setting key or collection ref rewrite the request.
A new or changed method comes with its wire test in packages/client/src/resources/<r>.test.ts — one it per method, asserting URL, verb, headers and body in a single toEqual over capture.calls from @metabase/client/testing/fetch-capture. That test is what pins the path and query vocabulary; a method landed without one is FAIL.
Forbidden patterns and their permitted homes
CLAUDE.md declares these as a separate policy per package, applied to production source only (*.test.ts files are exempt). A pattern's permitted home in one package says nothing about the other: JSON.parse has a home in the client and none in the CLI, fetch has a home in each but at different paths. The e2e-side restrictions below are part of this review too.
The client — every file under packages/client/src/
| Pattern | Permitted only in |
|---|---|
bare fetch(, globalThis.fetch, third-party HTTP libs (got/axios/node-fetch/undici) |
http/** |
new URL( |
http/**, url.ts |
JSON.parse( |
json.ts |
setTimeout inside an await wait loop |
poll.ts, http/retry.ts |
process.stdout.write, process.stderr.write |
nowhere — the client owns no output |
process.exit( |
nowhere — the client never ends the run |
@clack/prompts, @clack/core |
nowhere — prompting is the CLI's job |
fs, fs/promises, node:fs, node:fs/promises |
nowhere — the client touches no files |
childprocess, node:childprocess |
nowhere — the client spawns nothing |
a .ts extension in an import specifier |
nowhere |
any dependency beyond zod, semver, node: builtins |
nowhere |
an import resolving into packages/cli |
nowhere |
The CLI — every file under packages/cli/src/
| Pattern | Permitted only in |
|---|---|
process.exit( |
cli.ts |
process.stderr.write |
cli.ts, output/** |
process.stdout.write |
output/** |
JSON.parse( |
nowhere — use parseJson from @metabase/client/json |
new URL( |
nowhere — use the helpers in @metabase/client/url |
bare fetch(, globalThis.fetch, third-party HTTP libs |
core/npm-registry.ts (not a Metabase endpoint) |
setTimeout inside an await wait loop |
nowhere — use pollUntil from @metabase/client/poll |
child_process (spawn, exec, etc.) |
runtime/process.ts |
@clack/prompts, @clack/core |
output/prompt.ts |
Record<string, unknown> |
nowhere under commands/ — the walker scopes this rule to command files; output/projection.ts is the canonical untyped envelope and sits outside that scope |
an import of @metabase/client/http/ from output/ |
nowhere — presentation uses @metabase/client/errors |
a .ts extension in an import specifier |
nowhere |
process.env["MB"], process.env["METABASE"] |
core/env.ts — CLI env vars go through readEnv. A CLAUDE.md rule, not walker-enforced; judge it by reading, not by trusting the gate |
an /api/… path literal ("/api/ or ` /api/ ``) |
nowhere under commands/ — endpoints are the client's resources/ layer |
.requestParsed(, .requestRaw(, .requestStream(, paginatePages( |
nowhere under commands/ — a command calls client.<resource>.<method>(…) |
The last two are absolute: neither has a sanctioned home anywhere in the CLI. A diff that lands a command naming a path or driving the transport is FAIL.
What those two patterns bound is a path literal and a transport call, not every client function that can open a socket. commands/auth/login.ts calls tryDiscoverMetadata from @metabase/client/http/oauth, which fetches the OAuth discovery document before any credential exists and therefore before a client can be constructed; that one is sanctioned. A second such call from commands/ is FAIL — the pattern rules cannot see it, so review is the only thing that can.
Two rules bind both packages but come from elsewhere, so judge them repo-wide rather than per-package:
console.{log,warn,error,info,debug}— forbidden everywhere, includingoutput/**andtests/..oxlintrc.jsonsetsno-console: errorwith no overrides; write throughoutput/render.tsinstead.- Platform env vars (
XDGCONFIGHOME,APPDATA,npmconfigprefix) are read where they are used —core/paths.ts,core/install-method.ts,core/auth/temp-config-home.ts.readEnvowns theMB/METABASEfamily only; do not flag the others.
The test tree — every file under the repo-root tests/, outside the walker but still reviewed here
| Pattern | Permitted only in |
|---|---|
process.exit(, process.std*.write |
tests/e2e/setup/** |
/api/testing/snapshot, /api/testing/restore |
tests/e2e/setup/** — a beforeEach there restores before every test |
fetch( against Metabase |
tests/e2e/setup/** (bootstrap, oauth harness) |
child_process / execa |
tests/e2e/run-cli.ts, tests/e2e/setup/, scripts/ |
Hand-written ENOENT shape check (error.code === "ENOENT") |
nowhere — use isFileNotFoundError from @metabase/client/errors |
Inline JSON.parse(...) ; Schema.parse(...) |
nowhere — use parseJson(raw, Schema, { source }) from @metabase/client/json |
Type strictness rules (always)
- No
ascast in any form:as X,as unknown as,as never,as any. Use type guards or Zod.parse. - No
anyin declared types. - No
Record<string, unknown>used as a typed value (it is allowed only inside the structurally-untyped output envelope). - No
!non-null assertions. - No
// @ts-ignore,// @ts-nocheck,// @ts-expect-error(paired comment with// reason:and// FIXME(date)only). - Every cross-network value passes through Zod
.parsebefore downstream code touches it. - Imports are extensionless. Type-only imports use
import type.
Test-parity rules
- New
packages/cli/src/commands/<noun>/<verb>.ts⇒ atests/e2e/<noun>.e2e.test.tsexists and exercises that subcommand end-to-end viarunCli. Adding a new command without an e2e test is FAIL. - New
packages/cli/src/commands/<noun>/<verb>.ts(any leaf) ⇒packages/cli/src/runtime/command-help.test.ts's literalALL_COMMANDSlist includes the new path. Renaming or deleting a command without updating the list is FAIL. - New or changed method in
packages/client/src/resources/<r>.ts⇒packages/client/src/resources/<r>.test.tscovers it with a wire assertion overcapture.calls(URL, verb, headers, body) built oncaptureFetchfrom@metabase/client/testing/fetch-capture. A method with no wire test is FAIL — nothing else pins the path or the query vocabulary at the unit tier. - New
packages/client/src/domain/<r>.ts⇒ no fixture-and-parse-test pair required. A unit test of the formSchema.parse(fixture).toEqual(fixture)is a tautology against Zod and is FAIL if introduced. The schema's contract is tested by the e2e tier hitting the live API. - New pure helper anywhere in
packages/client/src/or in the CLI'score/,output/,runtime/⇒ a colocated unit test (<name>.test.tsbeside<name>.ts, inside the same package) imports from it. - New helper with infinite input space (URL parsing, JSON parsing, projection, byte cap, polling) ⇒ a property test (
fast-check).
E2E test contract
- E2E tests drive the built
packages/cli/dist/cli.mjsthroughrunClifromtests/e2e/run-cli.ts. Directexeca,spawn, orchild_processcalls insidetests/e2e/**/*.e2e.test.tsare FAIL. - E2E tests read admin credentials via
readBootstrap()fromtests/e2e/bootstrap-data.tsand seeded entity ids viaSEEDEDfromtests/e2e/seed/seeded.ts. Inlinefetchto Metabase, hard-coded API keys, hard-coded entity ids, or calls to/api/setupfrom a test are FAIL. - The
BootstrapZod schema andBOOTSTRAPFILEPATHconstant live only intests/e2e/bootstrap-data.ts. Re-declaring either intests/e2e/setup/bootstrap.ts(or anywhere else) is FAIL — the writer imports from the reader to prevent shape drift. --jsonoutput assertions parse through the schema imported from the package that owns it:@metabase/client/domain/<r>for a resource,../../packages/cli/src/commands/<noun>/<verb>for a command-shaped output (e.g.LoginResult,AuthStatus,<Resource>ListEnvelope). Re-declaring az.object({...})in a test that mirrors either export is FAIL.- Each test or test-group gets its own
XDGCONFIGHOMEviamkTempConfigHome(). Sharing config home across tests that mutate credentials is FAIL. - A suite whose command declares non-baseline
capabilitiesgates itself withrequireServer("<lane>", {...})fromtests/e2e/server-gate.ts, passing a lane label naming the describe or test the gate guards so an unmet gate is reported in the closing coverage block. A suite that fails instead of skipping on a server that cannot satisfy the command is FAIL.
Axes (output one row per axis)
For each axis below, output PASS, FAIL, or UNKNOWN. For every FAIL and UNKNOWN, quote the offending snippet with path:line and a one-sentence explanation.
- Layering — direction rules above, judged against the package the file is in.
- Type strictness — rules above.
- Output discipline —
console.,process.exit,process.std.writeoutside permitted homes. A file underpackages/client/src/writing to a stream or ending the process is FAIL regardless of path — the client has no permitted home for either. - Boundary discipline —
JSON.parse, rawfetch/globalThis.fetch/third-party HTTP libraries (got/axios/node-fetch/undici)/new URL,child_process, pollingsetTimeoutoutside permitted homes. Read the per-package table — the CLI has no home forJSON.parse,new URL, or a wait loop, and delegates all three to the client.
4a. Package boundary — an import in packages/client/ that resolves into packages/cli (by @metabase/cli specifier or by a relative path escaping the client's src tree) is FAIL. A production client file importing anything beyond zod, semver, and node: builtins is FAIL. A CLI file reaching into the client by relative path instead of the @metabase/client/<path> specifier is FAIL. packages/cli/src/output/ importing @metabase/client/http/ is FAIL. Anything outside client.ts and resources/ importing resources/ is FAIL. 4b. Request placement** — a file under packages/cli/src/commands/ that names an /api/… path literal, or calls requestParsed/requestRaw/requestStream/paginatePages, is FAIL. Quote the path template and name the resource method it belongs on.
- Resource contract — every new or changed resource in
packages/client/src/domain/exports<Resource>(with.loose()) and<Resource>Compact(with.pick({...}).strip()— the trailing.strip()is mandatory), and its<resource>Viewlives CLI-side inpackages/cli/src/output/views/<r>.ts(aResourceView/ColumnDef/tableColumnsin the domain file is FAIL); naming follows the convention; commands consumez.infer<typeof Schema>rather thanArray<Record<string, unknown>>or inlineas { ... }casts. Every new or changed method inpackages/client/src/resources/satisfies all eight conventions above, is composed ontoclient.ts, and returns a domain value rather than the server's envelope. - List windowing — a list command builds its envelope with one of the three helpers in
packages/cli/src/output/window.ts, picked by who applied the window:windowList(data, ctx.range, total)when the method returned the whole result set as aListResult<T>and the slice is client-side (the common case);windowServerPage(data, total, ctx.range)when the endpoint appliedlimit/offsetitself and reports a count;collectForOutput(source, view, ctx)for a genuinely paged endpoint, wheresourceis aPageSource<T>forwarding the helper'sPageRequest(max,pageSize) into the resource's<thing>Pagesmethod. A source that dropsmaxorpageSizeis FAIL — the byte budget cannot bound the walk. A hand-rolled envelope literal is FAIL — it produces a plausible shape whosehasmore/nextoffsetno test catches. A list verb missing...listFlags, or one that never readsctx.range, is FAIL: the--limit/--offsetan agent needs to page silently do nothing. - Test parity — rules above.
- Comment hygiene — no WHAT-comments; no task/PR/issue/external-path references; no comments-as-history.
- Imports / extensions — no
from "...\.(ts|js)";. Type-only imports useimport type. - TS escape hatches — no
// @ts-ignore,// @ts-nocheck,// @ts-expect-errorwithout pairedreason:+FIXME(date). - Reuse / supersession — for each new helper, grep both packages for existing helpers covering the same intent; a CLI helper duplicating something the client already exports (
parseJson,pollUntil,paginatePages,errorMessage,normalizeUrl) is the common case and is FAIL. A new helper that subsumes an older narrower one without deleting the old one is FAIL. - Type drift — a hand-written
interface/typewhose keys mirror an in-repo value's shape (could betypeof X, mapped overkeyof typeof X, or a library generic likeParsedArgs<typeof cmd.args>) is FAIL. Hand-maintained parallel definitions drift silently. - Catch-all directories — new files under
shared/,helpers/,_utils/,common/,lib/,misc/, or any other bucket-name directory is FAIL. Files must be named by purpose. - Test quality — assertions over a structure must be full (
toEqual(<full object>)/toEqual(<full array>)) — single-property pokes when the whole object is observable is FAIL (e.g. a sequence ofexpect(parsed.id).toBe(...)/expect(parsed.name).toBe(...)afterparseJsoncollapses to onetoEqual({...})). Redundant assertions (e.g. asserting.lengthbeforetoEqualof the array) is FAIL. Error assertions that check only the type or only the message — never both — is FAIL. Fixture fields that no test reads (and are not structurally required to satisfy a type) is FAIL.
- Exit codes: expect(<r>.exitCode).not.toBe(0) is FAIL. Assert the exact integer — toBe(2) for ConfigError, toBe(130) for AbortError, toBe(2) for CapabilityError, toBe(1) for everything else (HttpError, ValidationError, NetworkError, TimeoutError, UnknownError). The taxonomy lives in packages/client/src/errors.ts (CapabilityError in packages/client/src/version/preflight-error.ts); if you can't tell which code is right, the test isn't asserting the contract you think it is. - Error strings: expect(<stream>).toMatch(/.../i) for stderr / error messages is FAIL. Use toContain("<exact substring>") or toBe("<exact full string>"). The narrow exception is asserting absence of a pattern (expect(out).not.toMatch(/pattern/)). Where the message contains a dynamic value (a byte count, a generated id, a path), build the expected string from the same data the production code consumed and assert with toBe. Replacing the dynamic part with \d+ or .* in a regex is FAIL. - e2e stderr: the harness is never a TTY, so the CLI writes a JSON error envelope to stderr. expect(<r>.stderr).toBe("<message>") can never match and is FAIL — stderr carries the envelope plus any leading warn() lines. Assert through cliErrorMessage(<r>.stderr) from tests/e2e/cli-error.ts. A raw expect(<r>.stderr).toContain(...) is acceptable for a plain substring, but is FAIL when the expected message contains a quote, backslash, or newline — the envelope JSON-escapes those, so the raw substring is absent and only cliErrorMessage sees the real text.
- README parity — the root
README.mdis the user-facing command reference and must document every command inpackages/cli/src/commands/(each group exported as a top-level subcommand). Adding a new command, renaming one, removing one, or changing a flag's name/description without a matching rootREADME.mdupdate is FAIL. Adding/removing a flag inGLOBALFLAGARGS(packages/cli/src/commands/global-flags.ts, composed fromoutputFlags/profileFlag/connectionFlagsinpackages/cli/src/commands/flags.ts) without updating the common-flags section is FAIL. A change to a root script (bun run check, thee2e:*family) without updating the "Working in the repo" section is FAIL.packages/client/README.mdis the client's own reference, governed by the barrel exports inpackages/client/src/index.tsand thedomain/pair contract — adding, removing, or renaming a barrel export, or changing a documented<Resource>/<Resource>Compactsample so it diverges from the schema, without updating it is FAIL.packages/cli/README.mdis a pointer to the root one for the npm page — a diff that grows it into a second command reference is FAIL. - Fail-fast boundaries — boundary code (JSON parsing, file I/O, HTTP response handling, stdin reads) must throw or return a typed error on malformed input. Diff that introduces silent coercion to
{}/[]/"",?? ""/?? 0/?? []/?? {}to satisfy a type when the real meaning is "absent" or "error", emptycatch {}blocks, ortry/catchthat swallows the error and returns a default — is FAIL. Quote the offending site. - Magic literals — diff introduces a numeric or string literal that already appears elsewhere in the repo without a shared constant (timeouts, byte caps, exit codes, profile names, account identifiers, file modes). Grep both packages for the literal before deciding; one occurrence is fine, two without a constant is FAIL.
- Boolean traps — diff adds a function with 2+ boolean parameters, or a call site with 2+ positional boolean arguments where intent is non-obvious from the call. Use a named-options object or split functions. FAIL.
- Type-guard validity — a function declared
(value): value is Tmust validate the property that distinguishesT.instanceof Errornarrowing to a more specific error subtype, or any guard whose body cannot rule out a non-Tinput, is a hidden cast and FAIL. - Eloquence — diff introduces ceremony that has no real-world counterpart. Stacked
override readonlychains where a plain assignment would do, intermediate abstract classes that exist solely to deduplicate four lines, generic parameters that don't earn their complexity, option-bag wrappers around single-arg calls — FAIL. The test: would two engineers writing this from scratch both reach for this pattern? If no, it's overengineered. Quote the offending construct and a simpler alternative. - Expression complexity — diff introduces a big inline boolean or ternary chain in an
if,return, or assignment. Multi-clause&&/||mixes,?.ladders combined with??fallbacks, or nested ternaries (a ? x : b ? y : z) inside a conditional are FAIL. Each meaningful clause should be a named local (const hasBudget = …; const isFresh = …). The condition in theifshould read as one phrase, not a puzzle. Quote the offending site. - Mocking discipline —
vi.mock(...)is a last resort. Diff introduces a module mock to test a one-line wrapper / one-line delegation, or to substitute logic owned by the system under test, is FAIL — that test asserts only what the mock was configured to return (a tautology). Acceptable: hermetic isolation of side-effecting external deps in integration tests where the real path pollutes the host (e.g.@napi-rs/keyring, real network, real subprocess), or fixture injection into a fully-exercised pipeline. Not acceptable: mocking an internal module so a unit test can fire a single branch — instead prefer a real value flowing through real imports (realZodError, realHttpError, real Zod schema), or push the coverage to the smoke/integration tier. Prefer integration over isolation when in doubt. Quote anyvi.mockcall and the one-line wrapper it shadows. - E2E CLI invocation discipline — under
tests/e2e/**/*.e2e.test.ts, any direct call toexeca,spawn,exec, orchild_processis FAIL — e2e tests drive the binary throughrunClifromtests/e2e/run-cli.ts. Inlinefetch(<metabase-url>...)from a test file is FAIL — credential setup belongs intests/e2e/setup/bootstrap.tsand tests read state viareadBootstrap(). - Bootstrap-data unity — the
BootstrapZod schema (the.bootstrap.jsonshape) and theBOOTSTRAPFILEPATHconstant must live only intests/e2e/bootstrap-data.ts. Any redeclaration of the same shape elsewhere (typically intests/e2e/setup/bootstrap.ts) is FAIL — writer and reader share one source. - Test schema reuse — when a production module exports a Zod schema describing CLI output, tests must import it from wherever it lives. Schemas that fall under this rule include: per-command output schemas (e.g.
LoginResult,AuthStatus) and every<Resource>ListEnvelopeexported frompackages/cli/src/commands/<noun>/list.ts(built vialistEnvelopeSchema(<Resource>Compact)frompackages/cli/src/output/types.ts), plus every<Resource>/<Resource>Compactinpackages/client/src/domain/. A test file that declares az.object({...})mirroring the same fields as a known export — including hand-rolling a list-envelope shape ({ data, returned, offset, limit, total, hasmore, nextoffset, truncated }) instead of importing the command's<Resource>ListEnvelope— is FAIL. Quote both the test declaration and the canonical export. - Helper reuse in tests — test code reimplementing a helper the client already exports (e.g. inline
JSON.parse+Schema.parseinstead ofparseJsonfrom@metabase/client/json;error.code === "ENOENT"instead ofisFileNotFoundErrorfrom@metabase/client/errors; deadline+sleep loops instead ofpollUntilfrom@metabase/client/poll) is FAIL. Test files are allowed to import these across the layering and package boundaries because the layering rules apply to production source only. - License-token discipline — diff that introduces (a) a hardcoded real EE token (any string matching
^mb(dev[0-9a-f]{57}|[0-9a-f]{64})$outside a clearly-fake placeholder likembdev0123…), (b)console.log/process.std.write/Bash(echo $...)/Read(.env)ofMBPREMIUMEMBEDDINGTOKENorMETASTOREDEVSERVERURLor any binding holding their value, (c) writing the token to a file outside the test process's stdin pipe, (d) an assertiontoContain/toEqual/toBethat compares against the token string is FAIL. Acceptable:process.env["MBPREMIUMEMBEDDING_TOKEN"] === ""for skip-gating;runCli({ stdin: token })to thread the token opaquely. Quote the offending site.
Output format
| # | Axis | Status | Detail |
|---|------|--------|--------|
| 1 | Layering | PASS | |
| 2 | Type strictness | FAIL | packages/cli/src/commands/card/get.ts:12 — `as unknown as Card` |
| 3 | Output discipline | PASS | |
...
Then exactly one of:
RESULT: PASS — task may be marked done.RESULT: FAIL (<n> failures, <m> unknowns) — fix issues above before marking done.
If FAIL, follow the table with a bulleted list of the next concrete actions to address each failure (one bullet per FAIL, with path:line).
Rules for the review itself
- Read-only. Do not modify any files.
- Do not run
tsc,oxlint,oxfmt, or the test suites here. Those are separate; this skill is the architectural / non-deterministic layer. - Do not green-light by default. If you cannot determine an axis from the diff, mark
UNKNOWNwith the reason — never PASS. - Be ruthless. The point of this skill is to be strict where the type-checker can't be.