{"owner":"MaterializeInc","repo":"materialize","hasSkills":true,"totalSkillsCount":72,"totalTokensCount":43201,"categories":["anthropic-skill","root-instruction","claude-rule","command-prompt","plugin-manifest"],"hasMcp":true,"mcpConfig":{"mcpServers":{"materialize":{"command":"npx","args":["-y","@modelcontextprotocol/server-materialize"]}}},"found":[".agents/skills/mz-debug-ci/SKILL.md",".agents/skills/mz-benchmark/SKILL.md",".agents/skills/mz-console-review/SKILL.md",".agents/skills/mz-dbt-release/SKILL.md",".agents/skills/mz-adapter-guide/SKILL.md",".agents/skills/mz-commit/SKILL.md",".agents/skills/mz-dependencies/SKILL.md",".agents/skills/mz-parallel-workload/SKILL.md",".agents/skills/mz-limits-test/SKILL.md",".agents/skills/mz-feature-flags/SKILL.md",".agents/skills/mz-platform-checks/SKILL.md",".agents/skills/mz-pr-review/SKILL.md",".agents/skills/mz-profile/SKILL.md",".agents/skills/mz-query-perf/SKILL.md",".agents/skills/mz-query-tracing/SKILL.md",".agents/skills/mz-release-signoff/SKILL.md",".agents/skills/mz-run/SKILL.md",".agents/skills/mz-test/SKILL.md","AGENTS.md","CLAUDE.md","console/AGENTS.md","console/CLAUDE.md",".agents/skills/README.md",".claude/commands/update-docs.md",".agents/skills/mz-console-review/cost-and-structure.md",".agents/skills/mz-debug-ci/bk-fallback.md",".agents/skills/mz-debug-ci/rare-failures.md","ci/plugins/cloudtest/README.md","ci/plugins/mzcompose/README.md","ci/plugins/scratch-aws-access/README.md",".agents/skills/mz-release-signoff/references/adapter.md",".agents/skills/mz-release-signoff/references/compute.md",".agents/skills/mz-release-signoff/references/persist.md",".agents/skills/mz-release-signoff/references/reference-dashboards.md",".agents/skills/mz-release-signoff/references/sources-and-sinks.md","doc/developer/generated/compute/extensions/_module.md","doc/developer/generated/compute/extensions/arrange.md","doc/developer/generated/compute/extensions/reduce.md","doc/developer/generated/compute/extensions/temporal_bucket.md","doc/developer/generated/mz-deploy/cli/commands/_module.md","doc/developer/generated/mz-deploy/cli/commands/abort.md","doc/developer/generated/mz-deploy/cli/commands/apply_all.md","doc/developer/generated/mz-deploy/cli/commands/apply_connections.md","doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md","doc/developer/generated/mz-deploy/cli/commands/apply_objects.md","doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md","doc/developer/generated/mz-deploy/cli/commands/apply_sources.md","doc/developer/generated/mz-deploy/cli/commands/apply_tables.md","doc/developer/generated/mz-deploy/cli/commands/clean.md","doc/developer/generated/mz-deploy/cli/commands/clusters.md","doc/developer/generated/mz-deploy/cli/commands/compile.md","doc/developer/generated/mz-deploy/cli/commands/debug.md","doc/developer/generated/mz-deploy/cli/commands/delete.md","doc/developer/generated/mz-deploy/cli/commands/describe.md","doc/developer/generated/mz-deploy/cli/commands/dev.md","doc/developer/generated/mz-deploy/cli/commands/explain.md","doc/developer/generated/mz-deploy/cli/commands/grants.md","doc/developer/generated/mz-deploy/cli/commands/list.md","doc/developer/generated/mz-deploy/cli/commands/lock.md","doc/developer/generated/mz-deploy/cli/commands/log.md","doc/developer/generated/mz-deploy/cli/commands/mcp.md","doc/developer/generated/mz-deploy/cli/commands/new_project.md","doc/developer/generated/mz-deploy/cli/commands/profile.md","doc/developer/generated/mz-deploy/cli/commands/promote.md","doc/developer/generated/mz-deploy/cli/commands/roles.md","doc/developer/generated/mz-deploy/cli/commands/setup.md","doc/developer/generated/mz-deploy/cli/commands/setup_schema.md","doc/developer/generated/mz-deploy/cli/commands/sql.md","doc/developer/generated/mz-deploy/cli/commands/stage.md","doc/developer/generated/mz-deploy/cli/commands/test.md","doc/developer/generated/mz-deploy/cli/commands/wait.md","doc/developer/generated/mz-deploy/cli/commands/test/lower.md"],"skills":{".agents/skills/mz-debug-ci/SKILL.md":"---\nname: mz-debug-ci\ndescription: >\n  Investigate CI failures on PR via gh + Buildkite MCP or bk CLI. Trigger: failing checks,\n  Buildkite failures, CI issues — \"why is CI red\", \"build broken\",\n  \"checks failing\", \"what went wrong in CI\", \"nightly broke\",\n  \"tests failing on this PR\", or pasted Buildkite URL. Also PR number +\n  why failing.\nargument-hint: <PR number, GitHub PR URL, or Buildkite build URL>\n---\n\nInvestigate CI failures for a Materialize PR or Buildkite build.\n\nIf the input is a Buildkite build URL rather than a PR (a scheduled nightly on\nmain, a release-qualification build), start directly at \"Listing a\nbuild's failed jobs directly\", taking `<PIPELINE>` and `<BUILD_NUMBER>`\nstraight from the URL:\n`https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>`,\nthen continue from Step 2.\n\n## Prerequisites\n\nThis skill requires `gh` (GitHub CLI), authenticated, plus one of two ways\nto reach Buildkite: the Buildkite MCP server's tools (preferred, next\nsection) or `bk` (Buildkite CLI), authenticated. If any of these turns out\nto be missing or unauthenticated, **stop immediately** and tell the user\nwhat to fix (`bk configure` or `bk auth login` for the Buildkite CLI, or the\nMCP setup below). Do not attempt to use the REST API directly or any other\nworkaround.\n\n## Buildkite MCP: preferred when available\n\nWhen the Buildkite MCP server's tools are available in the session (tool\nnames like `get_build_failure_summary`, `search_logs`, ...), prefer them\nover `bk` everywhere below: structured JSON with the\njob-state semantics already applied, and no log cleanup needed. The server documents its\nown tools; this section adds only what it does not know:\n\n- `org_slug` is `materialize`. `get_build_failure_summary` replaces \"Listing\n  a build's failed jobs directly\", Step 2, and often Step 3;\n  `search_logs`/`read_logs`/`tail_logs` replace the `bk job log` pipelines;\n  the artifact and annotation tools replace the corresponding `bk` commands.\n  Everything Materialize-specific (annotation contents, `bin/ci-failures`,\n  `bin/ci-shards`, Steps 4-5) applies unchanged.\n- The summary's job list is bounded by the response's `job_limit` (10 on the\n  hosted server, where a larger `max_jobs` is silently clamped), with terminal\n  problem jobs sorted before downstream broken ones. On `jobs_truncated:\n  true`, get the definitive failed-job list from `list_jobs` with\n  `state: \"failed,timed_out\"` and `include_retried_jobs: false`. That last\n  parameter is what preserves triage semantics: the server defaults it to\n  true, which counts every failed attempt of a retried job separately,\n  including jobs that passed on retry. The summary bounds annotation content\n  too: on `content_truncated: true`, fetch the full set with\n  `list_annotations`.\n\nIf the MCP is absent, read `bk-fallback.md` in this skill's directory for\nthe `bk` command recipes and continue with those. At the end, mention to\nthe user that they can set the server up (hosted read-only endpoint:\n`claude mcp add --scope user --transport http buildkite https://mcp.buildkite.com/mcp/readonly`;\nit saves tool calls but costs somewhat more tokens).\n\n## Search existing failures by pattern\n\nSearch for already recorded failures in CI with a short, stable error\nsubstring. The output is JSON whose `content` blobs are huge, so project it\ndown, and read the total from `.meta.totalRowCount`, which ignores the row\ncap and is the \"how chronic is this?\" number:\n\n```bash\nbin/ci-failures 'foobar' 2>/dev/null | jq -r '.meta.totalRowCount,\n  (.data[] | [.build_date[0:10], .build_identifier, .test_suite, .issue] | @tsv)'\n```\n\nThe positional pattern matches failure content. `--search <text>` replaces\nthe positional pattern and matches across build, test, issue, content, and\nbranch at once. Narrow results with `--branch main`, `--version`, `--issue`,\n`--test` (the Buildkite job name), `--build`, `--start-date`, or\n`--end-date`; `--size` raises the row cap (default 31, max 100).\n\n## Step 1: Find the build\n\nParse `$ARGUMENTS` to get the PR number (a plain number or a\n`https://github.com/MaterializeInc/materialize/pull/<PR_NUMBER>` URL), then\nlist the failing checks:\n\n```bash\ngh pr checks <PR_NUMBER> 2>&1\n```\n\nFailing rows link to\n`https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>#<JOB_ID>`,\nthe identifiers used throughout this skill.\n\n### Listing a build's failed jobs directly\n\nBuildkite mirrors each job's result into GitHub as a\n`buildkite/<pipeline>/<job>` commit status (never-dispatched jobs post\nnothing), but `gh pr checks` reads the PR's current head commit only. A build\ntherefore shows up there only if it ran on exactly that commit. For everything\nelse (scheduled nightlies on main, release-qualification builds, a PR nightly\nfrom before the latest push), list the genuinely failed jobs via the API:\n`list_jobs` as described in the MCP section, or with `bk` the listing recipe\nin `bk-fallback.md`. Either way, also grab the build's commit and branch:\nStep 5's known-vs-new checks need the commit, and neighboring-build lookups\nneed the branch.\n\nJob-state semantics matter here:\n- The failure states are `failed` and `timed_out`, the same set as\n  `BUILDKITE_RELEVANT_FAILED_BUILD_STEP_STATES` in\n  `misc/python/materialize/buildkite_insights/buildkite_api/buildkite_constants.py`.\n- `broken` is not a failure. Buildkite marks a job `broken` when its\n  configuration prevents it from running, for example a branch filter or an\n  `if:` condition that evaluates false, so the job never dispatches. Every\n  build contains broken jobs (a routine main nightly has dozens), so counting\n  them massively over-counts failures.\n- Jobs with `soft_failed: true` are allowed to fail without making the build\n  red; no current CI step sets it, so filtering it is defensive.\n- `waiting_failed` is not a failure either: the job's dependency (usually a\n  build step) failed, so it never started. Its presence is informative,\n  though. A PR build full of `waiting_failed` jobs ran no tests at all, so\n  report that fixing the visible failures is necessary but no proof the\n  build then goes green.\n- Retried jobs (automatic or manual) appear only as their latest attempt, so\n  a job that failed and then passed on retry counts as passed. That is the\n  right verdict for build triage; fetch the earlier attempts only when an\n  in-depth investigation needs them. A manual retry that failed again is\n  itself a signal: someone already tried to shake the failure off and it\n  reproduced.\n\nTo find a PR's builds that are not on its current head in the first place (an\nolder nightly, a pre-push run), get the branch with\n`gh pr view <PR_NUMBER> --json headRefName` and filter Buildkite builds by\nbranch; the MCP's `list_builds` takes a `branch:` filter, and fork branches\nare named `<owner>:<branch>` on Buildkite.\n\n## Step 2: Check annotations first\n\n**Before diving into logs**, fetch the build annotations. They contain pre-extracted error messages, stack traces, and links to known flaky test issues — this saves significant time compared to grepping through raw logs. With the MCP they arrive in `get_build_failure_summary` (or `list_annotations`); with `bk`, use the annotations recipe in `bk-fallback.md`.\n\nBesides the error itself, an error annotation (`style` `\"error\"`) carries\nknown-issue links (Linear keys like `CPU-170`, or legacy\n`database-issues/#NNNN`) and the job's main-branch history, a flaky-test\nindicator.\n\nThings to know when reconciling annotations against jobs:\n\n- Error annotations persist from failed attempts even when a retry later\n  passed, so a build can have more error annotations than failed jobs. A\n  `bin/ci-annotate-errors` annotation's `context` field is `<JOB_ID>-error`,\n  and that job id can belong to a retried attempt that the default job\n  listing does not contain. (Other producers use other contexts, e.g.\n  `images-not-public` from the build step.)\n- Only jobs running through the mzcompose/cloudtest plugins get annotations\n  and `bin/ci-failures` rows (both come from `bin/ci-annotate-errors`). Plain\n  shell steps (lint, Security advisories, and similar checks) produce neither,\n  so a real failure there has no annotation at all. Go straight to its job\n  log.\n- Info-style annotations (\"<job> succeeded with known error logs\") mean the\n  job passed and every logged error matched an open known issue; they are\n  not failures. When a job went red via \"Test succeeded, but unknown errors\n  found in logs\" (see Step 3), the unknown and potential-regression entries\n  in its annotation are what turned it red. The reverse also exists: a\n  failed job turns green (\"<job> would have failed with known error logs\")\n  when all its logged errors match issues marked `ci-ignore-failure: true`.\n\n## Step 3: Fetch logs when needed\n\nOnly fetch full logs when annotations don't provide enough detail. On PR test\nbuilds, read compile and lint job logs first (clippy, lint-and-rustfmt): they\noften explain every downstream failure.\n\nFetch and search logs with the MCP's `search_logs`/`read_logs`/`tail_logs`,\nor with `bk` use the job-log recipes in `bk-fallback.md`. The log tail can be a\nred herring: some jobs print long non-error output after the actual error\n(cargo deny's dependency tree runs hundreds of lines past the advisory), so\nwhen the tail shows no error, search or grep for the error instead of\ntailing more.\n\nNOTE: for mzcompose-based jobs (testdrive, SQLsmith/SQLancer, platform checks,\n...) the job console log holds only the harness's output. The services' own\noutput (environmentd/clusterd panics and errors) goes to `services.log` and\nsimilar files inside the job's log artifacts. A panic that is absent from\nthe job's console log can still be the failure, so never conclude \"this\nerror did not happen in this run\" from it alone. Check the annotations or\n`bin/ci-failures` instead, both are fed by `bin/ci-annotate-errors`, which\nscans the uploaded log artifacts (`services.log`, `run.log`, junit XML, ...)\nat the end of each job. The same scan can fail a job whose own workflow\npassed: the console log then reads `Test succeeded, but unknown errors found\nin logs, marking as failed`, and the cause is whatever the scan found in the\nartifacts, not a test failure. Alternatively, download the log artifacts and\ngrep them directly.\n\n### Artifacts\n\nJobs upload artifacts (junit XML, service logs, coredumps, ...). Use the\nMCP's `list_artifacts_for_build`/`list_artifacts_for_job`/`get_artifact`,\nor with `bk` use the artifacts recipes in `bk-fallback.md`.\n\n### Shard contents\n\nSharded jobs (SLT, testdrive, platform checks, feature benchmark, ...) record\nthe workflows/files/scenarios they ran in build meta-data. `bin/ci-shards`\nshows it:\n\n```bash\n# Mapping of every sharded job to what it ran, with a link per job whose\n# `#` fragment is the `<JOB_ID>` for fetching job logs (omit links with --no-url)\nbin/ci-shards https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>\n\n# Which job(s) ran a specific item (test file, scenario, or workflow,\n# `workflow_foo_bar` function names are matched as `foo-bar`)\nbin/ci-shards https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER> numeric.td\n```\n\n## Step 4: Categorize failures\n\nUse these Materialize-specific patterns to diagnose:\n\n### Clippy errors\nCode lint issues in changed files. Fix the code, not the lint config.\n\n### `check-test-flags` lint failure\nA new dyncfg (`Config::new(\"name\", ...)`) was introduced but not registered in the required places. Reproduce with `bin/lint-test-flags`.\n- `misc/python/materialize/parallel_workload/action.py` (`FlipFlagsAction.flags_with_values` or `uninteresting_flags`)\n- `misc/python/materialize/mzcompose/__init__.py` (get_variable_system_parameters / get_minimal_system_parameters / UNINTERESTING_SYSTEM_PARAMETERS)\n\n### Doctests failure\nThe Doctests job runs `bin/doc --document-private-items` (`ci/test/lint-doc.sh`), which sets `RUSTDOCFLAGS=-D warnings` and uses the pinned nightly. Reproduce on one crate with `bin/doc --document-private-items -p <crate>`. A common cause is a public doc comment linking a private item (`rustdoc::private_intra_doc_links`), which fails even with `--document-private-items`. `cargo doc` stops at the first failing crate, so a fix can reveal errors in the next crate.\n\n### Merge skew cargo check failure\nThis PR-only job (`ci/test/check-merge-with-target.sh`) merges the PR with its target branch and runs `cargo check --all-targets`. When it is red while the PR's own build and clippy jobs pass, the branch compiles alone but not merged with current main. Rebase and fix as the mz-commit skill describes.\n\n### Testdrive cascades\nAfter one test crashes environmentd, all subsequent tests in that shard fail with `Name or service not known` or `connection closed`. **Only the first failure in a shard matters** — everything after it is a cascade. Look for the first `error:` or `FAIL` in the log.\n\nTestdrive shards with the same number (e.g., `testdrive-10` and `testdrive-with-alloydb-10`) run the same tests — if both fail, it's likely to be the same root cause.\n\n### SLT failures\nCheck whether it's wrong output (behavioral change) vs. connection error (crash/timeout). Wrong output means the query semantics changed. An `InconsistentViewOutcome` failure comes from CI's `--auto-index-selects` mode; the mz-test skill explains the mechanism and the exemption lists.\n\n### Timeouts\nA `timed_out` job hit its step budget (`timeout_in_minutes` in\n`ci/<pipeline>/pipeline.template.yml`). The annotation records only the fact\n(\"test timed out\"), never the cause. Find the cause in the console log: the\nlast completed unit of work, plus anything marked as still running when the\njob was canceled (nextest prints `SLOW [>1200.000s]` lines naming the stuck\ntests). If no such marker appears, compare wall-clock against the job's last\npassing run to tell a gradually outgrown budget from a hung test.\n\n### Infrastructure flakes\nObserved: failures in unrelated jobs while downloading from `github.com/.../releases/download/` were GitHub CDN outages. Retry the job instead of debugging it.\n\n### Rarer classes\n\nIf the failure matches one of these, read `rare-failures.md` in this\nskill's directory for its triage pattern: `New regression against\n<version>` (feature benchmark), a cargo-fuzz crash, a Miri failure, a\nlimits-test failure, or `ImagesNotPublicError`.\n\n## Step 5: Summarize\n\nGroup failures by **root cause**, not by job name — typically many failing\njobs share just 1-2 root causes. Per root cause, report a description, the\naffected jobs, and what to fix.\n\nDistinguish between issues that are clearly caused by the change under test vs. pre-existing flaky tests. The annotations often link to known flaky test issues — use these to identify pre-existing flakes vs. new regressions.\n\nMore ways to establish known vs new:\n\n- Check whether main already fixed it. Any build can run code that is behind\n  main: a nightly investigated the morning after, but also a fresh PR based\n  on an older main. From an up-to-date main checkout run\n  `git log <BUILD_COMMIT>..HEAD -- <suspect-file>` or\n  `git log -S '<error token>'`. A fix can also sit in a not-yet-merged PR,\n  invisible to git log: search open PRs for the step name or error token\n  (`gh pr list --state open --search '<...>'`); a PR's file list shows\n  which of the build's root causes it covers. Do this for every failure,\n  and name any hit in the summary so nobody authors a duplicate. When\n  citing a later build as evidence\n  of a fix, confirm the specific job's state there is `passed`: a build can\n  be green because the job was `broken` and never ran.\n- Known-issue tracking lives in Linear. Annotations and `bin/ci-failures`\n  cite issue keys like `CPU-170` or `SS-361`, which resolve to\n  `https://linear.app/materializeinc/issue/<KEY>`. Legacy references point at\n  GitHub `MaterializeInc/database-issues` instead.\n- Annotations and `bin/ci-failures` classify a failure three ways; for the\n  latter two, check the signature's history with `bin/ci-failures` before\n  treating the failure as new. A known-issue link means it matched an open\n  tracked issue. `\"UNKNOWN ERROR\"` means it matched no tracked issue, not\n  that the failure is new: a long streak under that label is a chronic\n  untracked flake worth filing. An issue reference tagged\n  `(POTENTIAL REGRESSION)` means it matched a tracked issue that is now\n  closed: a known signature with stale tracking, not by itself evidence of\n  a new regression; suggest to the user that the closed issue may need\n  reopening. The issue matching is a content heuristic and often wrong, so\n  verify that the matched issue actually describes the problem: search the\n  tracker for the error text, since the right issue may already exist open,\n  or the problem may need a totally new one.\n",".agents/skills/mz-benchmark/SKILL.md":"---\nname: mz-benchmark\ndescription: >\n  Add/modify/debug Materialize perf benchmark scenarios. Three frameworks:\n  Feature Benchmark (single-op micro), Scalability Test (SQL throughput under\n  concurrency), Parallel Benchmark (sustained latency via scenarios.py).\n  Trigger: \"benchmark\", \"feature benchmark\", \"scalability test\",\n  \"parallel benchmark\", \"performance regression\", \"micro-benchmark\", \"TPS\",\n  \"latency test\", or edits in feature_benchmark/scenarios/,\n  scalability/workload/workloads/, parallel_benchmark/scenarios.py.\n  Note: measurement, not panic-stress (see mz-parallel-workload).\n---\n\n# Benchmark Frameworks\n\nMaterialize has some benchmark frameworks targetting local Docker, each suited to different performance concerns. Choose the right one based on what you're measuring.\n\n## Decision Guide\n\n| Concern | Framework | Best For |\n|---------|-----------|----------|\n| Single-operation latency | **Feature Benchmark** | \"How fast is this SELECT / INSERT / CREATE INDEX?\" |\n| SQL throughput under concurrency | **Scalability Test** | \"How many QPS can we sustain at 1/4/16/64/256 concurrent clients?\" |\n| Sustained performance over time | **Parallel Benchmark** | \"Does latency degrade over a 2-minute mixed workload?\" |\n\nA Rust `cargo bench` target must reference the production allocator with a\n`mz-alloc-default` dev-dependency and `use mz_alloc_default as _;` (see\n`src/repr/benches/row.rs`). Without it the bench runs on the system allocator,\nnot jemalloc.\n\nScalability Test and Parallel Benchmark connect with psycopg 3 and leave\n`prepare_threshold` at its default of 5, so a query switches to a prepared\nstatement on its sixth execution per connection. Set it explicitly and warm\nconnections up when per-query latency matters.\n\n---\n\n## 1. Feature Benchmark (Micro-benchmarks)\n\nMeasures wall-clock time of individual SQL operations without concurrency. Uses testdrive fragments. Great for regression-testing specific code paths.\n\n### How to Add a Scenario\n\n1. Add a class in one of the `scenarios/*.py` files. Extend `Scenario` (default, runs in CI), `ScenarioBig` (longer, opt-in), or `ScenarioDisabled`.\n2. Implement `benchmark()` returning a `Td(...)` with timing markers (`/* A */` and `/* B */`).\n3. Optionally implement `shared()`, `init()`, `before()` if setup is required.\n\n### Scenario Template\n\n```python\nfrom materialize.feature_benchmark.action import TdAction\nfrom materialize.feature_benchmark.measurement_source import MeasurementSource, Td\nfrom materialize.feature_benchmark.scenario import Scenario\n\nclass MyCategory(Scenario):\n    \"\"\"Group parent - won't run (not a leaf).\"\"\"\n    pass\n\nclass MySpecificBenchmark(MyCategory):\n    \"\"\"Leaf class - this runs.\"\"\"\n\n    def init(self) -> list[TdAction]:\n        return [\n            self.table_ten(),  # Creates table 'ten' with values 0-9\n            TdAction(\"\"\"\n                > CREATE TABLE t1 (f1 INTEGER);\n                > INSERT INTO t1 SELECT {unique_values} FROM {join};\n                > SELECT COUNT(*) FROM t1;\n                {n}\n            \"\"\".format(\n                unique_values=self.unique_values(),\n                join=self.join(),\n                n=self.n(),\n            )),\n        ]\n\n    def benchmark(self) -> MeasurementSource:\n        return Td(\"\"\"\n> SELECT 1;\n  /* A */\n1\n\n> SELECT COUNT(*) FROM t1 WHERE f1 % 2 = 0;\n  /* B */\n{half_n}\n\"\"\".format(half_n=self.n() // 2))\n```\n\n### Timing Markers\n\n- `/* A */` - Start marker. Measurement begins AFTER this completes.\n- `/* B */` - End marker. Measurement ends when this completes.\n- Measures: time from end of A to end of B.\n- For a single query, use `SELECT 1; /* A */` as a dummy start to not measure the overhead of Testdrive starting up.\n\n### Scenario Phases (execution order)\n\n1. **`shared()`** - Runs once for BOTH instances (e.g., create Kafka topics)\n2. **`init()`** - Runs once per instance (create tables, populate data)\n3. **`before()`** - Runs before EACH iteration (restart, cleanup)\n4. **`benchmark()`** - Runs repeatedly, measured\n\nThe result is the minimum (`MinAggregation`) after discarding the first 3\nmeasurements. `ProbForMin(threshold=0.90)` in\n`test/feature-benchmark/mzcompose.py` can end a scenario after 6 measurements,\nso the verdict can rest on 3 values and one outlier can flip it. Read the raw\nper-iteration values before trusting a regression or improvement.\n\nThe measured cluster `cluster_default` is an unmanaged replica on the\n`clusterd` container, configured by the `Clusterd(...)` service in\n`create_clusterd_service`. That function maps `enable_unified_cluster` onto\nthe service, but other parameters that shape the replica process do not reach\nit. For example, `enable_timely_zero_copy` has no effect because `Clusterd`\nhardcodes its timely config.\n\n### Helpers\n\n- `self.n()` - Returns 10^SCALE (default SCALE=6 → 1,000,000)\n- `self.table_ten()` / `self.view_ten()` - Creates table/view with values 0-9\n- `self.unique_values()` - SQL expression generating N unique values via cartesian product\n- `self.join()` - SQL FROM clause for cartesian product joins\n- `self.keyschema()` / `self.schema()` - Kafka schema helpers\n\n### Tips\n\n- **Aim for ~1 second** query time. Too fast = noise; too slow = Unbearably slow CI.\n- **Test td fragments locally** with standalone testdrive before running the full benchmark.\n- **Use `SCALE`** to control data volume: `SCALE = 5` → 100K rows.\n- **Use `FIXED_SCALE = True`** if `--scale` should not affect your scenario. This is usually done when a scenario doesn't scale well, add an explanation.\n- **Use `RELATIVE_THRESHOLD`** to customize regression sensitivity per scenario.\n\n### Running\n\n```bash\nbin/mzcompose --find feature-benchmark run default --root-scenario=MyCategory\nbin/mzcompose --find feature-benchmark run default --root-scenario=MySpecificBenchmark\n```\n\n---\n\n## 2. Scalability Test (SQL Throughput Under Concurrency)\n\nMeasures TPS (transactions per second) at increasing concurrency levels (1, 2, 4, 8, ..., 256 clients). Compares Materialize against Postgres.\n\nThe load is closed-loop: `concurrency` threads each issue the next operation\nwhen the previous one completes (`benchmark_executor.py`). TPS is therefore\ncapped at concurrency divided by mean latency, and a flattening curve can be\nthe client cap rather than a server knee. Use Parallel Benchmark's `OpenLoop`\nto measure behavior at a fixed offered load, and report p99 and max with it.\n\n### How to Add a Workload\n\n1. Create a new file or add to an existing file in `workload/workloads/`.\n2. Extend a marker class (`DmlDqlWorkload`, `DdlWorkload`, or `ConnectionWorkload`).\n3. Implement `operations()` returning a list of `Operation` objects.\n\n### Workload Template (Simple DML/DQL)\n\n```python\nfrom materialize.scalability.operation.operations.operations import (\n    SelectOne,\n    InsertDefaultValues,\n    SelectCountInMv,\n)\nfrom materialize.scalability.workload.workload_markers import DmlDqlWorkload\n\nclass MyWorkload(DmlDqlWorkload):\n    def operations(self) -> list[\"Operation\"]:\n        return [InsertDefaultValues(), SelectCountInMv()]\n```\n\n### Workload Template (DDL with Data Exchange)\n\n```python\nfrom materialize.scalability.operation.operation_data import OperationData\nfrom materialize.scalability.operation.operations.operations import (\n    CreateTableX, PopulateTableX, SelectStarFromTableX, DropTableX,\n)\nfrom materialize.scalability.operation.scalability_operation import OperationChainWithDataExchange\nfrom materialize.scalability.workload.workload import WorkloadWithContext\nfrom materialize.scalability.workload.workload_markers import DdlWorkload\n\nclass MyDdlWorkload(WorkloadWithContext, DdlWorkload):\n    def amend_data_before_execution(self, data: OperationData) -> None:\n        data.push(\"table_seed\", data.get(\"worker_id\"))\n\n    def operations(self) -> list[\"Operation\"]:\n        return [\n            OperationChainWithDataExchange([\n                CreateTableX(), PopulateTableX(), SelectStarFromTableX(), DropTableX(),\n            ])\n        ]\n```\n\n### Custom Operations\n\n```python\nfrom materialize.scalability.operation.scalability_operation import SimpleSqlOperation\n\nclass MyCustomSelect(SimpleSqlOperation):\n    def sql_statement(self) -> str:\n        return \"SELECT COUNT(*) FROM t1 WHERE f1 > 500;\"\n```\n\n### Built-in Operations\n\n`SelectOne`, `SelectStar`, `SelectLimit`, `SelectCount`, `SelectCountInMv`, `SelectUnionAll`, `Update`, `InsertDefaultValues`, `CreateTableX`, `PopulateTableX`, `DropTableX`, `CreateMvOnTableX`, `DropMvOfTableX`, `Connect`, `Disconnect`.\n\n### Database Schema (auto-created)\n\nFor each workload, the framework creates: `CREATE TABLE t1 (f1 INTEGER DEFAULT 1)`, inserts a row, and creates `CREATE MATERIALIZED VIEW mv1 AS SELECT count(*) AS count FROM t1`.\n\n### Running\n\n```bash\nbin/mzcompose --find scalability run default --workload MyWorkload\nbin/mzcompose --find scalability run default --workload-group-marker DmlDqlWorkload\nbin/mzcompose --find scalability run default --min-concurrency 1 --max-concurrency 128\n```\n\n---\n\n## 3. Parallel Benchmark (Sustained Performance)\n\nMeasures latency distributions (p50/p95/p99) and QPS over a sustained load period (default 120s). Supports mixed workloads with both open-loop and closed-loop concurrency. Detects latency degradation over time via slope analysis.\n\n### How to Add a Scenario\n\n1. Add a class in `scenarios.py` extending `Scenario`.\n2. In `__init__`, call `self.init(phases=[...])` with `TdPhase` (setup) and `LoadPhase` (benchmark).\n\n### Scenario Template\n\n```python\nfrom materialize.parallel_benchmark.framework import (\n    ClosedLoop,\n    LoadPhase,\n    OpenLoop,\n    Periodic,\n    ReuseConnQuery,\n    Scenario,\n    StandaloneQuery,\n    TdPhase,\n)\n\nclass MyScenario(Scenario):\n    def __init__(self, c, conn_infos):\n        self.init(\n            phases=[\n                # Setup phase (testdrive)\n                TdPhase(\"\"\"\n                    > CREATE TABLE bench_t (f1 INTEGER);\n                    > INSERT INTO bench_t SELECT generate_series(1, 10000);\n                    > CREATE MATERIALIZED VIEW bench_mv AS\n                      SELECT COUNT(*) AS c FROM bench_t;\n                    > SELECT c FROM bench_mv;\n                    10000\n                \"\"\"),\n                # Benchmark phase\n                LoadPhase(\n                    duration=120,  # seconds\n                    actions=[\n                        OpenLoop(\n                            action=StandaloneQuery(\n                                \"SELECT c FROM bench_mv\",\n                                conn_infos[\"materialized\"],\n                            ),\n                            dist=Periodic(per_second=100),\n                            report_regressions=True,\n                        ),\n                        ClosedLoop(\n                            action=ReuseConnQuery(\n                                \"INSERT INTO bench_t VALUES (1)\",\n                                conn_infos[\"materialized\"],\n                            ),\n                            report_regressions=True,\n                        ),\n                    ],\n                ),\n            ],\n            guarantees={\n                \"SELECT c FROM bench_mv (standalone)\": {\n                    \"qps\": 10,\n                    \"p99\": 500,\n                },\n            },\n        )\n```\n\n### Action Types\n\n| Class | Behavior | Use When |\n|-------|----------|----------|\n| `StandaloneQuery` | New connection per operation | Testing connection overhead + query |\n| `ReuseConnQuery` | Persistent single connection | Testing pure query latency |\n| `PooledQuery` | Draws from connection pool | Testing pooled throughput (set `conn_pool_size > 0`) |\n\n### Load Patterns\n\n| Class | Behavior |\n|-------|----------|\n| `OpenLoop(dist=Periodic(per_second=N))` | Fixed-rate: N operations/second regardless of latency |\n| `OpenLoop(dist=Gaussian(mean, stddev))` | Variable-rate: sleep drawn from gaussian distribution |\n| `ClosedLoop(...)` | Sequential: next operation starts when previous completes |\n\n### Guarantees and Regression Thresholds\n\n```python\n# Absolute guarantees (fail if not met)\nguarantees={\n    \"SELECT 1 (standalone)\": {\"qps\": 15, \"p99\": 400, \"max\": 500},\n}\n\n# Relative thresholds (fail if regression vs baseline)\nregression_thresholds={\n    \"SELECT 1 (pooled)\": {\"qps\": 0.8, \"p99\": 1.3},  # QPS must be >=80% of baseline\n}\n```\n\nAction names are auto-generated as `\"{query} ({connection_type})\"`.\n\n### Available Metrics\n\n`queries`, `qps`, `min`, `max`, `avg`, `p50`, `p95`, `p99`, `p99.9`, ..., `p99.999999`, `std`, `slope`.\n\n### Disabling a Scenario\n\n```python\nfrom materialize.parallel_benchmark.framework import disabled\n\n@disabled(\"Reason for disabling\")\nclass MyScenario(Scenario):\n    ...\n```\n\n### Running\n\n```bash\nbin/mzcompose --find parallel-benchmark run default --scenario MyScenario\nbin/mzcompose --find parallel-benchmark run default --scenario MyScenario --load-phase-duration 60\n```\n",".agents/skills/mz-console-review/SKILL.md":"---\nname: mz-console-review\ndescription: >\n  Review a Console (`console/`) PR or diff the way Console reviewers do: run the\n  change to verify claims, check it against the patterns and helpers the app\n  already has, and post findings as one GitHub review. Trigger: \"review this\n  console PR\", \"review my console changes\", \"does this console change look ok\",\n  a github.com/MaterializeInc/materialize/pull/<N> link whose files are under\n  `console/`, or a request to look over a React/TypeScript diff in this repo.\n  For Rust changes use mz-pr-review instead.\nargument-hint: [pr-number]\nallowed-tools: [Bash, Read, Grep, Glob]\n---\n\n# Console code review\n\n`mz-pr-review` covers the Rust checklists, none of which apply under `console/`.\nThe canonical Console rules live in `console/CLAUDE.md` (component conventions,\nimport restrictions, TypeScript and React patterns, testing philosophy) and in\n`console/doc/` (`guide-testing.md`, `guide-tanstack-table.md`,\n`mz-backwards-compatibility.md`). Read the sections the diff touches. Do not\nrestate them in comments, cite them.\n\nThis skill is what Console reviewers actually flag. Section 4 is distilled from\nroughly 570 reviewer comments on about 150 Console PRs from 2025 and 2026, and is\nordered by how often each kind of feedback came up. Sections 5 and 6 are not from\nthat tally. They cover cost and structure that reviewers rarely catch by\nreading, measured on `mz_catalog_server` and in render-count probes.\n\n## 1. Get the diff, and make it runnable\n\nRemotes are usually `upstream` = `MaterializeInc/materialize` and `origin` = a\npersonal fork, so a PR branch will not resolve against `origin`:\n\n```bash\ngit fetch upstream pull/<N>/head:pr-<N>-review\n```\n\nTo run anything you need the files inside `console/`, because `node_modules`\nlives at `console/node_modules` and a fresh worktree does not have it. Check the\nfiles into a tree with no local edits under `<path>`, run, then clean up. The\nrestore removes files the PR added and reverts the ones it changed, and it also\ndiscards any local edits under `<path>`:\n\n```bash\ngit checkout pr-<N>-review -- console/src/<path>\n(cd console && npx vitest run src/<path>)\ngit restore --source=HEAD --staged --worktree -- console/src/<path>\ngit branch -D pr-<N>-review\n```\n\nConfirm `git status --short` is empty before you finish.\n\n## 2. Verify every behavioral claim before writing it\n\nRun the suite and state the result (\"30/30 pass\"). Probe anything you intend to\nassert. A claim about draw order, nullability, or event handling that you reasoned\nout but did not execute is a claim you will get wrong.\n\nVitest hides `console.log` in passing tests. Assert against a sentinel to see a\nvalue: `expect(actual).toEqual(\"MARKER\")`.\n\njsdom makes graph and pointer assertions silently vacuous:\n\n| Symptom | Cause | Fix in the probe |\n|---|---|---|\n| Geometric assertions pass trivially | `ParentSize` measures nothing, width is 0 | `vi.mock(\"@visx/responsive/lib/components/ParentSize\", ...)` with a fixed size |\n| Pointer handler commits `NaN` | no `createSVGPoint` / `getScreenCTM`, so visx `localPoint` fails | `vi.mock(\"@visx/event\", () => ({ localPoint: vi.fn(() => ({ x, y })) }))` |\n\nMeasure colors instead of judging them: `d3.lab(color).l` gives perceived\nlightness. `d3` is already a dependency.\n\nFor SQL claims, compile the builder and read the SQL. Kysely's `.select` appends,\nit does not replace, so a \"fallback\" select can emit a duplicate column.\n\n## 3. Read the PR before the code\n\nReviewers raise these before any line comment:\n\n* **Motivation is traceable.** A linked issue (a Linear key such as `Closes: CNS-192`, or `Fixes database-issues#N` for a legacy GitHub issue), a Slack thread, or a design doc. If a reviewer cannot tell why the change exists, ask for context before reviewing.\n* **UI changes link the Figma design** in the description.\n* **Stacked PRs are linked** in each description, or the work is one PR with logical commits. Review semantic commits one at a time when the author asks.\n* **Commit messages are imperative and descriptive** per `doc/developer/guide-changes.md`, not \"addressed feedback\".\n* **A removed compatibility shim states its floor.** If the PR deletes a version map or gate, the description names the minimum supported environment version.\n* **WIP code is marked** with `TODO` (spelled `TODO`, not `TO-DO`), including leftover `console.log`.\n* **New pages and routes sit behind a LaunchDarkly flag.** The point is developer experience: it lets the work merge incrementally. Self-managed flags default to `true` in `src/config/flexibleDeploymentFlags.ts`, so a flag meant to stay off there must be set to `false` explicitly.\n\n## 4. What reviewers flag, most frequent first\n\n### 4.1 Remove and simplify\n\nThe single most common request. Look for:\n\n* Unused variables, atoms, exports, props, and query keys. Variables that are always `true`.\n* Redundant fallbacks (`?? 0` behind a value that cannot be null) and conditionals that render nothing.\n* Wrapper hooks that re-expose library options as custom `enable*` props. Keep wrappers thin, since the library's own options are better documented.\n* Features with no requirement behind them (column pinning, LRU layers). Ask \"is there a requirement for this?\"\n* A mini rules engine where two `if` statements would do.\n* Extra state flags (`destroyed`, `initialized`) that every method must check, and piles of refs. Stale refs are a bug source.\n* A copied error state when the mutation hook already exposes `error`.\n* `!!x` inside an expression that is already boolean, and several early returns that can merge into one.\n\n### 4.2 SQL and catalog correctness\n\n* **Nullable columns.** `disk_bytes` and friends are nullable. Coalesce, and fix the cast. When dividing, check the *denominator* for zero, not each operand.\n* **`ORDER BY ... DESC` puts nulls first.** Add `nulls last` where the column is nullable.\n* **User objects are `id LIKE 'u%'`**, via `isSystemId` in `~/api/materialize`. Not `name NOT LIKE 'mz_%'`, and not a bare `startsWith(\"u\")`.\n* **Statistics tables keep rows for dropped replicas** for up to a day. Aggregates like `bool_and(snapshot_committed)` must filter to extant replicas.\n* **Sources:** filter to top-level sources the way `sourceList.ts` does. Subsources and progress collections re-enter through joins on `mz_hydration_statuses`. Newer sources expose subsources as tables.\n* **Latest vs max.** Is the query returning the most recent sample or the largest one, and is that what the UI claims?\n* **A windowed flag must not drive a point-in-time answer.** \"Unreadable at some point in the window\" is not \"unreadable now\". Check which columns and predicates a window-wide flag reaches.\n* **Multi-process skew.** Summing usage over processes hides a single process about to OOM. Prefer `MAX`, and prefer existing catalog views over recomputing their math client-side.\n* **Two surfaces, two answers.** When a PR touches one of several queries for the same metric (list vs detail page), check the staleness bounds and column semantics match. A column can mean different things in the old and new relation.\n* Factor a repeated join or subquery into `src/api/materialize/expressionBuilders.ts` and left-join it, rather than `GROUP BY` just to pull one column.\n* Aggregate with `jsonb_agg` / `list_agg` and format on the client, instead of `STRING_AGG` or `pg_size_pretty` in SQL.\n* Raw SQL goes through the `sql` tagged template. Qualified names are structured (`{ databaseName, schemaName, name }` quoted with `sql.id()` or `quoteIdentifier`), never a dotted string, which breaks on names containing periods.\n* Comment catalog behavior the docs do not cover (for example `object_sub_id IS NULL` meaning the relation itself), and rewrite clever predicates for intent (`BOOL_AND(x IS NOT NULL)` over count arithmetic).\n\n### 4.3 Reuse before re-rolling\n\nBefore accepting any new helper, grep for what it replaces. Reviewers name these most:\n\n| Re-rolled | Already exists |\n|---|---|\n| clamp, pluralize, capitalize, null-guard | `clamp`, `pluralize`, `capitalizeSentence`, `notNullOrUndefined` in `~/util` |\n| Percent and byte text | `formatPercentage(fraction, decimals)` (takes a fraction. Call it directly and handle null inline, since a local `formatPercent` wrapper reads as a re-roll), `formatBytesShort`, `humanizeBytes`, `formatMemoryUsage` in `~/utils/format` |\n| Sorting | d3 `sort(iterable, ...accessors)` (multi-key ties, returns a copy), `handleSortOrder` in `~/utils/sort`, `sortingFunctions` (`nullsLast`, `numericNullsLast`) in `components/Table/tableColumnBuilders.ts`. The same comparator written twice becomes one shared helper |\n| `60_000`-style time constants | `millisecondsInMinute`, `millisecondsInHour` from `date-fns/constants` (not the `date-fns` root in v4) |\n| Object type text | `kebabToTitleCase`, `kebabToSentenceCase`, `kebabToScreamingSpaceCase` (SQL keywords such as `MATERIALIZED VIEW`) in `~/util`, `formatObjectType` in `platform/clusters/format.ts` |\n| Route strings built by hand | path helpers in `~/platform/routeHelpers` (`regionPath`, `useBuildWorkflowGraphPath`, ...). Add one there when it is missing |\n| Logic copied from a neighboring page | extract one helper and switch both call sites to it, so the copies cannot drift |\n| System-object check | `isSystemId` in `~/api/materialize` |\n| Duration formatting, lag caps | `formatInterval`, `formatDurationForAxis` in `~/utils/format` (fractional units are reserved for seconds). `formatDurationForAxis` is for axes: it rounds to whole minutes and caps at 24h, so 90s reads \"2m\" in text. The 24h cap `MAX_LAG_MS` is module-private there and `FreshnessGraph.tsx` declares its own copy, so export one instead of adding a third |\n| An anchor with custom hover/underline | `TextLink` from `~/components/TextLink` |\n| Status colors copied from a pill | export `colorSchemeToStyles` from `StatusPill.tsx` |\n| Layout props threaded one by one | the `containerProps` pattern, see `Gauge.tsx` |\n| A new cluster-sizes query | `useAvailableClusterSizes` / `fetchAvailableClusterSizes` |\n| A new \"all objects\" fetch | `useAllObjects` and the global subscribes (`useGlobalUpsertSubscribe`) |\n| A new filter dropdown | mirror `SchemaFilter.tsx` / `DatabaseFilter.tsx` (reserved \"all\" id, caller sets width) |\n| A new DAG layout | `dagreGraphHelpers.ts` and the workflow graph components |\n| A `useEffect` + timeout debounce | `lodash.debounce` (see `ObjectExplorer.tsx`) |\n| Previous-value or latest-value refs, or a callback read inside a debounce or timer | `usePrevious`, `useLatestRef` from `@chakra-ui/react` (see `useAutomaticallyConnectSocket.ts`) |\n| Tab active state from `location.pathname` | `NavLink`'s `isActive`, see `PageTabStrip` |\n| Set diffing by hand | `Set.prototype.difference` |\n| Secret masking | export `formatAppPassword` from `~/queries/frontegg` |\n| A per-tenant localStorage key | export `buildCacheKey` from `platform/shell/commandCache.ts` |\n| Chart axes, grid, tooltip, legend, cursor | `buildXYGraphLayoutProps` and friends in `~/utils/graph`, `~/components/graphComponents`, `useChartLegend`, `useCursorState`, `colors.lineGraph` |\n| visx plumbing | `Line` from `@visx/shape` for straight lines (as `GraphLineCursor` does), `Group` from `@visx/group` instead of repeating offset arithmetic on each child, `ParentSizeProvidedProps` to type a `ParentSize` child's props, `useDrag` from `~/hooks/useDrag` for draggable handles |\n| Several small components for one feature | one `xComponents.tsx` file, like `formComponents.tsx` |\n\nWhen a new component duplicates an existing one, say which pages it lands on and\nwhat those pages have today.\n\n### 4.4 Tests\n\nValue over volume. Look for:\n\n* **Tests that assert on query-builder string output, SQL snapshots, CSS attributes, or class names.** Replace with SQL tests (`*.test.sql.ts`) that create real objects and assert results, or with user-visible assertions.\n* **Missing corner cases.** Ask for the harder fixture: a deeper role hierarchy, overlapping buckets, multiple inherited roles, users that can log in. For graph fixtures, an ASCII diagram in a comment helps.\n* **Assert the outcome, not the call.** After a `GRANT`, query `mz_internal.mz_show_all_privileges`.\n* **Fixtures that do not look like real data.** Mock only the relation under test, and make values realistic.\n* **`vi.mock` / `vi.spyOn` on app modules.** Reviewers dislike them (unstable, break on circular imports). Prefer MSW via `buildSqlQueryHandlerV2`, with per-call query keys when one mutation issues several statements, or dependency injection.\n* **Redundant happy-path slices.** One happy-path test plus separate corner cases beats ten tests that all break together. Multiple assertions per integration test are fine.\n* Unnecessary setup (`GRANT CREATEROLE` when the test already logs in as a superuser).\n* Regex over `toContain` for generated code, `!` over `?.` inside tests so they fail early, no `if` branches around expects, constants hoisted out of `describe` in `SCREAMING_SNAKE_CASE`.\n* Every bug found in review gets a regression test. New helpers and classes get unit tests.\n* A test that asserts a function's arity or a mock's call shape breaks when someone adds an optional parameter. Assert what the user sees instead.\n\n### 4.5 Types and nullability\n\n* No `any`, no casts that the types do not need (`as unknown as T`).\n* Ask why each field is nullable when every caller defines it.\n* Fields that are always null together become one nested object (`cluster: { id, name } | null`).\n* Two booleans that can conflict (`usersOnly`, `rolesOnly`) become a discriminated union.\n* Boolean props are optional (`canIssue?: boolean`) so `<X canIssue />` works. An error prop is `error?: boolean`, not `unknown`.\n* Type a required key as required instead of throwing when it is missing.\n* `===` over `==`. Prefer `undefined` over `null` for optional values.\n\n### 4.6 Naming\n\n* Units are suffixes: `durationMs`, not `ms`.\n* Booleans are present tense (`hasActiveSubscription`). Components are nouns, not verbs (`LicenseKeyCtaContent`, not `GetLicenseKey`).\n* `Inner` suffix only for a suspense boundary split, `Content` for a block of content.\n* `build...Query` is reserved for SQL builders. Transforms are `transform...`, fetchers `fetch...`, DOM listener setup `register...`.\n* Do not alias a name as an id (`parent.name as parentId`). SQL aliases are camelCase, named for the general concept (`relationComment`, not `tableComment`).\n* Callback props keep parity with DOM events (`onPointMove`, `onClick`), not new verbs like `onPointSelect`.\n* A variant that changes styling is a `variant` prop, not inferred from which callbacks were passed.\n* Icons end in `Icon`. Version gates are named constants (`MCP_OAUTH_MIN_VERSION`), not inline strings.\n* Loop variables say what they index (`bucketIndex`, not `j`).\n\n### 4.7 Code comments\n\nThe rules are in the root `CLAUDE.md` \"Code comments\" section. What reviewers\nflag in practice: comments with fluff, details that make no sense out of context\n(\"avoids alphanumeric sorting issues\"), and comments that no longer match the\ncode. They ask *for* comments on undocumented catalog behavior, on unconventional\nuses (react-query used only to get loading/error state for an initializer), on\nnon-obvious algorithms, and on props whose meaning is subtle. It is not necessary\nto describe the evolution of the code by referring to code that has been removed\n(\"use quicksort because bubble sort is inefficient\").\n\n### 4.8 Loading, error, and empty states\n\n* Every `useQuery` renders distinct loading, error (`isError`), and empty UI. A mutation failure is visible to the user, not only `console.error`.\n* No state falls through to an endless spinner (a disabled region is an error, not \"loading\").\n* Loading is a spinner inside the card, not \"Loading...\" text, which shifts layout on every refetch. Empty states render inside the card, not as a blank page.\n* Prefer `useQuery` with explicit states over new `useSuspenseQuery`.\n* **The global subscribes start empty.** `useAllClusters` and `useAllObjects` return `data: []` until `snapshotComplete`, so a page that reads empty as \"none\" shows \"No clusters\" or a false success on a cold load. Gate on `snapshotComplete`, and render the subscribe's error.\n* An entity with nothing to report gets one empty state, not a green \"0 of 0 within target\" summary over an empty chart.\n\n### 4.9 Data layer\n\n* Query keys start with `buildRegionQueryKey(...)` (`~/api/buildQueryKeySchema`), so switching region or organization never serves another region's cache.\n* Queries live under `src/api/materialize/...`, not in `platform/`.\n* `staleTime` / `gcTime` are deliberate. If data never goes stale, use `Infinity`, and invalidate on logout or mutation.\n* Refresh derived data by putting its query key in the namespace that mutations already invalidate, rather than polling (a 60s poll means 60s-stale ownership).\n* No new `useSql*` hook calls. `console/CLAUDE.md` (\"React Query\") has the read and write pattern.\n* Global subscribes are hoisted to the route that owns the section, or navigating remounts them and the view jitters.\n* Validate user-supplied parameters at the `build...Query` layer (see the start-time check in `queryHistoryList.ts`), not only in the UI.\n* Anything persisted to localStorage is scoped by organization and region, and cleared on region switch. An unscoped key leaks one tenant's catalog into another session.\n\n### 4.10 React hooks and state\n\n* No `useEffect` to sync props into state or to react to a user action. Do it in the event handler, or initialize state in the modal's `onOpen`.\n* Effect dependencies hold the primitive that matters (the JWT string, not the whole query result), so work reruns only when it should.\n* TanStack table `columns` are static. Dynamic data flows through `data`, or cells remount and tooltips vanish.\n* One owner per lifecycle: a connection manager owns connect, disconnect, and retry, and the hook is a thin shell. Shell output changes go through the `websocketFsm.ts` state machine.\n* For Jotai, a derived write-only atom with an early return, not returning the previous object and hoping it skips a render.\n* A helper that mutates variables outside its scope is harder to read than the same code inline with a comment.\n\n### 4.11 Routing and URL state\n\n* View modes and modal flows (graph vs table, edit role) are subroutes, so a reload keeps the user's place.\n* Filter and lookback state belongs in the URL, not localStorage, when it should survive reload and share.\n* Do not branch on `location.pathname` when `<Route>` already branches.\n* A URL-selected entity that does not exist (a dropped cluster in an old link) must not silently show a different one. Write the fallback into the URL, or say it was not found.\n* Imports use `~/` unless the file shares a parent directory.\n\n### 4.12 Versions, flags, and deployment modes\n\n* A query that reads a new column or relation breaks older environments. Gate it with `useEnvironmentGate` and a named version constant, per `console/doc/mz-backwards-compatibility.md`. Self-managed can run a new Console against much older environments.\n* Test across versions with emulator images on another port (`misc/docker/README.md` in the Console tree) or a local kind cluster.\n* Cloud-only initialization goes in `CloudAppInitializer`, not `AppInitializer` (flexible deployments and impersonation).\n* `forceShow` on a nav item breaks when the environment is disabled.\n* Superuser checks through `usePrivileges` hit `has_table_privilege` and load environmentd. Use `useIsSuperUser`.\n* An `AppConfigSwitch` whose branches render the same thing is noise.\n\n### 4.13 Visual and UX QA\n\nReviewers run the branch and attach screenshots or recordings. If you can run it\n(`console/CLAUDE.md` has the dev commands), do. Otherwise list these for the human\nreviewer:\n\n* Overflow, especially in modals, filters, and form footers.\n* Dark mode. Any literal color (`colors.green[50]`, hex, `hsl(`) is suspect, since it does not flip.\n* Spacing on the 4px grid in relative units (`3`, not `\"12px\"`). Valid `textStyle` values come from `src/theme/typography.ts`. Valid Chakra tokens for `zIndex`. Chakra style props over `style`.\n* **Types do not catch invalid style values.** Chakra passes `alignItems=\"left\"` straight through (Chrome computes it as `normal`, which `CSS.supports(\"align-items\", \"left\")` confirms), and an undefined `textStyle` such as `text-ui-sm` falls back to the default without an error.\n* Consistency with existing patterns: the app's purple color scheme, existing button hover behavior, filter chips like query history, an Apply button for unit inputs. \"We don't do this anywhere else\" is a common and valid objection.\n* Layout shift on refetch, jitter on navigation, flicker on load.\n* Thresholds that agree with each other (a value turns orange at 5s but the filter starts at a minute).\n* Copy: title case in titles, correct terminology (\"role with LOGIN attribute\", not \"privilege\"), where to save a generated config, a link to the docs page.\n* Semantic elements (`ul`/`li` for lists).\n* **Mouse-only click targets.** A clickable `Tr`, `HStack` or legend pill with `onClick` cannot be reached from the keyboard. Make it a button (`as=\"button\"`, `type=\"button\"`) with `aria-pressed` for toggles (see `connectComponents.tsx`), and prefer `:focus-visible` over an always-on focus ring.\n* One placeholder for a missing value across a page (`-` or `—`, not both).\n\n### 4.14 Security and session traps\n\nRare but high severity, and most often caught by automated QA review after merge:\n\n* Tenant or region state that survives a switch: caches, module-level singletons, persisted rows seeded with `snapshotComplete: true`.\n* Auth flows where the client clock or cached token decides a redirect. Redirects should be server-authoritative, or users loop.\n* Two concurrent sessions (password and OIDC) where queries silently run as the wrong account.\n* State-changing `GET`s, and protections that rely on another system's cookie config (`SameSite`) instead of checking `Origin` / `Sec-Fetch-Site`.\n* `btoa` on non-ASCII input. Service accounts where the code assumes a user email.\n\n### 4.15 Charts and graphs\n\nGraph PRs collect the same gotchas. Check each:\n\n* **Reuse first.** The 4.3 rows for graph helpers and visx plumbing. A block near identical to `FreshnessGraph`, `UtilizationGraph` or `ThresholdLineGraph` should come from the same pieces.\n* **Hover redraws only the cursor and tooltip.** Mark layers (lines, bars, rows) are `React.memo` with stable props: scales and layouts memoized on width and domain, series memoized. A memo only helps if callers hoist their arrays. One built inline reruns every render.\n* **Volatile inputs get their own layer.** Anything derived from `useAllObjects` or `useAllClusters` gets a new identity on any change anywhere in the environment. Draw it as its own memoized layer, or key its memo by content, so an unrelated DDL does not redraw the chart.\n* **Which point is hovered.** `findNearestDatum` returns the nearest bucket start, which near a bar's right edge is the next bar. For bucketed bars, find the bucket that contains the time.\n* **Decorative layers keep `pointerEvents=\"none\"`** and sit under the event overlay, or a full-width strip swallows hover.\n* **Labels state only what the data says.** A max-per-bucket series is a peak, not an average. Name the unit precisely (\"% of replica CPU allocation\"), and do not label an inferred state as recorded.\n* **Wide windows stay readable.** Bars narrower than a few pixels alias into stripes. Merge buckets client-side, keeping each metric's peak.\n* **Colors.** Series take `colors.lineGraph`. A generated palette varies perceived lightness (`d3.hcl` / `d3.lab`), not HSL lightness.\n* **Axis formatters are for axes.** `formatDurationForAxis` and tick formatters round. Headline and table values need the unrounded formatter.\n* **Refresh cost.** A chart that polls re-reads its whole window on every refetch. See 5.1 in `cost-and-structure.md`.\n\n### 4.16 Design docs\n\nFor `doc/design/` PRs, reviewers ask for concrete ranges and limits, error states\n(an unresponsive cluster), reuse of existing global subscribes and components,\nconsistency when two polled queries resolve at different times (a subscribe\navoids it), client-side formatting, and a scoped first version (superuser only)\nbefore the general case.\n\n## 5. Check what the change costs\n\nWhen the diff changes a query builder, a subscribe, a `refetchInterval` or\n`staleTime`, or anything that runs at pointer or keystroke rate, read section 5\nof `cost-and-structure.md` in this skill's directory and work through it. The\n`mz_catalog_server` cost and the render cost it checks do not show up in a read\nof the diff.\n\n## 6. Audit hooks and component structure\n\nWhen the diff adds or moves a `useEffect`, `useMemo`, `useCallback`, or\n`React.memo`, or splits a component, read section 6 of `cost-and-structure.md`.\n\n## 7. Write the comment the way a Console reviewer writes it\n\nFindings are triaged with the person you are reviewing for first. Present the\nlist, let them cut and rephrase, then post. Do not post a review off your own draft.\n\nComments are short, hedged, and collaborative. State the claim, give at most one\nclause of why, name the alternative, stop. Typical openers: `Nit:`, `I think we\nshould`, `Maybe we can`, `Could we reuse`, `Let's just`, `I'd recommend`,\n`I wonder if`, `Kinda feels like`, `Why is X nullable here?`, `How does this look\nin dark mode?`.\n\nReal comments at the right length:\n\n> nit: We usually like to use `<>` for react fragments\n\n> Can most likely reuse the TextLink component\n\n> Why is objectId nullable here? Usage of this hook shows me that it's always defined\n\n> Kinda feels like these should be grouped into a `cluster` object, especially if both of them can be null\n\n> Let's remove this comment.\n\nKeep measurements and probe output out of the comment. They belong in your reply\nto the person you are reviewing for. A bug claim is the exception: attach the\nrepro (a screenshot, a recording, or a short diff that triggers it), because that\nis what makes it actionable.\n\nPoint at the existing pattern with a permalink rather than describing it. Offer\npseudocode or a suggestion block when the fix is not obvious. Say when something\nis not blocking: \"Doesn't need to be blocking but I suggest in a followup PR,\"\n\"We can do this as a followup.\"\n\n## 8. Post as a single review\n\nOne review, not N comments, so the author gets one notification:\n\n```bash\ngh api repos/MaterializeInc/materialize/pulls/<N>/reviews \\\n  --method POST --input review.json\n```\n\n`review.json` is `{commit_id, body, event: \"COMMENT\", comments: [...]}`. Use\n`event: \"COMMENT\"` unless the person asked to approve or request changes. Each\ninline comment is `{path, line, side: \"RIGHT\", body}`, and a multi-line anchor adds\n`start_line` and `start_side`. General findings go in `body`.\n\nPermalink every code reference and pin the SHA:\n\n* Code already on main gets the PR's **base** SHA (`gh pr view <N> --json baseRefOid`).\n* Code inside the PR gets the **head** SHA.\n* Verify each cited line resolves before posting: `git show \"<sha>:<path>\" | sed -n '<line>p'`. When the SHA is in a shell variable, brace it (`\"${sha}:${path}\"`). zsh reads `$sha:c` as a modifier and drops the colon, even inside quotes.\n\nAfterwards, confirm what landed:\n\n```bash\ngh api \"repos/MaterializeInc/materialize/pulls/<N>/comments?per_page=100\" \\\n  --jq '[.[] | select(.pull_request_review_id == <id>)] | length'\n```\n\nBefore posting, check for a pending review. GitHub allows one pending review per\nperson per PR, so a stray one blocks a new review, and one pinned to an older\ncommit cannot take new comments because their lines resolve against that commit:\n\n```bash\ngh api \"repos/MaterializeInc/materialize/pulls/<N>/reviews?per_page=100\" \\\n  --jq '[.[] | select(.state == \"PENDING\")]'\n```\n\n* **Inline comments must fall inside a diff hunk.** Read the ranges with `gh api \"repos/MaterializeInc/materialize/pulls/<N>/files\" --jq '.[].patch' | grep '^@@'`. Anything outside goes on the file (`subjectType: FILE`) or in the body.\n* **Replies and new threads in one review** need GraphQL: `addPullRequestReview` with `commitOID`, then `addPullRequestReviewThread` and `addPullRequestReviewThreadReply` with that `pullRequestReviewId`, then `submitPullRequestReview` with `event: COMMENT`.\n* **Pending comments are invisible to REST**, where `pulls/comments/<id>` returns 404. Read and edit them through GraphQL (`updatePullRequestReviewComment`).\n* **Link the priorities.** When a review mixes bugs and nits, list the bugs in the body with `#discussion_r<id>` links so the author fixes those first.\n* **Pace the batches.** When the author has just received a large review, post the blocking items and hold structural suggestions until they respond.\n\n## 9. Re-review after the author pushes\n\n* **Diff from the last SHA you reviewed, not from the base:** `git fetch upstream pull/<N>/head:pr-<N>-r2 && git diff <last-reviewed-sha> pr-<N>-r2`.\n* **Sort each earlier comment into fixed, partly fixed, or open by reading the code at the new head.** Replies do not count, and a fix without a reply is fine. Thread state comes from GraphQL `reviewThreads { isResolved isOutdated }`.\n* **Probe every fix for the bug it might introduce.** Removing an effect left a debounce that could strand a drag handle, and scoring unreadable objects as breaching reached a \"right now\" column. Both passed the existing tests.\n* **When the author pushes back with numbers, check they measured what you suggested,** and time a plan label rather than trusting it. `lookup` against `differential join` said nothing about which was faster. Reply in the thread with your own measurement, since numbers belong in that reply.\n* **Re-run the suite on the new head,** and say so in the body.\n\n## Output\n\nGroup findings as **Blocking**, **Strong suggestions**, **Nits**, and mark\nfollow-up candidates. Open the summary with one specific sentence on what the PR\ndoes well (\"Loving the tests!\", \"Great work making this page dynamic, and it uses\na lot of our existing patterns\"), then the verdict (\"Small nits but otherwise\nlooking really good\", \"Once my comments are addressed, LGTM\"). Name the test\nresult. Flag anything you could not verify rather than asserting it.\n",".agents/skills/mz-dbt-release/SKILL.md":"---\nname: mz-dbt-release\ndescription: >\n  Cut a dbt-materialize PyPI release: bump the version in `__version__.py`\n  and `setup.py`, date the `Unreleased` CHANGELOG entry, and open the\n  release PR with a `Ship: <url>` body. Trigger: \"cut a dbt release\",\n  \"release dbt-materialize\", \"release the dbt adapter\", \"ship\n  dbt-materialize vX.Y.Z\", \"publish dbt-materialize to PyPI\", \"bump\n  dbt-materialize version\", \"new dbt adapter version\". Use this skill\n  even if the user just says \"ship the dbt adapter\" or pastes a feature\n  PR and asks for \"the next dbt release\" without naming version mechanics.\n---\n\n# Cutting a dbt-materialize release\n\nA release PR bumps the published version of the `dbt-materialize` PyPI\npackage. It is mechanical: three files change, one commit, one PR.\n\nThe adapter lives in `misc/dbt-materialize/`. PyPI publication is handled by\nCI after the PR merges.\n\n## Files to touch\n\nExactly three files:\n\n| File | Change |\n|---|---|\n| `misc/dbt-materialize/dbt/adapters/materialize/__version__.py` | Bump `version = \"X.Y.Z\"` |\n| `misc/dbt-materialize/setup.py` | Bump `version=\"X.Y.Z\"` (kwarg in `setup(...)`) |\n| `misc/dbt-materialize/CHANGELOG.md` | Insert `## X.Y.Z - YYYY-MM-DD` between `## Unreleased` and the existing bullets |\n\nBoth version strings must stay in sync — the comments in each file say so.\n\n## Picking the next version\n\n* Default: bump the **patch** component (`1.9.9 → 1.9.10`).\n* The **minor** component tracks the required `dbt-postgres` minor. Only bump\n  minor when upgrading to a new `dbt-postgres` minor (e.g. `1.9.x → 1.10.0`).\n* Read the current version from `__version__.py`.\n\n## Confirming what's being released\n\nThe `## Unreleased` section of `CHANGELOG.md` is the source of truth for what\nships. If it's empty, there is nothing to release. Cross-check with:\n\n```\ngit log <prev-release-sha>..HEAD -- misc/dbt-materialize/\n```\n\nIf a commit since the previous release touched `misc/dbt-materialize/` but is\nnot represented under `## Unreleased`, stop and ask whether it should be\nincluded — don't silently invent a changelog entry.\n\n## CHANGELOG edit\n\nThe release does **not** rewrite the unreleased bullets; it just dates them.\nInsert a new dated heading immediately after `## Unreleased`, leaving\n`## Unreleased` itself empty:\n\n```\n## Unreleased\n\n## 1.9.10 - 2026-05-20\n\n* Support unmanaged clusters in `deploy_init`. ...\n```\n\nUse today's date in `YYYY-MM-DD`.\n\n## Branch, commit, PR\n\n* Branch name: `dbt-release-X.Y.Z`, branched from current `main`.\n* Commit subject: `dbt-materialize: release vX.Y.Z`\n* Commit body / PR body: a single `Ship: <url>` line pointing at the PR that\n  triggered this release (the feature/fix PR being shipped). If multiple PRs\n  are being shipped, pick the headline one — the changelog already enumerates\n  them.\n* PR title: same as the commit subject.\n* PR targets `MaterializeInc/materialize:main`.\n\nExample:\n\n```\ngh pr create --repo MaterializeInc/materialize --base main \\\n  --head <your-fork>:dbt-release-1.9.10 \\\n  --title \"dbt-materialize: release v1.9.10\" \\\n  --body \"Ship: https://github.com/MaterializeInc/materialize/pull/36625\"\n```\n\n## What you do NOT run\n\nThis PR touches only Python version strings and Markdown. Skip:\n\n* `cargo check`, `cargo clippy`, Cargo.lock work — no Rust changes.\n* Test suites — there are no tests to add for a version bump.\n\n`bin/fmt` and `bin/lint` cover `misc/dbt-materialize/*.py`, but a\none-character version bump almost never triggers them. Run them if\nyou've made an accidental wider change; otherwise they're optional.\n\n## Verifying before pushing\n\n```\ngit diff --stat\n```\n\nShould show exactly three files, predominantly a `+4 / -2` shape (one\nline each in the two version files, two added lines in `CHANGELOG.md`).\nA few extra lines in `CHANGELOG.md` are fine if a bullet was reformatted\nwhile finalising it; an unexpected fourth file means you've gone\noff-script.\n\n## Quick reference\n\n```\n# 1. Branch\ngit checkout -b dbt-release-X.Y.Z\n\n# 2. Edit the three files (see table above)\n\n# 3. Verify\ngit diff --stat   # expect 3 files, +4/-2\n\n# 4. Commit\ngit commit -am \"dbt-materialize: release vX.Y.Z\n\nShip: <feature-pr-url>\"\n\n# 5. Push and open PR\ngit push -u origin dbt-release-X.Y.Z\ngh pr create --repo MaterializeInc/materialize --base main \\\n  --head <your-fork>:dbt-release-X.Y.Z \\\n  --title \"dbt-materialize: release vX.Y.Z\" \\\n  --body \"Ship: <feature-pr-url>\"\n```\n\n## Historical reference\n\nRecent release commits (find via `git log --oneline -- misc/dbt-materialize/dbt/adapters/materialize/__version__.py`):\n\n* v1.9.10 — PR #36636\n* v1.9.9 — PR #36609\n* v1.9.8 — PR #36529\n\nInspect any of them with `git show <sha>` to see the exact diff shape.\n",".agents/skills/mz-adapter-guide/SKILL.md":"---\nname: mz-adapter-guide\ndescription: >\n  Correctness invariants + architecture: adapter, coordinator, pgwire, peek\n  paths, timestamp oracle. Trigger: questions about these subsystems —\n  \"how does coordinator work\", \"what are read holds\", \"explain peek path\",\n  \"how does timestamp selection work\", \"why does this query block\". Also\n  when editing OR reviewing code in src/adapter/, src/pgwire/,\n  src/timestamp-oracle/ (reviewing a PR, diff, or commit that touches these\n  paths counts).\n---\n\n# Adapter Guide Skill\n\nWhen working on or reviewing the adapter layer or related crates, always read\nand follow the guidance in `doc/developer/guide-adapter.md`. That document\ncontains correctness invariants, architectural notes, a list of optimizations\nthat have been tried and rejected for correctness reasons, and a checklist for\nreviewing adapter changes. Read it before making changes and before reviewing\nthem.\n\nRead `doc/developer/guide-adapter.md` now.\n\nWhen working on the timestamp oracle (`src/timestamp-oracle/`), also read the\nmodule-level docs in `src/timestamp-oracle/src/lib.rs` and\n`src/timestamp-oracle/src/batching_oracle.rs` for the `TimestampOracle` trait\ncontract and the batching/linearizability invariants.\n\nWhen you or the user discover that an approach is incorrect or heading in a\nwrong direction, suggest distilling the learning into\n`doc/developer/guide-adapter.md`. Add it to whichever section fits best — a\nnew correctness invariant, a new subsection under an existing invariant, a\nrejected optimization, or a new top-level section if needed. Describe what was\ntried, why it's wrong, and any relevant context. This keeps the guide up to\ndate and prevents future re-discovery of the same pitfalls.\n",".agents/skills/mz-commit/SKILL.md":"---\nname: mz-commit\ndescription: >\n  Trigger: \"commit\", \"prepare commit\", \"create PR\", \"push\", \"open pull request\",\n  or mentions committing, pre-commit checks, pull requests in Materialize. Also\n  \"ship it\", \"ready to merge\". For code review use mz-pr-review.\n---\n\n# Committing in Materialize\n\nRead `doc/developer/guide-changes.md` for the full conventions on submitting and reviewing changes.\n\n## Pre-commit checklist\n\nBefore committing, run these and fix any warnings:\n\n1. `bin/fmt` (formats all `.rs`, `.py`, and `.proto` files; takes no file arguments)\n2. `bin/lint` (can error if tools are missing; use `bin/ci-builder run stable bin/lint` as an alternative)\n3. `cargo clippy --all-targets -- -D warnings`\n\n`bin/lint` and clippy do not run rustdoc. When the diff adds imports or edits\ndoc comments, run the Doctests check on the changed crates (command in the\nmz-debug-ci skill). When a `metric!` definition changed, `bin/lint`\nregenerates `doc/user/data/metrics.yml`. Commit the result, or the \"Lint and\nrustfmt\" job fails on the diff.\n\nAfter rebasing onto upstream/main, run `cargo check --workspace --all-targets`\neven when the rebase had no conflicts. Upstream changes can still break the\nbranch, for example a dependency your branch needs that upstream removed as\nunused (Nightly's `bin/unused-deps` reports those), a duplicate item, or a new\nenum variant your match does not cover. CI checks this in the\n\"Merge skew cargo check\" job.\n\nDo not manually update `*.snap` files.\nUse `cargo test` followed by `cargo insta accept` to update snapshot files.\nRewrite datadriven test expectations with `REWRITE=1 cargo test ...`.\n\n### LIR plan changes: regenerate the schema registry\n\nThe stable LIR format lets optimized plans be stored durably and reused, so\ntwo snapshot tests in `mz-compute-types` pin what a stored plan looks like\nand what it means. Both key off `LIR_VERSION` in\n`src/compute-types/src/plan.rs`.\n\n`lir_schema` traces the serde schema of everything reachable from\n`LirRelationExpr` into `tests/snapshots/lir_v{LIR_VERSION}.json`. Run it\nwhenever the diff changes the serde-visible shape of a type the trace\nreaches: anything in `src/compute-types/src/plan.rs` or\n`src/compute-types/src/plan/`, a `*Func` payload, or a plan-reachable type\nin `mz-expr` or `mz-repr` such as `AggregateFunc`, `TableFunc`,\n`SqlScalarType` or `DateTimeUnits`:\n\n```\ncargo test -p mz-compute-types --test lir_schema\n```\n\nRegenerate with `REWRITE=1 cargo test -p mz-compute-types --test lir_schema`\nand review the diff. It is the reviewable record of the format change. If\nthe current `LIR_VERSION` has shipped (see below), bump it first so the old\n`lir_v{N}.json` stays in place for migration tooling and the rewrite targets\na fresh file.\n\n### Scalar function changes: regenerate the function registry\n\nThe schema registry sees a plan's shape, not what its functions compute.\n`func_registry` covers that gap for `UnaryFunc`, `BinaryFunc`, and\n`VariadicFunc`. Run it whenever the diff touches\n`src/expr/src/scalar/func.rs` or `src/expr/src/scalar/func/` (including\n`impls/` and `#[sqlfunc]` bodies):\n\n```\ncargo test -p mz-compute-types --test func_registry\n```\n\n* It fails when a function is added, removed, or has a changed property\n  (nullability, error behavior, monotonicity, output type, sqlfunc\n  signature) or a changed `#[sqlfunc]` body. If the failure names removed or\n  changed functions, or a body change that alters results, settle the\n  `LIR_VERSION` question below **before** regenerating. A rewrite at the old\n  version overwrites that version's entry in `func_registry_digests.json`\n  instead of adding a new one, no later check catches it, and that entry is\n  the only key back into git history for what the shipped version's\n  functions meant. Once the version is settled, regenerate the checked-in\n  snapshots and review the diff under `src/compute-types/tests/snapshots/`:\n\n  ```\n  REWRITE=1 cargo test -p mz-compute-types --test func_registry\n  ```\n\n  Never hand-edit `func_registry*.json`. The test rebuilds the registry and\n  compares it against all three files, so an edit fails it.\n* **New function with a payload** (any variant that carries data, e.g. a\n  target type, a regex, a scale): the registry build panics naming the\n  variant. Add a representative `Sample` to `unary_samples()`,\n  `binary_samples()`, or `variadic_samples()` in\n  `src/expr/src/scalar/func/registry.rs`. If a property depends on the\n  payload (e.g. a cast only errors when it has a scale), add a second sample\n  with `.labeled(\"...\")` covering the other branch. Payload-free variants and\n  plain `#[sqlfunc]` functions need no sample.\n* **Removed or changed function, or a body change that alters results**: if\n  the current `LIR_VERSION` has shipped in a release, bump it so stored plans\n  are replanned, and only then regenerate. An unshipped version is\n  regenerated in place. Treat the version as shipped when the latest release tag carries the\n  same `LIR_VERSION`:\n\n  ```\n  git fetch --tags\n  git show $(git tag --list 'v*' --sort=-v:refname | grep -v -- '-rc' | head -1):src/compute-types/src/plan.rs | grep 'pub const LIR_VERSION'\n  ```\n\n  A bump is a judgment call, so confirm it with the user before making it.\n  Additions never need a bump. When unsure whether a body change alters\n  results, say so in the PR description rather than deciding silently.\n\nCommit the regenerated snapshots together with the function change, and call\nout the registry diff (added, removed, changed functions) in the PR\ndescription.\n\n## PR titles and commit messages\n\nMaterialize uses squash merging, so the PR title becomes the commit subject on `main`.\n\n* Use imperative mood: \"Fix X\" not \"Fixed X\" or \"Fixes X\".\n* Be specific: \"Fix panic in catalog sync when controller restarts\" not \"Fix bug\".\n* Prefix with area if helpful: `adapter: `, `storage: `, `compute: `, `sql: `.\n\nWrite a thorough PR description explaining the rationale for the change.\nMention which tests were added or modified in the pull request description, but do not list which tests were run.\nAdd release notes for user-visible changes (should complete \"This release will...\").\n\n## Issue tracking: Linear only for new issues\n\nNew issues are filed in Linear, never in the `database-issues` GitHub repo. `database-issues` is legacy. Its open issues are still valid to read, link, and close, so an existing `database-issues#NNNN` reference in code or a comment stays as it is.\n\nReference the Linear issue by its its key, e.g. `Closes: SQL-450`. Don't include the full URL containing the issue title.\n\nWhen a change closes a legacy GitHub issue, `Fixes database-issues#NNNN` works for auto-closing.\n\n## Cargo.lock discipline\n\nNever regenerate the entire Cargo.lock — bare `cargo update` bumps every semver-compatible dep and introduces unrelated breakage (e.g., `os_info` pulling in `objc2` on macOS, `chrono-tz` changing timezone data, `serde_path_to_error` changing error formats).\n\n* **Adding a dep or changing features**: just `cargo check`. It updates only what's needed.\n* **Updating one crate**: `cargo update -p <crate>` (add `--precise <ver>` to pin).\n* **After any Cargo.lock change**, review the diff:\n  ```\n  git diff Cargo.lock | grep '^[+-]version' | head -40\n  ```\n  Pin back anything that moved unexpectedly: `cargo update -p <crate> --precise <old-version>`.\n* **After rebase conflicts in Cargo.lock**: resolve by taking HEAD's version then running `cargo check` (not `cargo update`). This preserves existing pins while adding only what the new commits require.\n\n## Git conventions\n\n* Work against the `main` branch of `MaterializeInc/materialize`.\n* Push branches to your fork.\n* Pull requests target `main` on `MaterializeInc/materialize`.\n* Each PR should contain one semantic change.\n* Put non-trivial user docs for a new feature in a separate PR. See \"Adding documentation for new features\" in `doc/user/README.md`.\n\n## Splitting large branches\n\nThe repo is squash-merge-only: every PR lands as exactly one commit on `main`, however many commits it had internally. Keep each commit/PR under ~500 changed lines, split by concern, not by the chronology of how the code was written.\n\n* Prefer GitHub's native stacked-PR support to land a chain of dependent PRs, where it's set up for the branch. Where it isn't, fall back to sequential landing, one PR at a time: cut a PR from the front, merge, `git fetch upstream && git rebase upstream/main` the remaining tail, repeat. Squash-only means merging an earlier PR always replaces its commits with one new commit, so any later branch still built on the old commits needs a rebase either way; sequential landing keeps that to one clean rebase per merge instead of compounding across an unmanaged chain.\n* Land foundational or low-conflict commits (design docs, scaffolding, new deps) first. The longer a tail branch lives, the more likely a concurrent unrelated PR touches the same files and turns a clean rebase into a real conflict.\n* To split one large diff into smaller commits, or squash a long messy history into fewer reviewable units, as an alternative to interactive rebase: `git reset --soft <base>` stages the full diff, then stage per group with `git add <files>` for a file-level split, or use `git checkout <sha> -- .` to reproduce an exact historical checkpoint's tree for a chronological squash. (Claude Code's Bash tool has no interactive terminal, so agents need this non-interactive form; use whichever you prefer by hand.) `git checkout <sha> -- .` only adds or updates paths present in `<sha>`, it never removes files that shouldn't exist yet at that checkpoint: compute the removal set first with `comm -23 <(git ls-files|sort) <(git ls-tree -r --name-only <sha>|sort)` and `git rm -f` those, or the checkpoint commit silently carries files from later history. Verify every checkpoint with `git diff HEAD <sha>` (must be empty) before moving to the next. Reattach already-clean commits on top with `git rebase --onto <new-branch> <old-base>` (also non-interactive).\n",".agents/skills/mz-dependencies/SKILL.md":"---\nname: mz-dependencies\ndescription: >\n  Trigger: adding, removing, or updating a Rust dependency, changing crate\n  features, touching Cargo.toml or Cargo.lock, \"cargo update\", \"add a crate\",\n  \"bump a dependency\", or a new dependency license in Materialize.\n---\n\n# Dependency management\n\n## Workspace dependencies\n\nAll third-party versions declared in `[workspace.dependencies]` in root\n`Cargo.toml`. Members use `dep.workspace = true` or\n`dep = { workspace = true, optional = true }`.\n\n* **Add new dep**: add to `[workspace.dependencies]` in root `Cargo.toml` first, then `dep.workspace = true` in member crate.\n* **Never inline version** in member `Cargo.toml` — `bin/lint-cargo` enforces.\n* **Update version**: change once in root `Cargo.toml`.\n* Cargo unifies features workspace-wide, so workspace declaration is union of all features across crates. Members just use `dep.workspace = true`.\n\n## Cargo.lock\n\nNever regenerate full Cargo.lock. When changing deps:\n\n* **Add dep or change features**: run `cargo check` — updates only what changed.\n* **Update specific crate**: `cargo update -p <crate>` (optional `--precise <version>`).\n* **Never bare `cargo update`** — bumps every semver-compatible dep, causes unrelated breakage from transitive changes.\n* **If lock regenerated**, diff before commit (`git diff Cargo.lock | grep '^[+-]version'`) and pin back unintended bumps with `cargo update -p <crate> --precise <old-version>`.\n\n## Licensing\n\nTwo files control license policy, **keep in sync**: `deny.toml` (`[licenses].allow`) and `about.toml` (`accepted`). New dep with new license not already allowed: add SPDX identifier to both.\n",".agents/skills/mz-parallel-workload/SKILL.md":"---\nname: mz-parallel-workload\ndescription: >\n  Extend parallel-workload stress framework: random SQL concurrently to catch\n  panics + unexpected errors (not perf — see mz-benchmark). Trigger:\n  \"parallel workload\", \"parallel-workload\", \"action.py\" re parallel workload,\n  or testing panics/unexpected errors under concurrency. Also \"add this to\n  parallel workload\" or bug that panics under concurrent DDL/DML.\n---\n\n# Extending Parallel Workload\n\nThe parallel-workload framework stress-tests Materialize by running random SQL actions concurrently across multiple threads. It catches **panics and unexpected query errors** - it does not verify result correctness.\n\n## When to Use This Framework\n\nAdd coverage here when a bug manifests as:\n- A panic under concurrent operations\n- An unexpected error from a query that should succeed (or fail gracefully)\n- A crash or connection loss triggered by specific DDL/DML combinations\n\n## How It Works\n\n1. The orchestrator spawns N worker threads, each assigned an **action list** (read, fetch, write, dml_nontrans, or ddl) based on the configured `Complexity`.\n2. Each worker randomly selects actions from its list using **weighted random choice** and executes them in a loop until the runtime expires.\n3. If an action raises a `QueryError`, the worker checks `action.errors_to_ignore(exe)`. If the error matches, it's logged and ignored. If not, the **test fails**.\n4. All threads share a `Database` object that tracks created objects (tables, views, clusters, etc.) with thread-safe locking.\n\n## Adding a New Action\n\n### Step 1: Define the Action Class\n\nAdd your class in `action.py`. Subclass `Action` and implement `run()`:\n\n```python\nclass MyNewAction(Action):\n    def run(self, exe: Executor) -> bool:\n        # Return False to skip (e.g., precondition not met)\n        # Return True after successful execution\n        exe.execute(\"SELECT my_new_feature()\", http=Http.RANDOM)\n        return True\n```\n\n### Step 2: Override `errors_to_ignore()` If Needed\n\nIf your action can produce expected errors (e.g., object was concurrently dropped), extend the base list:\n\n```python\n    def errors_to_ignore(self, exe: Executor) -> list[str]:\n        result = super().errors_to_ignore(exe)\n        result.extend([\n            \"my expected error message\",\n        ])\n        # Conditionally ignore errors based on complexity or scenario\n        if exe.db.complexity == Complexity.DDL:\n            result.extend([\"does not exist\"])\n        return result\n```\n\n### Step 3: Register in an Action List\n\nAdd your action class and weight to the appropriate list at the bottom of `action.py`:\n\n```python\n# In ddl_action_list, read_action_list, write_action_list, etc.\nddl_action_list = ActionList(\n    [\n        ...\n        (MyNewAction, 5),  # weight controls selection frequency\n        ...\n    ],\n    autocommit=True,  # True for DDL, False for transactional actions\n)\n```\n\n**Which list to use:**\n- `read_action_list` - SELECT, SUBSCRIBE, COPY TO (autocommit=False)\n- `fetch_action_list` - DECLARE CURSOR / FETCH (autocommit=False)\n- `write_action_list` - INSERT, COPY FROM (autocommit=False)\n- `dml_nontrans_action_list` - DELETE, UPDATE, INSERT RETURNING (autocommit=True)\n- `ddl_action_list` - CREATE/DROP/ALTER/RENAME (autocommit=True)\n\n## Common Patterns\n\n### Picking a Random Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    with exe.db.lock:\n        if not exe.db.tables:\n            return False\n        table = self.rng.choice(exe.db.tables)\n    # Use table outside db.lock but inside table.lock for mutations\n    with table.lock:\n        if table not in exe.db.tables:\n            return False  # Was dropped while we waited\n        exe.execute(f\"ALTER TABLE {table} ...\", http=Http.RANDOM)\n    return True\n```\n\n### Respecting Object Limits\n\n```python\ndef run(self, exe: Executor) -> bool:\n    if len(exe.db.tables) >= MAX_TABLES:\n        return False  # Skip if at capacity\n    ...\n```\n\nLimits are defined in `database.py`: `MAX_TABLES=5`, `MAX_VIEWS=15`, `MAX_CLUSTERS`, `MAX_SCHEMAS`, etc.\n\n### Creating a New Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    if len(exe.db.views) >= MAX_VIEWS:\n        return False\n    view_id = exe.db.view_id\n    exe.db.view_id += 1\n    try:\n        schema = self.rng.choice(exe.db.schemas)\n    except IndexError:\n        return False\n    with schema.lock:\n        if schema not in exe.db.schemas:\n            return False\n        view = View(self.rng, view_id, base_object, schema, ...)\n        view.create(exe)\n    exe.db.views.append(view)\n    return True\n```\n\n### Dropping a Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    with exe.db.lock:\n        if len(exe.db.views) <= 2:\n            return False  # Keep a minimum\n        view = self.rng.choice(exe.db.views)\n    with view.lock:\n        if view not in exe.db.views:\n            return False\n        exe.execute(f\"DROP VIEW {view}\", http=Http.RANDOM)\n        exe.db.views.remove(view)\n    return True\n```\n\n### Scenario-Specific Actions\n\nSome actions only run in specific scenarios. Override `applicable()` for\nthis, do NOT gate on the scenario inside `run()`: the worker skips\ninapplicable actions without counting an attempt, which keeps the\nend-of-run action coverage check (an action that never succeeds over a\nwhole run fails it) free of false positives.\n\n```python\ndef applicable(self, exe: Executor) -> bool:\n    return exe.db.scenario == Scenario.Rename\n\ndef run(self, exe: Executor) -> bool:\n    ...\n```\n\n### Using Prepared Statements\n\n```python\ndef run(self, exe: Executor) -> bool:\n    query = \"SELECT ...\"\n    if self.rng.choice([True, False]):\n        self.stmt_id += 1\n        self.exe_prepared(query, f\"mystmt{self.stmt_id}\", exe)\n    else:\n        exe.execute(query, http=Http.RANDOM)\n    return True\n```\n\n## Extending an Existing Action\n\nOften the simplest approach is to add new SQL variants to an existing action's `run()` method. For example, to test a new expression type, add it to `expression.py`. To test a new system flag, add it to `FlipFlagsAction.flags_with_values`.\n\n### Adding a System Flag to FlipFlagsAction\n\nIn `FlipFlagsAction.__init__`, add an entry to the `flags_with_values` dictionary:\n\n```python\nself.flags_with_values[\"my_new_flag\"] = [\"true\", \"false\"]\n```\n\n## Running\n\n```bash\n# Default: DDL complexity, regression scenario, 600s runtime\nbin/mzcompose --find parallel-workload run default\n\n# Custom options\nbin/mzcompose --find parallel-workload run default \\\n    --complexity=ddl \\\n    --scenario=regression \\\n    --runtime=300 \\\n    --seed=42\n\n# Available complexities: read, dml, ddl, ddl-only, random\n# Available scenarios: regression, cancel, kill, rename, backup-restore, 0dt-deploy, random\n```\n\n## Important Design Rules\n\n1. **Minimize locking** - Use `exe.db.lock` briefly to pick objects, then use per-object locks. Excessive locking reduces the chance of finding real race conditions. You can instead add an expected error in `errors_to_ignore`\n2. **Return False, don't raise** - If a precondition isn't met (empty list, at capacity), return `False` to skip the action.\n3. **Handle IndexError from `rng.choice`** - Objects can be concurrently removed. Either check length first under lock, or catch `IndexError` and return `False`.\n4. **Track state changes** - If you create an object, append it to the appropriate `exe.db` list. If you drop one, remove it.\n5. **Use `self.rng`** - Always use the action's seeded `random.Random` instance, never `random.choice()` directly. This makes failures reproducible via `--seed`.\n",".agents/skills/mz-limits-test/SKILL.md":"---\nname: mz-limits-test\ndescription: >\n  Add/modify/debug limits test. Trigger: \"limits test\", \"Generator subclass\",\n  \"many objects\", \"scaling test\", or stress-test Materialize with many objects\n  (tables, views, sources, indexes). Also edits in test/limits/mzcompose.py.\n---\n\n# Limits Test Framework\n\nThe limits test stresses Materialize with large numbers of objects, complex queries, and large ingestions. It catches regressions via fixpoint panics, stack overflows, CI timeouts (O(N^M) with N~1000), and OOM panics (2GB memory limit).\n\n## Where It Lives\n\n- **Main file**: `test/limits/mzcompose.py`\n- All Generator subclasses live in that single file\n- **Auto-discovery**: Subclasses of `Generator` are found automatically via `all_subclasses(Generator)` from `materialize.util` - no registration needed\n\n## Generator Base Class\n\n```python\nclass Generator:\n    COUNT: int = 1000      # Number of objects to create (override per test)\n    VERSION: str = \"1.0.0\" # Bump when test logic changes\n    MAX_COUNT: int | None = None  # Upper limit for --find-limit mode\n\n    @classmethod\n    def body(cls) -> None:\n        \"\"\"Override this. Print testdrive commands to stdout.\"\"\"\n        raise NotImplementedError\n\n    # Convenience iterators\n    @classmethod\n    def all(cls) -> range:       # range(1, COUNT+1)\n    @classmethod\n    def no_first(cls) -> range:  # range(2, COUNT+1) - skip first\n    @classmethod\n    def no_last(cls) -> range:   # range(1, COUNT) - skip last\n\n    @classmethod\n    def store_explain_and_run(cls, query: str) -> str | None:\n        \"\"\"Records EXPLAIN query for timing and prints `> {query}`.\"\"\"\n```\n\n`header()` is inherited - it resets the `public` schema and grants privileges. `footer()` prints a blank line. `generate()` calls `header()` → `body()` → `footer()`. `mz_system` is a built-in testdrive connection, so `$ postgres-execute connection=mz_system` works in `body()` without any registration.\n\n## How to Add a New Limits Test\n\n1. Add a new class in `test/limits/mzcompose.py` that extends `Generator`.\n2. Implement the `body()` classmethod. It must `print()` testdrive commands.\n3. Optionally override `COUNT` and `MAX_COUNT`, only if it's causing issues in CI.\n\n### Minimal Template\n\n```python\nclass MyNewFeature(Generator):\n    COUNT = min(Generator.COUNT, 100)  # Lower if operation is slow\n    MAX_COUNT = 800  # Optional: cap for --find-limit mode\n\n    @classmethod\n    def body(cls) -> None:\n        # Bump system limits if needed\n        print(\"$ postgres-execute connection=mz_system\")\n        print(f\"ALTER SYSTEM SET max_tables = {cls.COUNT * 10};\")\n        print(\"$ postgres-execute connection=mz_system\")\n        print(f\"ALTER SYSTEM SET max_objects_per_schema = {cls.COUNT * 10};\")\n\n        # Create objects\n        for i in cls.all():\n            print(f\"> CREATE TABLE t{i} (f1 INTEGER);\")\n            print(f\"> INSERT INTO t{i} VALUES ({i});\")\n\n        # Query and verify\n        for i in cls.all():\n            cls.store_explain_and_run(f\"SELECT * FROM t{i}\")\n            print(f\"{i}\")\n```\n\nYou can run it locally with `bin/mzcompose --find limits down && bin/mzcompose --find limits run main --scenario=MyNewFeature`, add `--find-limit` for the Release Qualification mode.\n",".agents/skills/mz-feature-flags/SKILL.md":"---\nname: mz-feature-flags\ndescription: >\n  Trigger: adding or changing a feature flag, dyncfg, or system parameter\n  default, reviewing a PR that adds one, LaunchDarkly flag setup, or\n  bin/lint-test-flags failures in Materialize.\n---\n\n# Feature flags\n\nChoosing whether a new feature flag defaults on or off in production is a\nproduct decision that needs context an agent rarely has. When adding a flag,\nask a human which default to use. When reviewing a PR, do not question the\ndefault it chose. However, in tests, a new flag should usually be on, so that\nsqllogictest and testdrive exercise the new code path. If\nits production default is off, turn it on for CI in\n`misc/python/materialize/mzcompose/__init__.py`\n(`get_minimal_system_parameters` if tests fail with it off, otherwise\n`get_variable_system_parameters`), which `bin/sqllogictest` and mzcompose's\n`Materialized` service pass to environmentd as system parameter defaults.\nRust integration tests use `TestHarness::with_system_parameter_default`.\nA value listed there overrides the compiled-in default in CI, so changing a\ndefault means changing both. `bin/lint-test-flags` fails unless every new\ndyncfg is registered for parallel workload and mzcompose (see the mz-debug-ci\nskill). A compiled-in default is not evidence of the production value: check\nwhether `test/launchdarkly-flag-consistency/mzcompose.py` lists the flag in\n`INTENTIONAL_LD_OVERRIDES` or `KNOWN_CROSS_ENV_DIVERGENCES`. A session\nvariable takes a system default only if `SESSION_SYSTEM_VARS` in\n`src/sql/src/session/vars.rs` lists it. Otherwise `--system-parameter-default`\nfor it only logs a warning at startup. Observed: the LaunchDarkly MCP\n`create-flag` tool creates boolean flags that serve true in every environment,\nincluding production. A human must set each environment's off variation and\nfallthrough to false.\n",".agents/skills/mz-platform-checks/SKILL.md":"---\nname: mz-platform-checks\ndescription: >\n  Create/modify/debug platform check. Trigger: \"platform check\",\n  \"platform-checks\", \"upgrade check\", \"restart check\", or writing Check class\n  testing feature survival across restarts/upgrades. Also edits in\n  misc/python/materialize/checks/all_checks/.\n---\n\n# Platform Checks\n\nThe Platform Checks framework is \"write once, run everywhere\" - a single test validates a feature across multiple scenarios (restarts, upgrades, etc.).\n\n## Key Concepts\n\n- **Check**: A Python class that tests one feature via testdrive fragments.\n- **Scenario**: The context (e.g., restart, upgrade) in which checks run.\n- **Action**: An individual step within a scenario (e.g., stop a container).\n\n## Where Checks Live\n\nAll checks are in `misc/python/materialize/checks/all_checks/`. The `__init__.py` auto-discovers every `.py` file in that directory, so adding a new file is all that's needed for registration.\n\n## Anatomy of a Check\n\nA check is a class that extends `Check` and implements three methods. Each returns a `Testdrive` object wrapping a testdrive fragment string.\n\n```python\nfrom textwrap import dedent\nfrom materialize.checks.actions import Testdrive\nfrom materialize.checks.checks import Check\n\nclass MyFeature(Check):\n    def initialize(self) -> Testdrive:\n        \"\"\"Run once at the start. Create tables, insert seed data, set up objects.\"\"\"\n        return Testdrive(dedent(\"\"\"\n            > CREATE TABLE my_feature_table (f1 INTEGER);\n            > INSERT INTO my_feature_table VALUES (1), (2), (3);\n        \"\"\"))\n\n    def manipulate(self) -> list[Testdrive]:\n        \"\"\"MUST return a list of exactly 2 Testdrive fragments.\n        Each runs after a restart/upgrade phase. Insert more data, ALTER objects,\n        or create derived objects (e.g., materialized views).\"\"\"\n        return [\n            Testdrive(dedent(\"\"\"\n                > INSERT INTO my_feature_table VALUES (4), (5);\n                > CREATE MATERIALIZED VIEW my_feature_view1 AS\n                  SELECT COUNT(*) AS c FROM my_feature_table;\n            \"\"\")),\n            Testdrive(dedent(\"\"\"\n                > INSERT INTO my_feature_table VALUES (6), (7);\n                > CREATE MATERIALIZED VIEW my_feature_view2 AS\n                  SELECT SUM(f1) AS s FROM my_feature_table;\n            \"\"\")),\n        ]\n\n    def validate(self) -> Testdrive:\n        \"\"\"Run after all initialize + manipulate steps. Verify everything survived.\n        MAY BE CALLED MORE THAN ONCE - must be idempotent. Any objects created\n        here must be TEMPORARY or explicitly dropped.\"\"\"\n        return Testdrive(dedent(\"\"\"\n            > SELECT * FROM my_feature_view1;\n            7\n            > SELECT * FROM my_feature_view2;\n            28\n        \"\"\"))\n```\n\n## Execution Order in a Scenario\n\n```\nStartMz → initialize() → [restart/upgrade] → manipulate(phase=0)\n  → [restart/upgrade] → manipulate(phase=1) → [restart/upgrade] → validate()\n```\n\nBoth manipulate phases always run. validate() may run multiple times.\n\n## Decorators and Version Gating\n\n### Disable a check\n```python\nfrom materialize.checks.checks import disabled\n\n@disabled(ignore_reason=\"due to SQL-450\")\nclass MyBrokenCheck(Check):\n    ...\n```\n\n### Non-idempotent external interactions\nIf a check ingests non-UPSERT data into Kafka/Postgres (i.e., replaying manipulate would duplicate data), annotate it:\n```python\nfrom materialize.checks.checks import externally_idempotent\n\n@externally_idempotent(False)\nclass MyKafkaCheck(Check):\n    ...\n```\n\n### Version-gated checks\nFor features that only exist from a certain version, override `_can_run`:\n```python\nfrom materialize.checks.executors import Executor\nfrom materialize.mz_version import MzVersion\n\nclass MyNewFeature(Check):\n    def _can_run(self, e: Executor) -> bool:\n        return self.base_version >= MzVersion.parse_mz(\"v0.80.0-dev\")\n```\n\n### Version-conditional testdrive lines\nUse `[version>=VVMMPP]` or `[version<VVMMPP]` prefixes for syntax that changed between versions (e.g., v0.92.0 → `9200`):\n```\n>[version<9200] CREATE SOURCE s FROM LOAD GENERATOR COUNTER (SCALE FACTOR 0.01)\n>[version>=9200] CREATE SOURCE s FROM LOAD GENERATOR COUNTER\n```\n\n## Tips for Good Checks\n\n1. **Naming**: Prefix ALL object names with a unique string (e.g., `my_feature_`) to avoid collisions with other checks running in the same Mz instance.\n2. **manipulate() returns exactly 2 elements**: The framework asserts this.\n3. **validate() is idempotent**: It may run multiple times. Use TEMPORARY objects or DROP at end.\n4. **Create objects in manipulate, not just initialize**: This tests that DDL works after restarts/upgrades.\n5. **Create derived objects on both old and new base objects**: e.g., a materialized view on a table from initialize AND on a table from manipulate.\n6. **Exercise full SQL syntax breadth**: Test every WITH option, every variant of the feature.\n7. **Ingest data in every phase**: Ensure data flows during restarts/upgrades.\n8. **Use `dedent()` on all testdrive strings**: Required for proper testdrive parsing.\n\n## Helper Methods on Check\n\n- `self.base_version`: The MzVersion the test started from (useful for upgrade scenarios).\n- `self.current_version`: The MzVersion currently running.\n- `self._kafka_broker()`: Returns the broker connection string for testdrive.\n- `self._default_cluster()`: Returns `\"quickstart\"`.\n- `self.rng`: A seeded `Random` instance for deterministic randomness.\n\n## Running\n\n```bash\n# Run a specific check against a specific scenario\nbin/mzcompose --find platform-checks run default --scenario=RestartEntireMz --check=MyFeature\n\n# Run with no restarts (useful for debugging the check itself)\nbin/mzcompose --find platform-checks run default --scenario=NoRestartNoUpgrade --check=MyFeature\n\n# Run all checks (default)\nbin/mzcompose --find platform-checks run default --scenario=RestartEntireMz\n```\n\n## Debugging CI Failures\n\n1. Find the scenario name near the top of the log: `Running ... --scenario=RestartEnvironmentdClusterdStorage`\n2. Find the failing check just before the error: `Running validate() from <materialize.checks.threshold.Threshold ...>`\n3. Reproduce locally: `bin/mzcompose --find platform-checks run default --scenario=RestartEnvironmentdClusterdStorage --check=Threshold`\n4. Try `--scenario=NoRestartNoUpgrade` to isolate whether the failure is restart/upgrade-related.\n",".agents/skills/mz-pr-review/SKILL.md":"---\nname: mz-pr-review\ndescription: >\n  Local code review of current branch vs Materialize standards. Trigger:\n  \"review my code\", \"review my changes\", \"check my diff\", \"does this look ok\",\n  \"what do you think of this PR\", \"code review\", or look over changes before\n  merging. Also PR number + wants feedback on quality, style, correctness.\n  For changes under `console/`, use mz-console-review instead.\nargument-hint: [base-branch]\nallowed-tools: [Bash, Read, Grep, Glob, Task]\n---\n\nPerform a local code review of the current branch's changes against Materialize project standards.\n\n## Steps\n\n1. Parse arguments from: $ARGUMENTS — a PR number, base branch, or nothing.\n2. Get the diff using the first method that works:\n   - **PR number given** (e.g. `123`): `gh pr diff 123`\n   - **git available**: `git diff <base>...HEAD` (default base: `main`)\n   - **jj available**: `jj diff -r <revset>` (default: diff from trunk)\n3. Get the file list from the same diff (add `--stat` for git, `--stat` for jj, or `gh pr diff 123 --stat` for PR).\n4. Review the diff against the checklists below.\n5. Present findings organized as: **Blocking**, **Strong suggestions**, **Nits**.\n\n## Review checklist\n\nThe overall developer guide for reviewing changes is defined in `doc/developer/guide-changes.md`, always read and follow its guidance.\n\n### Tests\n- Every behavior change has at least one new or modified test.\n- SQL/query behavior → look for `.slt` in `test/sqllogictest/`.\n- Wire/protocol behavior → look for `.pt` in `test/pgtest/`.\n- Rust logic/types/APIs → add or extend a Rust unit test in the crate (e.g. `#[cfg(test)]` or `tests/`); run with `cargo test -p mz-<crate>`.\n- Prefer testing observable behavior (SQL results, wire protocol) over implementation details.\n- Red flag: behavior change with no test changes.\n- For more testing guidelines, read `doc/developer/guide-testing.md`\n\n### LIR schema registry\nApplies when the diff changes the serde-visible shape of a type the LIR\nschema reaches: `src/compute-types/src/plan.rs`,\n`src/compute-types/src/plan/**`, a `*Func` payload, or a plan-reachable type\nin `mz-expr` or `mz-repr`.\n- `src/compute-types/tests/snapshots/lir_v{N}.json` is regenerated in the\n  same PR, and the diff matches the intended format change. Red flag: a\n  serde-visible change to a plan type with no snapshot diff.\n- If `LIR_VERSION` in `src/compute-types/src/plan.rs` had shipped, it is\n  bumped and the old `lir_v{N}.json` is left untouched rather than rewritten.\n- `Row` and `EvalError` never appear directly in plan types, only as\n  `StableRow` / `StableEvalError`, enforced by\n  `lir_schema_contains_only_stable_types`.\n\n### Scalar function registry\nApplies when the diff touches `UnaryFunc`, `BinaryFunc`, or `VariadicFunc`\n(`src/expr/src/scalar/func.rs`, `src/expr/src/scalar/func/**`, `#[sqlfunc]`\nbodies).\n- The registry snapshots under `src/compute-types/tests/snapshots/` are\n  regenerated in the same PR. A property change moves `func_registry.json`\n  and the current version's entry in `func_registry_digests.json`, a\n  `#[sqlfunc]` declaration or body change moves `func_registry_source.json`\n  alone, and a change to a hand-written variant's body moves neither.\n- Every new variant with a payload has a `Sample` in\n  `src/expr/src/scalar/func/registry.rs`, with a labeled second sample when a\n  property depends on the payload.\n- A removed function, a changed property, or a `body_fingerprint` change that\n  alters results for some input bumps `LIR_VERSION` in\n  `src/compute-types/src/plan.rs` if that version has shipped, and leaves the\n  old digest in `func_registry_digests.json` untouched. Additions need no\n  bump.\n- The PR description names the added, removed, and changed functions and,\n  for body changes, states whether results change.\n\n### Code style (Rust)\n- **Imports:** `std` → external crates → `crate::`; one `use` per module; prefer `crate::` over `super::` in non-test code.\n- **Errors:** Structured with `thiserror`; no bare `anyhow!(\"...\")`. `Display` should not print full error chain.\n- **Async:** Use `ore::task::spawn` / `spawn_blocking`, not raw `tokio::spawn`.\n- **Tests:** `#[mz_ore::test]`; panic in tests rather than returning `Result`.\n\n### Code style (SQL)\n- Keywords capitalized (`SELECT`, `FROM`); identifiers lowercase.\n- No space between function name and `(`.\n\n### Error messages\n- Primary: short, factual, lowercase first letter, no trailing punctuation.\n- Detail/hint: complete sentences, capitalized, period.\n- No \"unable\", \"bad\", \"illegal\", \"unknown\"; say what kind of object.\n\n### Sensitive data handling\n- Types holding passwords, keys, tokens, or credentials should use `mz_ore::secure::{SecureString, SecureVec}` or `zeroize::Zeroizing<T>` (from `mz_ore::secure`).\n- Sensitive types should **not** derive `Clone` or `Debug` (use custom `Debug` that redacts).\n- Stack-local buffers holding derived keys, nonces, or HMAC outputs should be wrapped in `Zeroizing<T>`.\n- See `doc/developer/generated/ore/secure.md` for full guidance and `src/ssh-util/src/keys.rs` for a reference implementation.\n\n### Architecture\n- **Simplicity:** No incidental complexity; simplify redundant logic.\n- **No special casing:** Prefer composable design over extra booleans/branches.\n- **Encapsulation:** sql-parser = grammar only (no semantic validation); sql = planning + semantics.\n- **Dependencies:** New crates must be justified.\n- For more design guidelines read: `doc/developer/best-practices.md`\n\n### Polish\n- No leftover `// XXX`, `// FIXME`, `dbg!`, `println!`, or commented-out code.\n- No unrelated formatting changes in untouched code.\n- New public items should have doc comments.\n\n### Release notes\n\nRelease notes are auto-generated per release from PR descriptions (the [`/mz-release-notes` skill](https://github.com/MaterializeInc/mz-skills/blob/main/.agents/skills/mz-release-notes/SKILL.md));\nauthors don't write them. So for a user-visible change, check that the PR *description* states the\nuser-observable effect in user-facing terms — not only implementation detail — so the classifier includes it.\n\n## One semantic change rule\n\nThe PR should do one thing. If it spans multiple CODEOWNERS areas (e.g. sql-parser + sql planner), consider suggesting a split.\n\n## Rules\n\n- Review the code, not the author. Explain the *why* behind suggestions.\n- Use **nit:** for preferences where reasonable people could disagree.\n- If the PR improves overall codebase health and blocking items are addressed, say so.\n- Do NOT make any changes — this is read-only review.\n",".agents/skills/mz-profile/SKILL.md":"# mz-profile\n\nPath: `.agents/skills/mz-profile/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-profile/SKILL.md)",".agents/skills/mz-query-perf/SKILL.md":"# mz-query-perf\n\nPath: `.agents/skills/mz-query-perf/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-perf/SKILL.md)",".agents/skills/mz-query-tracing/SKILL.md":"# mz-query-tracing\n\nPath: `.agents/skills/mz-query-tracing/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-tracing/SKILL.md)",".agents/skills/mz-release-signoff/SKILL.md":"# mz-release-signoff\n\nPath: `.agents/skills/mz-release-signoff/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/SKILL.md)",".agents/skills/mz-run/SKILL.md":"# mz-run\n\nPath: `.agents/skills/mz-run/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-run/SKILL.md)",".agents/skills/mz-test/SKILL.md":"# mz-test\n\nPath: `.agents/skills/mz-test/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-test/SKILL.md)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/AGENTS.md)","CLAUDE.md":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/CLAUDE.md)","console/AGENTS.md":"# console - AI Agent Protocol & Instructions\n\nPath: `console/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/AGENTS.md)","console/CLAUDE.md":"# console - Claude Agent Guidelines & System Prompt\n\nPath: `console/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/CLAUDE.md)",".agents/skills/README.md":"# Skills Skill\n\nPath: `.agents/skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/README.md)",".claude/commands/update-docs.md":"# Command: /update-docs\n\nPath: `.claude/commands/update-docs.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.claude/commands/update-docs.md)",".agents/skills/mz-console-review/cost-and-structure.md":"# Mz-console-review Skill\n\nPath: `.agents/skills/mz-console-review/cost-and-structure.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-console-review/cost-and-structure.md)",".agents/skills/mz-debug-ci/bk-fallback.md":"# Mz-debug-ci Skill\n\nPath: `.agents/skills/mz-debug-ci/bk-fallback.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/bk-fallback.md)",".agents/skills/mz-debug-ci/rare-failures.md":"# Mz-debug-ci Skill\n\nPath: `.agents/skills/mz-debug-ci/rare-failures.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/rare-failures.md)","ci/plugins/cloudtest/README.md":"# cloudtest Documentation\n\nPath: `ci/plugins/cloudtest/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/cloudtest/README.md)","ci/plugins/mzcompose/README.md":"# mzcompose Documentation\n\nPath: `ci/plugins/mzcompose/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/mzcompose/README.md)","ci/plugins/scratch-aws-access/README.md":"# scratch-aws-access Documentation\n\nPath: `ci/plugins/scratch-aws-access/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/scratch-aws-access/README.md)",".agents/skills/mz-release-signoff/references/adapter.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/adapter.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/adapter.md)",".agents/skills/mz-release-signoff/references/compute.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/compute.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/compute.md)",".agents/skills/mz-release-signoff/references/persist.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/persist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/persist.md)",".agents/skills/mz-release-signoff/references/reference-dashboards.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/reference-dashboards.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/reference-dashboards.md)",".agents/skills/mz-release-signoff/references/sources-and-sinks.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/sources-and-sinks.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/sources-and-sinks.md)","doc/developer/generated/compute/extensions/_module.md":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/_module.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/_module.md)","doc/developer/generated/compute/extensions/arrange.md":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/arrange.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/arrange.md)","doc/developer/generated/compute/extensions/reduce.md":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/reduce.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/reduce.md)","doc/developer/generated/compute/extensions/temporal_bucket.md":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/temporal_bucket.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/temporal_bucket.md)","doc/developer/generated/mz-deploy/cli/commands/_module.md":"# Command: /module\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/_module.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/_module.md)","doc/developer/generated/mz-deploy/cli/commands/abort.md":"# Command: /abort\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/abort.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/abort.md)","doc/developer/generated/mz-deploy/cli/commands/apply_all.md":"# Command: /apply_all\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_all.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_all.md)","doc/developer/generated/mz-deploy/cli/commands/apply_connections.md":"# Command: /apply_connections\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_connections.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_connections.md)","doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md":"# Command: /apply_network_policies\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md)","doc/developer/generated/mz-deploy/cli/commands/apply_objects.md":"# Command: /apply_objects\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_objects.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_objects.md)","doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md":"# Command: /apply_secrets\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md)","doc/developer/generated/mz-deploy/cli/commands/apply_sources.md":"# Command: /apply_sources\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_sources.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_sources.md)","doc/developer/generated/mz-deploy/cli/commands/apply_tables.md":"# Command: /apply_tables\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_tables.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_tables.md)","doc/developer/generated/mz-deploy/cli/commands/clean.md":"# Command: /clean\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/clean.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/clean.md)","doc/developer/generated/mz-deploy/cli/commands/clusters.md":"# Command: /clusters\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/clusters.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/clusters.md)","doc/developer/generated/mz-deploy/cli/commands/compile.md":"# Command: /compile\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/compile.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/compile.md)","doc/developer/generated/mz-deploy/cli/commands/debug.md":"# Command: /debug\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/debug.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/debug.md)","doc/developer/generated/mz-deploy/cli/commands/delete.md":"# Command: /delete\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/delete.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/delete.md)","doc/developer/generated/mz-deploy/cli/commands/describe.md":"# Command: /describe\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/describe.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/describe.md)","doc/developer/generated/mz-deploy/cli/commands/dev.md":"# Command: /dev\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/dev.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/dev.md)","doc/developer/generated/mz-deploy/cli/commands/explain.md":"# Command: /explain\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/explain.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/explain.md)","doc/developer/generated/mz-deploy/cli/commands/grants.md":"# Command: /grants\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/grants.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/grants.md)","doc/developer/generated/mz-deploy/cli/commands/list.md":"# Command: /list\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/list.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/list.md)","doc/developer/generated/mz-deploy/cli/commands/lock.md":"# Command: /lock\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/lock.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/lock.md)","doc/developer/generated/mz-deploy/cli/commands/log.md":"# Command: /log\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/log.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/log.md)","doc/developer/generated/mz-deploy/cli/commands/mcp.md":"# Command: /mcp\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/mcp.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/mcp.md)","doc/developer/generated/mz-deploy/cli/commands/new_project.md":"# Command: /new_project\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/new_project.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/new_project.md)","doc/developer/generated/mz-deploy/cli/commands/profile.md":"# Command: /profile\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/profile.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/profile.md)","doc/developer/generated/mz-deploy/cli/commands/promote.md":"# Command: /promote\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/promote.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/promote.md)","doc/developer/generated/mz-deploy/cli/commands/roles.md":"# Command: /roles\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/roles.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/roles.md)","doc/developer/generated/mz-deploy/cli/commands/setup.md":"# Command: /setup\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/setup.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/setup.md)","doc/developer/generated/mz-deploy/cli/commands/setup_schema.md":"# Command: /setup_schema\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/setup_schema.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/setup_schema.md)","doc/developer/generated/mz-deploy/cli/commands/sql.md":"# Command: /sql\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/sql.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/sql.md)","doc/developer/generated/mz-deploy/cli/commands/stage.md":"# Command: /stage\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/stage.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/stage.md)","doc/developer/generated/mz-deploy/cli/commands/test.md":"# Command: /test\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/test.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/test.md)","doc/developer/generated/mz-deploy/cli/commands/wait.md":"# Command: /wait\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/wait.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/wait.md)","doc/developer/generated/mz-deploy/cli/commands/test/lower.md":"# Command: /lower\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/test/lower.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/test/lower.md)"},"files":{".agents/skills/mz-debug-ci/SKILL.md":"---\nname: mz-debug-ci\ndescription: >\n  Investigate CI failures on PR via gh + Buildkite MCP or bk CLI. Trigger: failing checks,\n  Buildkite failures, CI issues — \"why is CI red\", \"build broken\",\n  \"checks failing\", \"what went wrong in CI\", \"nightly broke\",\n  \"tests failing on this PR\", or pasted Buildkite URL. Also PR number +\n  why failing.\nargument-hint: <PR number, GitHub PR URL, or Buildkite build URL>\n---\n\nInvestigate CI failures for a Materialize PR or Buildkite build.\n\nIf the input is a Buildkite build URL rather than a PR (a scheduled nightly on\nmain, a release-qualification build), start directly at \"Listing a\nbuild's failed jobs directly\", taking `<PIPELINE>` and `<BUILD_NUMBER>`\nstraight from the URL:\n`https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>`,\nthen continue from Step 2.\n\n## Prerequisites\n\nThis skill requires `gh` (GitHub CLI), authenticated, plus one of two ways\nto reach Buildkite: the Buildkite MCP server's tools (preferred, next\nsection) or `bk` (Buildkite CLI), authenticated. If any of these turns out\nto be missing or unauthenticated, **stop immediately** and tell the user\nwhat to fix (`bk configure` or `bk auth login` for the Buildkite CLI, or the\nMCP setup below). Do not attempt to use the REST API directly or any other\nworkaround.\n\n## Buildkite MCP: preferred when available\n\nWhen the Buildkite MCP server's tools are available in the session (tool\nnames like `get_build_failure_summary`, `search_logs`, ...), prefer them\nover `bk` everywhere below: structured JSON with the\njob-state semantics already applied, and no log cleanup needed. The server documents its\nown tools; this section adds only what it does not know:\n\n- `org_slug` is `materialize`. `get_build_failure_summary` replaces \"Listing\n  a build's failed jobs directly\", Step 2, and often Step 3;\n  `search_logs`/`read_logs`/`tail_logs` replace the `bk job log` pipelines;\n  the artifact and annotation tools replace the corresponding `bk` commands.\n  Everything Materialize-specific (annotation contents, `bin/ci-failures`,\n  `bin/ci-shards`, Steps 4-5) applies unchanged.\n- The summary's job list is bounded by the response's `job_limit` (10 on the\n  hosted server, where a larger `max_jobs` is silently clamped), with terminal\n  problem jobs sorted before downstream broken ones. On `jobs_truncated:\n  true`, get the definitive failed-job list from `list_jobs` with\n  `state: \"failed,timed_out\"` and `include_retried_jobs: false`. That last\n  parameter is what preserves triage semantics: the server defaults it to\n  true, which counts every failed attempt of a retried job separately,\n  including jobs that passed on retry. The summary bounds annotation content\n  too: on `content_truncated: true`, fetch the full set with\n  `list_annotations`.\n\nIf the MCP is absent, read `bk-fallback.md` in this skill's directory for\nthe `bk` command recipes and continue with those. At the end, mention to\nthe user that they can set the server up (hosted read-only endpoint:\n`claude mcp add --scope user --transport http buildkite https://mcp.buildkite.com/mcp/readonly`;\nit saves tool calls but costs somewhat more tokens).\n\n## Search existing failures by pattern\n\nSearch for already recorded failures in CI with a short, stable error\nsubstring. The output is JSON whose `content` blobs are huge, so project it\ndown, and read the total from `.meta.totalRowCount`, which ignores the row\ncap and is the \"how chronic is this?\" number:\n\n```bash\nbin/ci-failures 'foobar' 2>/dev/null | jq -r '.meta.totalRowCount,\n  (.data[] | [.build_date[0:10], .build_identifier, .test_suite, .issue] | @tsv)'\n```\n\nThe positional pattern matches failure content. `--search <text>` replaces\nthe positional pattern and matches across build, test, issue, content, and\nbranch at once. Narrow results with `--branch main`, `--version`, `--issue`,\n`--test` (the Buildkite job name), `--build`, `--start-date`, or\n`--end-date`; `--size` raises the row cap (default 31, max 100).\n\n## Step 1: Find the build\n\nParse `$ARGUMENTS` to get the PR number (a plain number or a\n`https://github.com/MaterializeInc/materialize/pull/<PR_NUMBER>` URL), then\nlist the failing checks:\n\n```bash\ngh pr checks <PR_NUMBER> 2>&1\n```\n\nFailing rows link to\n`https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>#<JOB_ID>`,\nthe identifiers used throughout this skill.\n\n### Listing a build's failed jobs directly\n\nBuildkite mirrors each job's result into GitHub as a\n`buildkite/<pipeline>/<job>` commit status (never-dispatched jobs post\nnothing), but `gh pr checks` reads the PR's current head commit only. A build\ntherefore shows up there only if it ran on exactly that commit. For everything\nelse (scheduled nightlies on main, release-qualification builds, a PR nightly\nfrom before the latest push), list the genuinely failed jobs via the API:\n`list_jobs` as described in the MCP section, or with `bk` the listing recipe\nin `bk-fallback.md`. Either way, also grab the build's commit and branch:\nStep 5's known-vs-new checks need the commit, and neighboring-build lookups\nneed the branch.\n\nJob-state semantics matter here:\n- The failure states are `failed` and `timed_out`, the same set as\n  `BUILDKITE_RELEVANT_FAILED_BUILD_STEP_STATES` in\n  `misc/python/materialize/buildkite_insights/buildkite_api/buildkite_constants.py`.\n- `broken` is not a failure. Buildkite marks a job `broken` when its\n  configuration prevents it from running, for example a branch filter or an\n  `if:` condition that evaluates false, so the job never dispatches. Every\n  build contains broken jobs (a routine main nightly has dozens), so counting\n  them massively over-counts failures.\n- Jobs with `soft_failed: true` are allowed to fail without making the build\n  red; no current CI step sets it, so filtering it is defensive.\n- `waiting_failed` is not a failure either: the job's dependency (usually a\n  build step) failed, so it never started. Its presence is informative,\n  though. A PR build full of `waiting_failed` jobs ran no tests at all, so\n  report that fixing the visible failures is necessary but no proof the\n  build then goes green.\n- Retried jobs (automatic or manual) appear only as their latest attempt, so\n  a job that failed and then passed on retry counts as passed. That is the\n  right verdict for build triage; fetch the earlier attempts only when an\n  in-depth investigation needs them. A manual retry that failed again is\n  itself a signal: someone already tried to shake the failure off and it\n  reproduced.\n\nTo find a PR's builds that are not on its current head in the first place (an\nolder nightly, a pre-push run), get the branch with\n`gh pr view <PR_NUMBER> --json headRefName` and filter Buildkite builds by\nbranch; the MCP's `list_builds` takes a `branch:` filter, and fork branches\nare named `<owner>:<branch>` on Buildkite.\n\n## Step 2: Check annotations first\n\n**Before diving into logs**, fetch the build annotations. They contain pre-extracted error messages, stack traces, and links to known flaky test issues — this saves significant time compared to grepping through raw logs. With the MCP they arrive in `get_build_failure_summary` (or `list_annotations`); with `bk`, use the annotations recipe in `bk-fallback.md`.\n\nBesides the error itself, an error annotation (`style` `\"error\"`) carries\nknown-issue links (Linear keys like `CPU-170`, or legacy\n`database-issues/#NNNN`) and the job's main-branch history, a flaky-test\nindicator.\n\nThings to know when reconciling annotations against jobs:\n\n- Error annotations persist from failed attempts even when a retry later\n  passed, so a build can have more error annotations than failed jobs. A\n  `bin/ci-annotate-errors` annotation's `context` field is `<JOB_ID>-error`,\n  and that job id can belong to a retried attempt that the default job\n  listing does not contain. (Other producers use other contexts, e.g.\n  `images-not-public` from the build step.)\n- Only jobs running through the mzcompose/cloudtest plugins get annotations\n  and `bin/ci-failures` rows (both come from `bin/ci-annotate-errors`). Plain\n  shell steps (lint, Security advisories, and similar checks) produce neither,\n  so a real failure there has no annotation at all. Go straight to its job\n  log.\n- Info-style annotations (\"<job> succeeded with known error logs\") mean the\n  job passed and every logged error matched an open known issue; they are\n  not failures. When a job went red via \"Test succeeded, but unknown errors\n  found in logs\" (see Step 3), the unknown and potential-regression entries\n  in its annotation are what turned it red. The reverse also exists: a\n  failed job turns green (\"<job> would have failed with known error logs\")\n  when all its logged errors match issues marked `ci-ignore-failure: true`.\n\n## Step 3: Fetch logs when needed\n\nOnly fetch full logs when annotations don't provide enough detail. On PR test\nbuilds, read compile and lint job logs first (clippy, lint-and-rustfmt): they\noften explain every downstream failure.\n\nFetch and search logs with the MCP's `search_logs`/`read_logs`/`tail_logs`,\nor with `bk` use the job-log recipes in `bk-fallback.md`. The log tail can be a\nred herring: some jobs print long non-error output after the actual error\n(cargo deny's dependency tree runs hundreds of lines past the advisory), so\nwhen the tail shows no error, search or grep for the error instead of\ntailing more.\n\nNOTE: for mzcompose-based jobs (testdrive, SQLsmith/SQLancer, platform checks,\n...) the job console log holds only the harness's output. The services' own\noutput (environmentd/clusterd panics and errors) goes to `services.log` and\nsimilar files inside the job's log artifacts. A panic that is absent from\nthe job's console log can still be the failure, so never conclude \"this\nerror did not happen in this run\" from it alone. Check the annotations or\n`bin/ci-failures` instead, both are fed by `bin/ci-annotate-errors`, which\nscans the uploaded log artifacts (`services.log`, `run.log`, junit XML, ...)\nat the end of each job. The same scan can fail a job whose own workflow\npassed: the console log then reads `Test succeeded, but unknown errors found\nin logs, marking as failed`, and the cause is whatever the scan found in the\nartifacts, not a test failure. Alternatively, download the log artifacts and\ngrep them directly.\n\n### Artifacts\n\nJobs upload artifacts (junit XML, service logs, coredumps, ...). Use the\nMCP's `list_artifacts_for_build`/`list_artifacts_for_job`/`get_artifact`,\nor with `bk` use the artifacts recipes in `bk-fallback.md`.\n\n### Shard contents\n\nSharded jobs (SLT, testdrive, platform checks, feature benchmark, ...) record\nthe workflows/files/scenarios they ran in build meta-data. `bin/ci-shards`\nshows it:\n\n```bash\n# Mapping of every sharded job to what it ran, with a link per job whose\n# `#` fragment is the `<JOB_ID>` for fetching job logs (omit links with --no-url)\nbin/ci-shards https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>\n\n# Which job(s) ran a specific item (test file, scenario, or workflow,\n# `workflow_foo_bar` function names are matched as `foo-bar`)\nbin/ci-shards https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER> numeric.td\n```\n\n## Step 4: Categorize failures\n\nUse these Materialize-specific patterns to diagnose:\n\n### Clippy errors\nCode lint issues in changed files. Fix the code, not the lint config.\n\n### `check-test-flags` lint failure\nA new dyncfg (`Config::new(\"name\", ...)`) was introduced but not registered in the required places. Reproduce with `bin/lint-test-flags`.\n- `misc/python/materialize/parallel_workload/action.py` (`FlipFlagsAction.flags_with_values` or `uninteresting_flags`)\n- `misc/python/materialize/mzcompose/__init__.py` (get_variable_system_parameters / get_minimal_system_parameters / UNINTERESTING_SYSTEM_PARAMETERS)\n\n### Doctests failure\nThe Doctests job runs `bin/doc --document-private-items` (`ci/test/lint-doc.sh`), which sets `RUSTDOCFLAGS=-D warnings` and uses the pinned nightly. Reproduce on one crate with `bin/doc --document-private-items -p <crate>`. A common cause is a public doc comment linking a private item (`rustdoc::private_intra_doc_links`), which fails even with `--document-private-items`. `cargo doc` stops at the first failing crate, so a fix can reveal errors in the next crate.\n\n### Merge skew cargo check failure\nThis PR-only job (`ci/test/check-merge-with-target.sh`) merges the PR with its target branch and runs `cargo check --all-targets`. When it is red while the PR's own build and clippy jobs pass, the branch compiles alone but not merged with current main. Rebase and fix as the mz-commit skill describes.\n\n### Testdrive cascades\nAfter one test crashes environmentd, all subsequent tests in that shard fail with `Name or service not known` or `connection closed`. **Only the first failure in a shard matters** — everything after it is a cascade. Look for the first `error:` or `FAIL` in the log.\n\nTestdrive shards with the same number (e.g., `testdrive-10` and `testdrive-with-alloydb-10`) run the same tests — if both fail, it's likely to be the same root cause.\n\n### SLT failures\nCheck whether it's wrong output (behavioral change) vs. connection error (crash/timeout). Wrong output means the query semantics changed. An `InconsistentViewOutcome` failure comes from CI's `--auto-index-selects` mode; the mz-test skill explains the mechanism and the exemption lists.\n\n### Timeouts\nA `timed_out` job hit its step budget (`timeout_in_minutes` in\n`ci/<pipeline>/pipeline.template.yml`). The annotation records only the fact\n(\"test timed out\"), never the cause. Find the cause in the console log: the\nlast completed unit of work, plus anything marked as still running when the\njob was canceled (nextest prints `SLOW [>1200.000s]` lines naming the stuck\ntests). If no such marker appears, compare wall-clock against the job's last\npassing run to tell a gradually outgrown budget from a hung test.\n\n### Infrastructure flakes\nObserved: failures in unrelated jobs while downloading from `github.com/.../releases/download/` were GitHub CDN outages. Retry the job instead of debugging it.\n\n### Rarer classes\n\nIf the failure matches one of these, read `rare-failures.md` in this\nskill's directory for its triage pattern: `New regression against\n<version>` (feature benchmark), a cargo-fuzz crash, a Miri failure, a\nlimits-test failure, or `ImagesNotPublicError`.\n\n## Step 5: Summarize\n\nGroup failures by **root cause**, not by job name — typically many failing\njobs share just 1-2 root causes. Per root cause, report a description, the\naffected jobs, and what to fix.\n\nDistinguish between issues that are clearly caused by the change under test vs. pre-existing flaky tests. The annotations often link to known flaky test issues — use these to identify pre-existing flakes vs. new regressions.\n\nMore ways to establish known vs new:\n\n- Check whether main already fixed it. Any build can run code that is behind\n  main: a nightly investigated the morning after, but also a fresh PR based\n  on an older main. From an up-to-date main checkout run\n  `git log <BUILD_COMMIT>..HEAD -- <suspect-file>` or\n  `git log -S '<error token>'`. A fix can also sit in a not-yet-merged PR,\n  invisible to git log: search open PRs for the step name or error token\n  (`gh pr list --state open --search '<...>'`); a PR's file list shows\n  which of the build's root causes it covers. Do this for every failure,\n  and name any hit in the summary so nobody authors a duplicate. When\n  citing a later build as evidence\n  of a fix, confirm the specific job's state there is `passed`: a build can\n  be green because the job was `broken` and never ran.\n- Known-issue tracking lives in Linear. Annotations and `bin/ci-failures`\n  cite issue keys like `CPU-170` or `SS-361`, which resolve to\n  `https://linear.app/materializeinc/issue/<KEY>`. Legacy references point at\n  GitHub `MaterializeInc/database-issues` instead.\n- Annotations and `bin/ci-failures` classify a failure three ways; for the\n  latter two, check the signature's history with `bin/ci-failures` before\n  treating the failure as new. A known-issue link means it matched an open\n  tracked issue. `\"UNKNOWN ERROR\"` means it matched no tracked issue, not\n  that the failure is new: a long streak under that label is a chronic\n  untracked flake worth filing. An issue reference tagged\n  `(POTENTIAL REGRESSION)` means it matched a tracked issue that is now\n  closed: a known signature with stale tracking, not by itself evidence of\n  a new regression; suggest to the user that the closed issue may need\n  reopening. The issue matching is a content heuristic and often wrong, so\n  verify that the matched issue actually describes the problem: search the\n  tracker for the error text, since the right issue may already exist open,\n  or the problem may need a totally new one.\n",".agents/skills/mz-benchmark/SKILL.md":"---\nname: mz-benchmark\ndescription: >\n  Add/modify/debug Materialize perf benchmark scenarios. Three frameworks:\n  Feature Benchmark (single-op micro), Scalability Test (SQL throughput under\n  concurrency), Parallel Benchmark (sustained latency via scenarios.py).\n  Trigger: \"benchmark\", \"feature benchmark\", \"scalability test\",\n  \"parallel benchmark\", \"performance regression\", \"micro-benchmark\", \"TPS\",\n  \"latency test\", or edits in feature_benchmark/scenarios/,\n  scalability/workload/workloads/, parallel_benchmark/scenarios.py.\n  Note: measurement, not panic-stress (see mz-parallel-workload).\n---\n\n# Benchmark Frameworks\n\nMaterialize has some benchmark frameworks targetting local Docker, each suited to different performance concerns. Choose the right one based on what you're measuring.\n\n## Decision Guide\n\n| Concern | Framework | Best For |\n|---------|-----------|----------|\n| Single-operation latency | **Feature Benchmark** | \"How fast is this SELECT / INSERT / CREATE INDEX?\" |\n| SQL throughput under concurrency | **Scalability Test** | \"How many QPS can we sustain at 1/4/16/64/256 concurrent clients?\" |\n| Sustained performance over time | **Parallel Benchmark** | \"Does latency degrade over a 2-minute mixed workload?\" |\n\nA Rust `cargo bench` target must reference the production allocator with a\n`mz-alloc-default` dev-dependency and `use mz_alloc_default as _;` (see\n`src/repr/benches/row.rs`). Without it the bench runs on the system allocator,\nnot jemalloc.\n\nScalability Test and Parallel Benchmark connect with psycopg 3 and leave\n`prepare_threshold` at its default of 5, so a query switches to a prepared\nstatement on its sixth execution per connection. Set it explicitly and warm\nconnections up when per-query latency matters.\n\n---\n\n## 1. Feature Benchmark (Micro-benchmarks)\n\nMeasures wall-clock time of individual SQL operations without concurrency. Uses testdrive fragments. Great for regression-testing specific code paths.\n\n### How to Add a Scenario\n\n1. Add a class in one of the `scenarios/*.py` files. Extend `Scenario` (default, runs in CI), `ScenarioBig` (longer, opt-in), or `ScenarioDisabled`.\n2. Implement `benchmark()` returning a `Td(...)` with timing markers (`/* A */` and `/* B */`).\n3. Optionally implement `shared()`, `init()`, `before()` if setup is required.\n\n### Scenario Template\n\n```python\nfrom materialize.feature_benchmark.action import TdAction\nfrom materialize.feature_benchmark.measurement_source import MeasurementSource, Td\nfrom materialize.feature_benchmark.scenario import Scenario\n\nclass MyCategory(Scenario):\n    \"\"\"Group parent - won't run (not a leaf).\"\"\"\n    pass\n\nclass MySpecificBenchmark(MyCategory):\n    \"\"\"Leaf class - this runs.\"\"\"\n\n    def init(self) -> list[TdAction]:\n        return [\n            self.table_ten(),  # Creates table 'ten' with values 0-9\n            TdAction(\"\"\"\n                > CREATE TABLE t1 (f1 INTEGER);\n                > INSERT INTO t1 SELECT {unique_values} FROM {join};\n                > SELECT COUNT(*) FROM t1;\n                {n}\n            \"\"\".format(\n                unique_values=self.unique_values(),\n                join=self.join(),\n                n=self.n(),\n            )),\n        ]\n\n    def benchmark(self) -> MeasurementSource:\n        return Td(\"\"\"\n> SELECT 1;\n  /* A */\n1\n\n> SELECT COUNT(*) FROM t1 WHERE f1 % 2 = 0;\n  /* B */\n{half_n}\n\"\"\".format(half_n=self.n() // 2))\n```\n\n### Timing Markers\n\n- `/* A */` - Start marker. Measurement begins AFTER this completes.\n- `/* B */` - End marker. Measurement ends when this completes.\n- Measures: time from end of A to end of B.\n- For a single query, use `SELECT 1; /* A */` as a dummy start to not measure the overhead of Testdrive starting up.\n\n### Scenario Phases (execution order)\n\n1. **`shared()`** - Runs once for BOTH instances (e.g., create Kafka topics)\n2. **`init()`** - Runs once per instance (create tables, populate data)\n3. **`before()`** - Runs before EACH iteration (restart, cleanup)\n4. **`benchmark()`** - Runs repeatedly, measured\n\nThe result is the minimum (`MinAggregation`) after discarding the first 3\nmeasurements. `ProbForMin(threshold=0.90)` in\n`test/feature-benchmark/mzcompose.py` can end a scenario after 6 measurements,\nso the verdict can rest on 3 values and one outlier can flip it. Read the raw\nper-iteration values before trusting a regression or improvement.\n\nThe measured cluster `cluster_default` is an unmanaged replica on the\n`clusterd` container, configured by the `Clusterd(...)` service in\n`create_clusterd_service`. That function maps `enable_unified_cluster` onto\nthe service, but other parameters that shape the replica process do not reach\nit. For example, `enable_timely_zero_copy` has no effect because `Clusterd`\nhardcodes its timely config.\n\n### Helpers\n\n- `self.n()` - Returns 10^SCALE (default SCALE=6 → 1,000,000)\n- `self.table_ten()` / `self.view_ten()` - Creates table/view with values 0-9\n- `self.unique_values()` - SQL expression generating N unique values via cartesian product\n- `self.join()` - SQL FROM clause for cartesian product joins\n- `self.keyschema()` / `self.schema()` - Kafka schema helpers\n\n### Tips\n\n- **Aim for ~1 second** query time. Too fast = noise; too slow = Unbearably slow CI.\n- **Test td fragments locally** with standalone testdrive before running the full benchmark.\n- **Use `SCALE`** to control data volume: `SCALE = 5` → 100K rows.\n- **Use `FIXED_SCALE = True`** if `--scale` should not affect your scenario. This is usually done when a scenario doesn't scale well, add an explanation.\n- **Use `RELATIVE_THRESHOLD`** to customize regression sensitivity per scenario.\n\n### Running\n\n```bash\nbin/mzcompose --find feature-benchmark run default --root-scenario=MyCategory\nbin/mzcompose --find feature-benchmark run default --root-scenario=MySpecificBenchmark\n```\n\n---\n\n## 2. Scalability Test (SQL Throughput Under Concurrency)\n\nMeasures TPS (transactions per second) at increasing concurrency levels (1, 2, 4, 8, ..., 256 clients). Compares Materialize against Postgres.\n\nThe load is closed-loop: `concurrency` threads each issue the next operation\nwhen the previous one completes (`benchmark_executor.py`). TPS is therefore\ncapped at concurrency divided by mean latency, and a flattening curve can be\nthe client cap rather than a server knee. Use Parallel Benchmark's `OpenLoop`\nto measure behavior at a fixed offered load, and report p99 and max with it.\n\n### How to Add a Workload\n\n1. Create a new file or add to an existing file in `workload/workloads/`.\n2. Extend a marker class (`DmlDqlWorkload`, `DdlWorkload`, or `ConnectionWorkload`).\n3. Implement `operations()` returning a list of `Operation` objects.\n\n### Workload Template (Simple DML/DQL)\n\n```python\nfrom materialize.scalability.operation.operations.operations import (\n    SelectOne,\n    InsertDefaultValues,\n    SelectCountInMv,\n)\nfrom materialize.scalability.workload.workload_markers import DmlDqlWorkload\n\nclass MyWorkload(DmlDqlWorkload):\n    def operations(self) -> list[\"Operation\"]:\n        return [InsertDefaultValues(), SelectCountInMv()]\n```\n\n### Workload Template (DDL with Data Exchange)\n\n```python\nfrom materialize.scalability.operation.operation_data import OperationData\nfrom materialize.scalability.operation.operations.operations import (\n    CreateTableX, PopulateTableX, SelectStarFromTableX, DropTableX,\n)\nfrom materialize.scalability.operation.scalability_operation import OperationChainWithDataExchange\nfrom materialize.scalability.workload.workload import WorkloadWithContext\nfrom materialize.scalability.workload.workload_markers import DdlWorkload\n\nclass MyDdlWorkload(WorkloadWithContext, DdlWorkload):\n    def amend_data_before_execution(self, data: OperationData) -> None:\n        data.push(\"table_seed\", data.get(\"worker_id\"))\n\n    def operations(self) -> list[\"Operation\"]:\n        return [\n            OperationChainWithDataExchange([\n                CreateTableX(), PopulateTableX(), SelectStarFromTableX(), DropTableX(),\n            ])\n        ]\n```\n\n### Custom Operations\n\n```python\nfrom materialize.scalability.operation.scalability_operation import SimpleSqlOperation\n\nclass MyCustomSelect(SimpleSqlOperation):\n    def sql_statement(self) -> str:\n        return \"SELECT COUNT(*) FROM t1 WHERE f1 > 500;\"\n```\n\n### Built-in Operations\n\n`SelectOne`, `SelectStar`, `SelectLimit`, `SelectCount`, `SelectCountInMv`, `SelectUnionAll`, `Update`, `InsertDefaultValues`, `CreateTableX`, `PopulateTableX`, `DropTableX`, `CreateMvOnTableX`, `DropMvOfTableX`, `Connect`, `Disconnect`.\n\n### Database Schema (auto-created)\n\nFor each workload, the framework creates: `CREATE TABLE t1 (f1 INTEGER DEFAULT 1)`, inserts a row, and creates `CREATE MATERIALIZED VIEW mv1 AS SELECT count(*) AS count FROM t1`.\n\n### Running\n\n```bash\nbin/mzcompose --find scalability run default --workload MyWorkload\nbin/mzcompose --find scalability run default --workload-group-marker DmlDqlWorkload\nbin/mzcompose --find scalability run default --min-concurrency 1 --max-concurrency 128\n```\n\n---\n\n## 3. Parallel Benchmark (Sustained Performance)\n\nMeasures latency distributions (p50/p95/p99) and QPS over a sustained load period (default 120s). Supports mixed workloads with both open-loop and closed-loop concurrency. Detects latency degradation over time via slope analysis.\n\n### How to Add a Scenario\n\n1. Add a class in `scenarios.py` extending `Scenario`.\n2. In `__init__`, call `self.init(phases=[...])` with `TdPhase` (setup) and `LoadPhase` (benchmark).\n\n### Scenario Template\n\n```python\nfrom materialize.parallel_benchmark.framework import (\n    ClosedLoop,\n    LoadPhase,\n    OpenLoop,\n    Periodic,\n    ReuseConnQuery,\n    Scenario,\n    StandaloneQuery,\n    TdPhase,\n)\n\nclass MyScenario(Scenario):\n    def __init__(self, c, conn_infos):\n        self.init(\n            phases=[\n                # Setup phase (testdrive)\n                TdPhase(\"\"\"\n                    > CREATE TABLE bench_t (f1 INTEGER);\n                    > INSERT INTO bench_t SELECT generate_series(1, 10000);\n                    > CREATE MATERIALIZED VIEW bench_mv AS\n                      SELECT COUNT(*) AS c FROM bench_t;\n                    > SELECT c FROM bench_mv;\n                    10000\n                \"\"\"),\n                # Benchmark phase\n                LoadPhase(\n                    duration=120,  # seconds\n                    actions=[\n                        OpenLoop(\n                            action=StandaloneQuery(\n                                \"SELECT c FROM bench_mv\",\n                                conn_infos[\"materialized\"],\n                            ),\n                            dist=Periodic(per_second=100),\n                            report_regressions=True,\n                        ),\n                        ClosedLoop(\n                            action=ReuseConnQuery(\n                                \"INSERT INTO bench_t VALUES (1)\",\n                                conn_infos[\"materialized\"],\n                            ),\n                            report_regressions=True,\n                        ),\n                    ],\n                ),\n            ],\n            guarantees={\n                \"SELECT c FROM bench_mv (standalone)\": {\n                    \"qps\": 10,\n                    \"p99\": 500,\n                },\n            },\n        )\n```\n\n### Action Types\n\n| Class | Behavior | Use When |\n|-------|----------|----------|\n| `StandaloneQuery` | New connection per operation | Testing connection overhead + query |\n| `ReuseConnQuery` | Persistent single connection | Testing pure query latency |\n| `PooledQuery` | Draws from connection pool | Testing pooled throughput (set `conn_pool_size > 0`) |\n\n### Load Patterns\n\n| Class | Behavior |\n|-------|----------|\n| `OpenLoop(dist=Periodic(per_second=N))` | Fixed-rate: N operations/second regardless of latency |\n| `OpenLoop(dist=Gaussian(mean, stddev))` | Variable-rate: sleep drawn from gaussian distribution |\n| `ClosedLoop(...)` | Sequential: next operation starts when previous completes |\n\n### Guarantees and Regression Thresholds\n\n```python\n# Absolute guarantees (fail if not met)\nguarantees={\n    \"SELECT 1 (standalone)\": {\"qps\": 15, \"p99\": 400, \"max\": 500},\n}\n\n# Relative thresholds (fail if regression vs baseline)\nregression_thresholds={\n    \"SELECT 1 (pooled)\": {\"qps\": 0.8, \"p99\": 1.3},  # QPS must be >=80% of baseline\n}\n```\n\nAction names are auto-generated as `\"{query} ({connection_type})\"`.\n\n### Available Metrics\n\n`queries`, `qps`, `min`, `max`, `avg`, `p50`, `p95`, `p99`, `p99.9`, ..., `p99.999999`, `std`, `slope`.\n\n### Disabling a Scenario\n\n```python\nfrom materialize.parallel_benchmark.framework import disabled\n\n@disabled(\"Reason for disabling\")\nclass MyScenario(Scenario):\n    ...\n```\n\n### Running\n\n```bash\nbin/mzcompose --find parallel-benchmark run default --scenario MyScenario\nbin/mzcompose --find parallel-benchmark run default --scenario MyScenario --load-phase-duration 60\n```\n",".agents/skills/mz-console-review/SKILL.md":"---\nname: mz-console-review\ndescription: >\n  Review a Console (`console/`) PR or diff the way Console reviewers do: run the\n  change to verify claims, check it against the patterns and helpers the app\n  already has, and post findings as one GitHub review. Trigger: \"review this\n  console PR\", \"review my console changes\", \"does this console change look ok\",\n  a github.com/MaterializeInc/materialize/pull/<N> link whose files are under\n  `console/`, or a request to look over a React/TypeScript diff in this repo.\n  For Rust changes use mz-pr-review instead.\nargument-hint: [pr-number]\nallowed-tools: [Bash, Read, Grep, Glob]\n---\n\n# Console code review\n\n`mz-pr-review` covers the Rust checklists, none of which apply under `console/`.\nThe canonical Console rules live in `console/CLAUDE.md` (component conventions,\nimport restrictions, TypeScript and React patterns, testing philosophy) and in\n`console/doc/` (`guide-testing.md`, `guide-tanstack-table.md`,\n`mz-backwards-compatibility.md`). Read the sections the diff touches. Do not\nrestate them in comments, cite them.\n\nThis skill is what Console reviewers actually flag. Section 4 is distilled from\nroughly 570 reviewer comments on about 150 Console PRs from 2025 and 2026, and is\nordered by how often each kind of feedback came up. Sections 5 and 6 are not from\nthat tally. They cover cost and structure that reviewers rarely catch by\nreading, measured on `mz_catalog_server` and in render-count probes.\n\n## 1. Get the diff, and make it runnable\n\nRemotes are usually `upstream` = `MaterializeInc/materialize` and `origin` = a\npersonal fork, so a PR branch will not resolve against `origin`:\n\n```bash\ngit fetch upstream pull/<N>/head:pr-<N>-review\n```\n\nTo run anything you need the files inside `console/`, because `node_modules`\nlives at `console/node_modules` and a fresh worktree does not have it. Check the\nfiles into a tree with no local edits under `<path>`, run, then clean up. The\nrestore removes files the PR added and reverts the ones it changed, and it also\ndiscards any local edits under `<path>`:\n\n```bash\ngit checkout pr-<N>-review -- console/src/<path>\n(cd console && npx vitest run src/<path>)\ngit restore --source=HEAD --staged --worktree -- console/src/<path>\ngit branch -D pr-<N>-review\n```\n\nConfirm `git status --short` is empty before you finish.\n\n## 2. Verify every behavioral claim before writing it\n\nRun the suite and state the result (\"30/30 pass\"). Probe anything you intend to\nassert. A claim about draw order, nullability, or event handling that you reasoned\nout but did not execute is a claim you will get wrong.\n\nVitest hides `console.log` in passing tests. Assert against a sentinel to see a\nvalue: `expect(actual).toEqual(\"MARKER\")`.\n\njsdom makes graph and pointer assertions silently vacuous:\n\n| Symptom | Cause | Fix in the probe |\n|---|---|---|\n| Geometric assertions pass trivially | `ParentSize` measures nothing, width is 0 | `vi.mock(\"@visx/responsive/lib/components/ParentSize\", ...)` with a fixed size |\n| Pointer handler commits `NaN` | no `createSVGPoint` / `getScreenCTM`, so visx `localPoint` fails | `vi.mock(\"@visx/event\", () => ({ localPoint: vi.fn(() => ({ x, y })) }))` |\n\nMeasure colors instead of judging them: `d3.lab(color).l` gives perceived\nlightness. `d3` is already a dependency.\n\nFor SQL claims, compile the builder and read the SQL. Kysely's `.select` appends,\nit does not replace, so a \"fallback\" select can emit a duplicate column.\n\n## 3. Read the PR before the code\n\nReviewers raise these before any line comment:\n\n* **Motivation is traceable.** A linked issue (a Linear key such as `Closes: CNS-192`, or `Fixes database-issues#N` for a legacy GitHub issue), a Slack thread, or a design doc. If a reviewer cannot tell why the change exists, ask for context before reviewing.\n* **UI changes link the Figma design** in the description.\n* **Stacked PRs are linked** in each description, or the work is one PR with logical commits. Review semantic commits one at a time when the author asks.\n* **Commit messages are imperative and descriptive** per `doc/developer/guide-changes.md`, not \"addressed feedback\".\n* **A removed compatibility shim states its floor.** If the PR deletes a version map or gate, the description names the minimum supported environment version.\n* **WIP code is marked** with `TODO` (spelled `TODO`, not `TO-DO`), including leftover `console.log`.\n* **New pages and routes sit behind a LaunchDarkly flag.** The point is developer experience: it lets the work merge incrementally. Self-managed flags default to `true` in `src/config/flexibleDeploymentFlags.ts`, so a flag meant to stay off there must be set to `false` explicitly.\n\n## 4. What reviewers flag, most frequent first\n\n### 4.1 Remove and simplify\n\nThe single most common request. Look for:\n\n* Unused variables, atoms, exports, props, and query keys. Variables that are always `true`.\n* Redundant fallbacks (`?? 0` behind a value that cannot be null) and conditionals that render nothing.\n* Wrapper hooks that re-expose library options as custom `enable*` props. Keep wrappers thin, since the library's own options are better documented.\n* Features with no requirement behind them (column pinning, LRU layers). Ask \"is there a requirement for this?\"\n* A mini rules engine where two `if` statements would do.\n* Extra state flags (`destroyed`, `initialized`) that every method must check, and piles of refs. Stale refs are a bug source.\n* A copied error state when the mutation hook already exposes `error`.\n* `!!x` inside an expression that is already boolean, and several early returns that can merge into one.\n\n### 4.2 SQL and catalog correctness\n\n* **Nullable columns.** `disk_bytes` and friends are nullable. Coalesce, and fix the cast. When dividing, check the *denominator* for zero, not each operand.\n* **`ORDER BY ... DESC` puts nulls first.** Add `nulls last` where the column is nullable.\n* **User objects are `id LIKE 'u%'`**, via `isSystemId` in `~/api/materialize`. Not `name NOT LIKE 'mz_%'`, and not a bare `startsWith(\"u\")`.\n* **Statistics tables keep rows for dropped replicas** for up to a day. Aggregates like `bool_and(snapshot_committed)` must filter to extant replicas.\n* **Sources:** filter to top-level sources the way `sourceList.ts` does. Subsources and progress collections re-enter through joins on `mz_hydration_statuses`. Newer sources expose subsources as tables.\n* **Latest vs max.** Is the query returning the most recent sample or the largest one, and is that what the UI claims?\n* **A windowed flag must not drive a point-in-time answer.** \"Unreadable at some point in the window\" is not \"unreadable now\". Check which columns and predicates a window-wide flag reaches.\n* **Multi-process skew.** Summing usage over processes hides a single process about to OOM. Prefer `MAX`, and prefer existing catalog views over recomputing their math client-side.\n* **Two surfaces, two answers.** When a PR touches one of several queries for the same metric (list vs detail page), check the staleness bounds and column semantics match. A column can mean different things in the old and new relation.\n* Factor a repeated join or subquery into `src/api/materialize/expressionBuilders.ts` and left-join it, rather than `GROUP BY` just to pull one column.\n* Aggregate with `jsonb_agg` / `list_agg` and format on the client, instead of `STRING_AGG` or `pg_size_pretty` in SQL.\n* Raw SQL goes through the `sql` tagged template. Qualified names are structured (`{ databaseName, schemaName, name }` quoted with `sql.id()` or `quoteIdentifier`), never a dotted string, which breaks on names containing periods.\n* Comment catalog behavior the docs do not cover (for example `object_sub_id IS NULL` meaning the relation itself), and rewrite clever predicates for intent (`BOOL_AND(x IS NOT NULL)` over count arithmetic).\n\n### 4.3 Reuse before re-rolling\n\nBefore accepting any new helper, grep for what it replaces. Reviewers name these most:\n\n| Re-rolled | Already exists |\n|---|---|\n| clamp, pluralize, capitalize, null-guard | `clamp`, `pluralize`, `capitalizeSentence`, `notNullOrUndefined` in `~/util` |\n| Percent and byte text | `formatPercentage(fraction, decimals)` (takes a fraction. Call it directly and handle null inline, since a local `formatPercent` wrapper reads as a re-roll), `formatBytesShort`, `humanizeBytes`, `formatMemoryUsage` in `~/utils/format` |\n| Sorting | d3 `sort(iterable, ...accessors)` (multi-key ties, returns a copy), `handleSortOrder` in `~/utils/sort`, `sortingFunctions` (`nullsLast`, `numericNullsLast`) in `components/Table/tableColumnBuilders.ts`. The same comparator written twice becomes one shared helper |\n| `60_000`-style time constants | `millisecondsInMinute`, `millisecondsInHour` from `date-fns/constants` (not the `date-fns` root in v4) |\n| Object type text | `kebabToTitleCase`, `kebabToSentenceCase`, `kebabToScreamingSpaceCase` (SQL keywords such as `MATERIALIZED VIEW`) in `~/util`, `formatObjectType` in `platform/clusters/format.ts` |\n| Route strings built by hand | path helpers in `~/platform/routeHelpers` (`regionPath`, `useBuildWorkflowGraphPath`, ...). Add one there when it is missing |\n| Logic copied from a neighboring page | extract one helper and switch both call sites to it, so the copies cannot drift |\n| System-object check | `isSystemId` in `~/api/materialize` |\n| Duration formatting, lag caps | `formatInterval`, `formatDurationForAxis` in `~/utils/format` (fractional units are reserved for seconds). `formatDurationForAxis` is for axes: it rounds to whole minutes and caps at 24h, so 90s reads \"2m\" in text. The 24h cap `MAX_LAG_MS` is module-private there and `FreshnessGraph.tsx` declares its own copy, so export one instead of adding a third |\n| An anchor with custom hover/underline | `TextLink` from `~/components/TextLink` |\n| Status colors copied from a pill | export `colorSchemeToStyles` from `StatusPill.tsx` |\n| Layout props threaded one by one | the `containerProps` pattern, see `Gauge.tsx` |\n| A new cluster-sizes query | `useAvailableClusterSizes` / `fetchAvailableClusterSizes` |\n| A new \"all objects\" fetch | `useAllObjects` and the global subscribes (`useGlobalUpsertSubscribe`) |\n| A new filter dropdown | mirror `SchemaFilter.tsx` / `DatabaseFilter.tsx` (reserved \"all\" id, caller sets width) |\n| A new DAG layout | `dagreGraphHelpers.ts` and the workflow graph components |\n| A `useEffect` + timeout debounce | `lodash.debounce` (see `ObjectExplorer.tsx`) |\n| Previous-value or latest-value refs, or a callback read inside a debounce or timer | `usePrevious`, `useLatestRef` from `@chakra-ui/react` (see `useAutomaticallyConnectSocket.ts`) |\n| Tab active state from `location.pathname` | `NavLink`'s `isActive`, see `PageTabStrip` |\n| Set diffing by hand | `Set.prototype.difference` |\n| Secret masking | export `formatAppPassword` from `~/queries/frontegg` |\n| A per-tenant localStorage key | export `buildCacheKey` from `platform/shell/commandCache.ts` |\n| Chart axes, grid, tooltip, legend, cursor | `buildXYGraphLayoutProps` and friends in `~/utils/graph`, `~/components/graphComponents`, `useChartLegend`, `useCursorState`, `colors.lineGraph` |\n| visx plumbing | `Line` from `@visx/shape` for straight lines (as `GraphLineCursor` does), `Group` from `@visx/group` instead of repeating offset arithmetic on each child, `ParentSizeProvidedProps` to type a `ParentSize` child's props, `useDrag` from `~/hooks/useDrag` for draggable handles |\n| Several small components for one feature | one `xComponents.tsx` file, like `formComponents.tsx` |\n\nWhen a new component duplicates an existing one, say which pages it lands on and\nwhat those pages have today.\n\n### 4.4 Tests\n\nValue over volume. Look for:\n\n* **Tests that assert on query-builder string output, SQL snapshots, CSS attributes, or class names.** Replace with SQL tests (`*.test.sql.ts`) that create real objects and assert results, or with user-visible assertions.\n* **Missing corner cases.** Ask for the harder fixture: a deeper role hierarchy, overlapping buckets, multiple inherited roles, users that can log in. For graph fixtures, an ASCII diagram in a comment helps.\n* **Assert the outcome, not the call.** After a `GRANT`, query `mz_internal.mz_show_all_privileges`.\n* **Fixtures that do not look like real data.** Mock only the relation under test, and make values realistic.\n* **`vi.mock` / `vi.spyOn` on app modules.** Reviewers dislike them (unstable, break on circular imports). Prefer MSW via `buildSqlQueryHandlerV2`, with per-call query keys when one mutation issues several statements, or dependency injection.\n* **Redundant happy-path slices.** One happy-path test plus separate corner cases beats ten tests that all break together. Multiple assertions per integration test are fine.\n* Unnecessary setup (`GRANT CREATEROLE` when the test already logs in as a superuser).\n* Regex over `toContain` for generated code, `!` over `?.` inside tests so they fail early, no `if` branches around expects, constants hoisted out of `describe` in `SCREAMING_SNAKE_CASE`.\n* Every bug found in review gets a regression test. New helpers and classes get unit tests.\n* A test that asserts a function's arity or a mock's call shape breaks when someone adds an optional parameter. Assert what the user sees instead.\n\n### 4.5 Types and nullability\n\n* No `any`, no casts that the types do not need (`as unknown as T`).\n* Ask why each field is nullable when every caller defines it.\n* Fields that are always null together become one nested object (`cluster: { id, name } | null`).\n* Two booleans that can conflict (`usersOnly`, `rolesOnly`) become a discriminated union.\n* Boolean props are optional (`canIssue?: boolean`) so `<X canIssue />` works. An error prop is `error?: boolean`, not `unknown`.\n* Type a required key as required instead of throwing when it is missing.\n* `===` over `==`. Prefer `undefined` over `null` for optional values.\n\n### 4.6 Naming\n\n* Units are suffixes: `durationMs`, not `ms`.\n* Booleans are present tense (`hasActiveSubscription`). Components are nouns, not verbs (`LicenseKeyCtaContent`, not `GetLicenseKey`).\n* `Inner` suffix only for a suspense boundary split, `Content` for a block of content.\n* `build...Query` is reserved for SQL builders. Transforms are `transform...`, fetchers `fetch...`, DOM listener setup `register...`.\n* Do not alias a name as an id (`parent.name as parentId`). SQL aliases are camelCase, named for the general concept (`relationComment`, not `tableComment`).\n* Callback props keep parity with DOM events (`onPointMove`, `onClick`), not new verbs like `onPointSelect`.\n* A variant that changes styling is a `variant` prop, not inferred from which callbacks were passed.\n* Icons end in `Icon`. Version gates are named constants (`MCP_OAUTH_MIN_VERSION`), not inline strings.\n* Loop variables say what they index (`bucketIndex`, not `j`).\n\n### 4.7 Code comments\n\nThe rules are in the root `CLAUDE.md` \"Code comments\" section. What reviewers\nflag in practice: comments with fluff, details that make no sense out of context\n(\"avoids alphanumeric sorting issues\"), and comments that no longer match the\ncode. They ask *for* comments on undocumented catalog behavior, on unconventional\nuses (react-query used only to get loading/error state for an initializer), on\nnon-obvious algorithms, and on props whose meaning is subtle. It is not necessary\nto describe the evolution of the code by referring to code that has been removed\n(\"use quicksort because bubble sort is inefficient\").\n\n### 4.8 Loading, error, and empty states\n\n* Every `useQuery` renders distinct loading, error (`isError`), and empty UI. A mutation failure is visible to the user, not only `console.error`.\n* No state falls through to an endless spinner (a disabled region is an error, not \"loading\").\n* Loading is a spinner inside the card, not \"Loading...\" text, which shifts layout on every refetch. Empty states render inside the card, not as a blank page.\n* Prefer `useQuery` with explicit states over new `useSuspenseQuery`.\n* **The global subscribes start empty.** `useAllClusters` and `useAllObjects` return `data: []` until `snapshotComplete`, so a page that reads empty as \"none\" shows \"No clusters\" or a false success on a cold load. Gate on `snapshotComplete`, and render the subscribe's error.\n* An entity with nothing to report gets one empty state, not a green \"0 of 0 within target\" summary over an empty chart.\n\n### 4.9 Data layer\n\n* Query keys start with `buildRegionQueryKey(...)` (`~/api/buildQueryKeySchema`), so switching region or organization never serves another region's cache.\n* Queries live under `src/api/materialize/...`, not in `platform/`.\n* `staleTime` / `gcTime` are deliberate. If data never goes stale, use `Infinity`, and invalidate on logout or mutation.\n* Refresh derived data by putting its query key in the namespace that mutations already invalidate, rather than polling (a 60s poll means 60s-stale ownership).\n* No new `useSql*` hook calls. `console/CLAUDE.md` (\"React Query\") has the read and write pattern.\n* Global subscribes are hoisted to the route that owns the section, or navigating remounts them and the view jitters.\n* Validate user-supplied parameters at the `build...Query` layer (see the start-time check in `queryHistoryList.ts`), not only in the UI.\n* Anything persisted to localStorage is scoped by organization and region, and cleared on region switch. An unscoped key leaks one tenant's catalog into another session.\n\n### 4.10 React hooks and state\n\n* No `useEffect` to sync props into state or to react to a user action. Do it in the event handler, or initialize state in the modal's `onOpen`.\n* Effect dependencies hold the primitive that matters (the JWT string, not the whole query result), so work reruns only when it should.\n* TanStack table `columns` are static. Dynamic data flows through `data`, or cells remount and tooltips vanish.\n* One owner per lifecycle: a connection manager owns connect, disconnect, and retry, and the hook is a thin shell. Shell output changes go through the `websocketFsm.ts` state machine.\n* For Jotai, a derived write-only atom with an early return, not returning the previous object and hoping it skips a render.\n* A helper that mutates variables outside its scope is harder to read than the same code inline with a comment.\n\n### 4.11 Routing and URL state\n\n* View modes and modal flows (graph vs table, edit role) are subroutes, so a reload keeps the user's place.\n* Filter and lookback state belongs in the URL, not localStorage, when it should survive reload and share.\n* Do not branch on `location.pathname` when `<Route>` already branches.\n* A URL-selected entity that does not exist (a dropped cluster in an old link) must not silently show a different one. Write the fallback into the URL, or say it was not found.\n* Imports use `~/` unless the file shares a parent directory.\n\n### 4.12 Versions, flags, and deployment modes\n\n* A query that reads a new column or relation breaks older environments. Gate it with `useEnvironmentGate` and a named version constant, per `console/doc/mz-backwards-compatibility.md`. Self-managed can run a new Console against much older environments.\n* Test across versions with emulator images on another port (`misc/docker/README.md` in the Console tree) or a local kind cluster.\n* Cloud-only initialization goes in `CloudAppInitializer`, not `AppInitializer` (flexible deployments and impersonation).\n* `forceShow` on a nav item breaks when the environment is disabled.\n* Superuser checks through `usePrivileges` hit `has_table_privilege` and load environmentd. Use `useIsSuperUser`.\n* An `AppConfigSwitch` whose branches render the same thing is noise.\n\n### 4.13 Visual and UX QA\n\nReviewers run the branch and attach screenshots or recordings. If you can run it\n(`console/CLAUDE.md` has the dev commands), do. Otherwise list these for the human\nreviewer:\n\n* Overflow, especially in modals, filters, and form footers.\n* Dark mode. Any literal color (`colors.green[50]`, hex, `hsl(`) is suspect, since it does not flip.\n* Spacing on the 4px grid in relative units (`3`, not `\"12px\"`). Valid `textStyle` values come from `src/theme/typography.ts`. Valid Chakra tokens for `zIndex`. Chakra style props over `style`.\n* **Types do not catch invalid style values.** Chakra passes `alignItems=\"left\"` straight through (Chrome computes it as `normal`, which `CSS.supports(\"align-items\", \"left\")` confirms), and an undefined `textStyle` such as `text-ui-sm` falls back to the default without an error.\n* Consistency with existing patterns: the app's purple color scheme, existing button hover behavior, filter chips like query history, an Apply button for unit inputs. \"We don't do this anywhere else\" is a common and valid objection.\n* Layout shift on refetch, jitter on navigation, flicker on load.\n* Thresholds that agree with each other (a value turns orange at 5s but the filter starts at a minute).\n* Copy: title case in titles, correct terminology (\"role with LOGIN attribute\", not \"privilege\"), where to save a generated config, a link to the docs page.\n* Semantic elements (`ul`/`li` for lists).\n* **Mouse-only click targets.** A clickable `Tr`, `HStack` or legend pill with `onClick` cannot be reached from the keyboard. Make it a button (`as=\"button\"`, `type=\"button\"`) with `aria-pressed` for toggles (see `connectComponents.tsx`), and prefer `:focus-visible` over an always-on focus ring.\n* One placeholder for a missing value across a page (`-` or `—`, not both).\n\n### 4.14 Security and session traps\n\nRare but high severity, and most often caught by automated QA review after merge:\n\n* Tenant or region state that survives a switch: caches, module-level singletons, persisted rows seeded with `snapshotComplete: true`.\n* Auth flows where the client clock or cached token decides a redirect. Redirects should be server-authoritative, or users loop.\n* Two concurrent sessions (password and OIDC) where queries silently run as the wrong account.\n* State-changing `GET`s, and protections that rely on another system's cookie config (`SameSite`) instead of checking `Origin` / `Sec-Fetch-Site`.\n* `btoa` on non-ASCII input. Service accounts where the code assumes a user email.\n\n### 4.15 Charts and graphs\n\nGraph PRs collect the same gotchas. Check each:\n\n* **Reuse first.** The 4.3 rows for graph helpers and visx plumbing. A block near identical to `FreshnessGraph`, `UtilizationGraph` or `ThresholdLineGraph` should come from the same pieces.\n* **Hover redraws only the cursor and tooltip.** Mark layers (lines, bars, rows) are `React.memo` with stable props: scales and layouts memoized on width and domain, series memoized. A memo only helps if callers hoist their arrays. One built inline reruns every render.\n* **Volatile inputs get their own layer.** Anything derived from `useAllObjects` or `useAllClusters` gets a new identity on any change anywhere in the environment. Draw it as its own memoized layer, or key its memo by content, so an unrelated DDL does not redraw the chart.\n* **Which point is hovered.** `findNearestDatum` returns the nearest bucket start, which near a bar's right edge is the next bar. For bucketed bars, find the bucket that contains the time.\n* **Decorative layers keep `pointerEvents=\"none\"`** and sit under the event overlay, or a full-width strip swallows hover.\n* **Labels state only what the data says.** A max-per-bucket series is a peak, not an average. Name the unit precisely (\"% of replica CPU allocation\"), and do not label an inferred state as recorded.\n* **Wide windows stay readable.** Bars narrower than a few pixels alias into stripes. Merge buckets client-side, keeping each metric's peak.\n* **Colors.** Series take `colors.lineGraph`. A generated palette varies perceived lightness (`d3.hcl` / `d3.lab`), not HSL lightness.\n* **Axis formatters are for axes.** `formatDurationForAxis` and tick formatters round. Headline and table values need the unrounded formatter.\n* **Refresh cost.** A chart that polls re-reads its whole window on every refetch. See 5.1 in `cost-and-structure.md`.\n\n### 4.16 Design docs\n\nFor `doc/design/` PRs, reviewers ask for concrete ranges and limits, error states\n(an unresponsive cluster), reuse of existing global subscribes and components,\nconsistency when two polled queries resolve at different times (a subscribe\navoids it), client-side formatting, and a scoped first version (superuser only)\nbefore the general case.\n\n## 5. Check what the change costs\n\nWhen the diff changes a query builder, a subscribe, a `refetchInterval` or\n`staleTime`, or anything that runs at pointer or keystroke rate, read section 5\nof `cost-and-structure.md` in this skill's directory and work through it. The\n`mz_catalog_server` cost and the render cost it checks do not show up in a read\nof the diff.\n\n## 6. Audit hooks and component structure\n\nWhen the diff adds or moves a `useEffect`, `useMemo`, `useCallback`, or\n`React.memo`, or splits a component, read section 6 of `cost-and-structure.md`.\n\n## 7. Write the comment the way a Console reviewer writes it\n\nFindings are triaged with the person you are reviewing for first. Present the\nlist, let them cut and rephrase, then post. Do not post a review off your own draft.\n\nComments are short, hedged, and collaborative. State the claim, give at most one\nclause of why, name the alternative, stop. Typical openers: `Nit:`, `I think we\nshould`, `Maybe we can`, `Could we reuse`, `Let's just`, `I'd recommend`,\n`I wonder if`, `Kinda feels like`, `Why is X nullable here?`, `How does this look\nin dark mode?`.\n\nReal comments at the right length:\n\n> nit: We usually like to use `<>` for react fragments\n\n> Can most likely reuse the TextLink component\n\n> Why is objectId nullable here? Usage of this hook shows me that it's always defined\n\n> Kinda feels like these should be grouped into a `cluster` object, especially if both of them can be null\n\n> Let's remove this comment.\n\nKeep measurements and probe output out of the comment. They belong in your reply\nto the person you are reviewing for. A bug claim is the exception: attach the\nrepro (a screenshot, a recording, or a short diff that triggers it), because that\nis what makes it actionable.\n\nPoint at the existing pattern with a permalink rather than describing it. Offer\npseudocode or a suggestion block when the fix is not obvious. Say when something\nis not blocking: \"Doesn't need to be blocking but I suggest in a followup PR,\"\n\"We can do this as a followup.\"\n\n## 8. Post as a single review\n\nOne review, not N comments, so the author gets one notification:\n\n```bash\ngh api repos/MaterializeInc/materialize/pulls/<N>/reviews \\\n  --method POST --input review.json\n```\n\n`review.json` is `{commit_id, body, event: \"COMMENT\", comments: [...]}`. Use\n`event: \"COMMENT\"` unless the person asked to approve or request changes. Each\ninline comment is `{path, line, side: \"RIGHT\", body}`, and a multi-line anchor adds\n`start_line` and `start_side`. General findings go in `body`.\n\nPermalink every code reference and pin the SHA:\n\n* Code already on main gets the PR's **base** SHA (`gh pr view <N> --json baseRefOid`).\n* Code inside the PR gets the **head** SHA.\n* Verify each cited line resolves before posting: `git show \"<sha>:<path>\" | sed -n '<line>p'`. When the SHA is in a shell variable, brace it (`\"${sha}:${path}\"`). zsh reads `$sha:c` as a modifier and drops the colon, even inside quotes.\n\nAfterwards, confirm what landed:\n\n```bash\ngh api \"repos/MaterializeInc/materialize/pulls/<N>/comments?per_page=100\" \\\n  --jq '[.[] | select(.pull_request_review_id == <id>)] | length'\n```\n\nBefore posting, check for a pending review. GitHub allows one pending review per\nperson per PR, so a stray one blocks a new review, and one pinned to an older\ncommit cannot take new comments because their lines resolve against that commit:\n\n```bash\ngh api \"repos/MaterializeInc/materialize/pulls/<N>/reviews?per_page=100\" \\\n  --jq '[.[] | select(.state == \"PENDING\")]'\n```\n\n* **Inline comments must fall inside a diff hunk.** Read the ranges with `gh api \"repos/MaterializeInc/materialize/pulls/<N>/files\" --jq '.[].patch' | grep '^@@'`. Anything outside goes on the file (`subjectType: FILE`) or in the body.\n* **Replies and new threads in one review** need GraphQL: `addPullRequestReview` with `commitOID`, then `addPullRequestReviewThread` and `addPullRequestReviewThreadReply` with that `pullRequestReviewId`, then `submitPullRequestReview` with `event: COMMENT`.\n* **Pending comments are invisible to REST**, where `pulls/comments/<id>` returns 404. Read and edit them through GraphQL (`updatePullRequestReviewComment`).\n* **Link the priorities.** When a review mixes bugs and nits, list the bugs in the body with `#discussion_r<id>` links so the author fixes those first.\n* **Pace the batches.** When the author has just received a large review, post the blocking items and hold structural suggestions until they respond.\n\n## 9. Re-review after the author pushes\n\n* **Diff from the last SHA you reviewed, not from the base:** `git fetch upstream pull/<N>/head:pr-<N>-r2 && git diff <last-reviewed-sha> pr-<N>-r2`.\n* **Sort each earlier comment into fixed, partly fixed, or open by reading the code at the new head.** Replies do not count, and a fix without a reply is fine. Thread state comes from GraphQL `reviewThreads { isResolved isOutdated }`.\n* **Probe every fix for the bug it might introduce.** Removing an effect left a debounce that could strand a drag handle, and scoring unreadable objects as breaching reached a \"right now\" column. Both passed the existing tests.\n* **When the author pushes back with numbers, check they measured what you suggested,** and time a plan label rather than trusting it. `lookup` against `differential join` said nothing about which was faster. Reply in the thread with your own measurement, since numbers belong in that reply.\n* **Re-run the suite on the new head,** and say so in the body.\n\n## Output\n\nGroup findings as **Blocking**, **Strong suggestions**, **Nits**, and mark\nfollow-up candidates. Open the summary with one specific sentence on what the PR\ndoes well (\"Loving the tests!\", \"Great work making this page dynamic, and it uses\na lot of our existing patterns\"), then the verdict (\"Small nits but otherwise\nlooking really good\", \"Once my comments are addressed, LGTM\"). Name the test\nresult. Flag anything you could not verify rather than asserting it.\n",".agents/skills/mz-dbt-release/SKILL.md":"---\nname: mz-dbt-release\ndescription: >\n  Cut a dbt-materialize PyPI release: bump the version in `__version__.py`\n  and `setup.py`, date the `Unreleased` CHANGELOG entry, and open the\n  release PR with a `Ship: <url>` body. Trigger: \"cut a dbt release\",\n  \"release dbt-materialize\", \"release the dbt adapter\", \"ship\n  dbt-materialize vX.Y.Z\", \"publish dbt-materialize to PyPI\", \"bump\n  dbt-materialize version\", \"new dbt adapter version\". Use this skill\n  even if the user just says \"ship the dbt adapter\" or pastes a feature\n  PR and asks for \"the next dbt release\" without naming version mechanics.\n---\n\n# Cutting a dbt-materialize release\n\nA release PR bumps the published version of the `dbt-materialize` PyPI\npackage. It is mechanical: three files change, one commit, one PR.\n\nThe adapter lives in `misc/dbt-materialize/`. PyPI publication is handled by\nCI after the PR merges.\n\n## Files to touch\n\nExactly three files:\n\n| File | Change |\n|---|---|\n| `misc/dbt-materialize/dbt/adapters/materialize/__version__.py` | Bump `version = \"X.Y.Z\"` |\n| `misc/dbt-materialize/setup.py` | Bump `version=\"X.Y.Z\"` (kwarg in `setup(...)`) |\n| `misc/dbt-materialize/CHANGELOG.md` | Insert `## X.Y.Z - YYYY-MM-DD` between `## Unreleased` and the existing bullets |\n\nBoth version strings must stay in sync — the comments in each file say so.\n\n## Picking the next version\n\n* Default: bump the **patch** component (`1.9.9 → 1.9.10`).\n* The **minor** component tracks the required `dbt-postgres` minor. Only bump\n  minor when upgrading to a new `dbt-postgres` minor (e.g. `1.9.x → 1.10.0`).\n* Read the current version from `__version__.py`.\n\n## Confirming what's being released\n\nThe `## Unreleased` section of `CHANGELOG.md` is the source of truth for what\nships. If it's empty, there is nothing to release. Cross-check with:\n\n```\ngit log <prev-release-sha>..HEAD -- misc/dbt-materialize/\n```\n\nIf a commit since the previous release touched `misc/dbt-materialize/` but is\nnot represented under `## Unreleased`, stop and ask whether it should be\nincluded — don't silently invent a changelog entry.\n\n## CHANGELOG edit\n\nThe release does **not** rewrite the unreleased bullets; it just dates them.\nInsert a new dated heading immediately after `## Unreleased`, leaving\n`## Unreleased` itself empty:\n\n```\n## Unreleased\n\n## 1.9.10 - 2026-05-20\n\n* Support unmanaged clusters in `deploy_init`. ...\n```\n\nUse today's date in `YYYY-MM-DD`.\n\n## Branch, commit, PR\n\n* Branch name: `dbt-release-X.Y.Z`, branched from current `main`.\n* Commit subject: `dbt-materialize: release vX.Y.Z`\n* Commit body / PR body: a single `Ship: <url>` line pointing at the PR that\n  triggered this release (the feature/fix PR being shipped). If multiple PRs\n  are being shipped, pick the headline one — the changelog already enumerates\n  them.\n* PR title: same as the commit subject.\n* PR targets `MaterializeInc/materialize:main`.\n\nExample:\n\n```\ngh pr create --repo MaterializeInc/materialize --base main \\\n  --head <your-fork>:dbt-release-1.9.10 \\\n  --title \"dbt-materialize: release v1.9.10\" \\\n  --body \"Ship: https://github.com/MaterializeInc/materialize/pull/36625\"\n```\n\n## What you do NOT run\n\nThis PR touches only Python version strings and Markdown. Skip:\n\n* `cargo check`, `cargo clippy`, Cargo.lock work — no Rust changes.\n* Test suites — there are no tests to add for a version bump.\n\n`bin/fmt` and `bin/lint` cover `misc/dbt-materialize/*.py`, but a\none-character version bump almost never triggers them. Run them if\nyou've made an accidental wider change; otherwise they're optional.\n\n## Verifying before pushing\n\n```\ngit diff --stat\n```\n\nShould show exactly three files, predominantly a `+4 / -2` shape (one\nline each in the two version files, two added lines in `CHANGELOG.md`).\nA few extra lines in `CHANGELOG.md` are fine if a bullet was reformatted\nwhile finalising it; an unexpected fourth file means you've gone\noff-script.\n\n## Quick reference\n\n```\n# 1. Branch\ngit checkout -b dbt-release-X.Y.Z\n\n# 2. Edit the three files (see table above)\n\n# 3. Verify\ngit diff --stat   # expect 3 files, +4/-2\n\n# 4. Commit\ngit commit -am \"dbt-materialize: release vX.Y.Z\n\nShip: <feature-pr-url>\"\n\n# 5. Push and open PR\ngit push -u origin dbt-release-X.Y.Z\ngh pr create --repo MaterializeInc/materialize --base main \\\n  --head <your-fork>:dbt-release-X.Y.Z \\\n  --title \"dbt-materialize: release vX.Y.Z\" \\\n  --body \"Ship: <feature-pr-url>\"\n```\n\n## Historical reference\n\nRecent release commits (find via `git log --oneline -- misc/dbt-materialize/dbt/adapters/materialize/__version__.py`):\n\n* v1.9.10 — PR #36636\n* v1.9.9 — PR #36609\n* v1.9.8 — PR #36529\n\nInspect any of them with `git show <sha>` to see the exact diff shape.\n",".agents/skills/mz-adapter-guide/SKILL.md":"---\nname: mz-adapter-guide\ndescription: >\n  Correctness invariants + architecture: adapter, coordinator, pgwire, peek\n  paths, timestamp oracle. Trigger: questions about these subsystems —\n  \"how does coordinator work\", \"what are read holds\", \"explain peek path\",\n  \"how does timestamp selection work\", \"why does this query block\". Also\n  when editing OR reviewing code in src/adapter/, src/pgwire/,\n  src/timestamp-oracle/ (reviewing a PR, diff, or commit that touches these\n  paths counts).\n---\n\n# Adapter Guide Skill\n\nWhen working on or reviewing the adapter layer or related crates, always read\nand follow the guidance in `doc/developer/guide-adapter.md`. That document\ncontains correctness invariants, architectural notes, a list of optimizations\nthat have been tried and rejected for correctness reasons, and a checklist for\nreviewing adapter changes. Read it before making changes and before reviewing\nthem.\n\nRead `doc/developer/guide-adapter.md` now.\n\nWhen working on the timestamp oracle (`src/timestamp-oracle/`), also read the\nmodule-level docs in `src/timestamp-oracle/src/lib.rs` and\n`src/timestamp-oracle/src/batching_oracle.rs` for the `TimestampOracle` trait\ncontract and the batching/linearizability invariants.\n\nWhen you or the user discover that an approach is incorrect or heading in a\nwrong direction, suggest distilling the learning into\n`doc/developer/guide-adapter.md`. Add it to whichever section fits best — a\nnew correctness invariant, a new subsection under an existing invariant, a\nrejected optimization, or a new top-level section if needed. Describe what was\ntried, why it's wrong, and any relevant context. This keeps the guide up to\ndate and prevents future re-discovery of the same pitfalls.\n",".agents/skills/mz-commit/SKILL.md":"---\nname: mz-commit\ndescription: >\n  Trigger: \"commit\", \"prepare commit\", \"create PR\", \"push\", \"open pull request\",\n  or mentions committing, pre-commit checks, pull requests in Materialize. Also\n  \"ship it\", \"ready to merge\". For code review use mz-pr-review.\n---\n\n# Committing in Materialize\n\nRead `doc/developer/guide-changes.md` for the full conventions on submitting and reviewing changes.\n\n## Pre-commit checklist\n\nBefore committing, run these and fix any warnings:\n\n1. `bin/fmt` (formats all `.rs`, `.py`, and `.proto` files; takes no file arguments)\n2. `bin/lint` (can error if tools are missing; use `bin/ci-builder run stable bin/lint` as an alternative)\n3. `cargo clippy --all-targets -- -D warnings`\n\n`bin/lint` and clippy do not run rustdoc. When the diff adds imports or edits\ndoc comments, run the Doctests check on the changed crates (command in the\nmz-debug-ci skill). When a `metric!` definition changed, `bin/lint`\nregenerates `doc/user/data/metrics.yml`. Commit the result, or the \"Lint and\nrustfmt\" job fails on the diff.\n\nAfter rebasing onto upstream/main, run `cargo check --workspace --all-targets`\neven when the rebase had no conflicts. Upstream changes can still break the\nbranch, for example a dependency your branch needs that upstream removed as\nunused (Nightly's `bin/unused-deps` reports those), a duplicate item, or a new\nenum variant your match does not cover. CI checks this in the\n\"Merge skew cargo check\" job.\n\nDo not manually update `*.snap` files.\nUse `cargo test` followed by `cargo insta accept` to update snapshot files.\nRewrite datadriven test expectations with `REWRITE=1 cargo test ...`.\n\n### LIR plan changes: regenerate the schema registry\n\nThe stable LIR format lets optimized plans be stored durably and reused, so\ntwo snapshot tests in `mz-compute-types` pin what a stored plan looks like\nand what it means. Both key off `LIR_VERSION` in\n`src/compute-types/src/plan.rs`.\n\n`lir_schema` traces the serde schema of everything reachable from\n`LirRelationExpr` into `tests/snapshots/lir_v{LIR_VERSION}.json`. Run it\nwhenever the diff changes the serde-visible shape of a type the trace\nreaches: anything in `src/compute-types/src/plan.rs` or\n`src/compute-types/src/plan/`, a `*Func` payload, or a plan-reachable type\nin `mz-expr` or `mz-repr` such as `AggregateFunc`, `TableFunc`,\n`SqlScalarType` or `DateTimeUnits`:\n\n```\ncargo test -p mz-compute-types --test lir_schema\n```\n\nRegenerate with `REWRITE=1 cargo test -p mz-compute-types --test lir_schema`\nand review the diff. It is the reviewable record of the format change. If\nthe current `LIR_VERSION` has shipped (see below), bump it first so the old\n`lir_v{N}.json` stays in place for migration tooling and the rewrite targets\na fresh file.\n\n### Scalar function changes: regenerate the function registry\n\nThe schema registry sees a plan's shape, not what its functions compute.\n`func_registry` covers that gap for `UnaryFunc`, `BinaryFunc`, and\n`VariadicFunc`. Run it whenever the diff touches\n`src/expr/src/scalar/func.rs` or `src/expr/src/scalar/func/` (including\n`impls/` and `#[sqlfunc]` bodies):\n\n```\ncargo test -p mz-compute-types --test func_registry\n```\n\n* It fails when a function is added, removed, or has a changed property\n  (nullability, error behavior, monotonicity, output type, sqlfunc\n  signature) or a changed `#[sqlfunc]` body. If the failure names removed or\n  changed functions, or a body change that alters results, settle the\n  `LIR_VERSION` question below **before** regenerating. A rewrite at the old\n  version overwrites that version's entry in `func_registry_digests.json`\n  instead of adding a new one, no later check catches it, and that entry is\n  the only key back into git history for what the shipped version's\n  functions meant. Once the version is settled, regenerate the checked-in\n  snapshots and review the diff under `src/compute-types/tests/snapshots/`:\n\n  ```\n  REWRITE=1 cargo test -p mz-compute-types --test func_registry\n  ```\n\n  Never hand-edit `func_registry*.json`. The test rebuilds the registry and\n  compares it against all three files, so an edit fails it.\n* **New function with a payload** (any variant that carries data, e.g. a\n  target type, a regex, a scale): the registry build panics naming the\n  variant. Add a representative `Sample` to `unary_samples()`,\n  `binary_samples()`, or `variadic_samples()` in\n  `src/expr/src/scalar/func/registry.rs`. If a property depends on the\n  payload (e.g. a cast only errors when it has a scale), add a second sample\n  with `.labeled(\"...\")` covering the other branch. Payload-free variants and\n  plain `#[sqlfunc]` functions need no sample.\n* **Removed or changed function, or a body change that alters results**: if\n  the current `LIR_VERSION` has shipped in a release, bump it so stored plans\n  are replanned, and only then regenerate. An unshipped version is\n  regenerated in place. Treat the version as shipped when the latest release tag carries the\n  same `LIR_VERSION`:\n\n  ```\n  git fetch --tags\n  git show $(git tag --list 'v*' --sort=-v:refname | grep -v -- '-rc' | head -1):src/compute-types/src/plan.rs | grep 'pub const LIR_VERSION'\n  ```\n\n  A bump is a judgment call, so confirm it with the user before making it.\n  Additions never need a bump. When unsure whether a body change alters\n  results, say so in the PR description rather than deciding silently.\n\nCommit the regenerated snapshots together with the function change, and call\nout the registry diff (added, removed, changed functions) in the PR\ndescription.\n\n## PR titles and commit messages\n\nMaterialize uses squash merging, so the PR title becomes the commit subject on `main`.\n\n* Use imperative mood: \"Fix X\" not \"Fixed X\" or \"Fixes X\".\n* Be specific: \"Fix panic in catalog sync when controller restarts\" not \"Fix bug\".\n* Prefix with area if helpful: `adapter: `, `storage: `, `compute: `, `sql: `.\n\nWrite a thorough PR description explaining the rationale for the change.\nMention which tests were added or modified in the pull request description, but do not list which tests were run.\nAdd release notes for user-visible changes (should complete \"This release will...\").\n\n## Issue tracking: Linear only for new issues\n\nNew issues are filed in Linear, never in the `database-issues` GitHub repo. `database-issues` is legacy. Its open issues are still valid to read, link, and close, so an existing `database-issues#NNNN` reference in code or a comment stays as it is.\n\nReference the Linear issue by its its key, e.g. `Closes: SQL-450`. Don't include the full URL containing the issue title.\n\nWhen a change closes a legacy GitHub issue, `Fixes database-issues#NNNN` works for auto-closing.\n\n## Cargo.lock discipline\n\nNever regenerate the entire Cargo.lock — bare `cargo update` bumps every semver-compatible dep and introduces unrelated breakage (e.g., `os_info` pulling in `objc2` on macOS, `chrono-tz` changing timezone data, `serde_path_to_error` changing error formats).\n\n* **Adding a dep or changing features**: just `cargo check`. It updates only what's needed.\n* **Updating one crate**: `cargo update -p <crate>` (add `--precise <ver>` to pin).\n* **After any Cargo.lock change**, review the diff:\n  ```\n  git diff Cargo.lock | grep '^[+-]version' | head -40\n  ```\n  Pin back anything that moved unexpectedly: `cargo update -p <crate> --precise <old-version>`.\n* **After rebase conflicts in Cargo.lock**: resolve by taking HEAD's version then running `cargo check` (not `cargo update`). This preserves existing pins while adding only what the new commits require.\n\n## Git conventions\n\n* Work against the `main` branch of `MaterializeInc/materialize`.\n* Push branches to your fork.\n* Pull requests target `main` on `MaterializeInc/materialize`.\n* Each PR should contain one semantic change.\n* Put non-trivial user docs for a new feature in a separate PR. See \"Adding documentation for new features\" in `doc/user/README.md`.\n\n## Splitting large branches\n\nThe repo is squash-merge-only: every PR lands as exactly one commit on `main`, however many commits it had internally. Keep each commit/PR under ~500 changed lines, split by concern, not by the chronology of how the code was written.\n\n* Prefer GitHub's native stacked-PR support to land a chain of dependent PRs, where it's set up for the branch. Where it isn't, fall back to sequential landing, one PR at a time: cut a PR from the front, merge, `git fetch upstream && git rebase upstream/main` the remaining tail, repeat. Squash-only means merging an earlier PR always replaces its commits with one new commit, so any later branch still built on the old commits needs a rebase either way; sequential landing keeps that to one clean rebase per merge instead of compounding across an unmanaged chain.\n* Land foundational or low-conflict commits (design docs, scaffolding, new deps) first. The longer a tail branch lives, the more likely a concurrent unrelated PR touches the same files and turns a clean rebase into a real conflict.\n* To split one large diff into smaller commits, or squash a long messy history into fewer reviewable units, as an alternative to interactive rebase: `git reset --soft <base>` stages the full diff, then stage per group with `git add <files>` for a file-level split, or use `git checkout <sha> -- .` to reproduce an exact historical checkpoint's tree for a chronological squash. (Claude Code's Bash tool has no interactive terminal, so agents need this non-interactive form; use whichever you prefer by hand.) `git checkout <sha> -- .` only adds or updates paths present in `<sha>`, it never removes files that shouldn't exist yet at that checkpoint: compute the removal set first with `comm -23 <(git ls-files|sort) <(git ls-tree -r --name-only <sha>|sort)` and `git rm -f` those, or the checkpoint commit silently carries files from later history. Verify every checkpoint with `git diff HEAD <sha>` (must be empty) before moving to the next. Reattach already-clean commits on top with `git rebase --onto <new-branch> <old-base>` (also non-interactive).\n",".agents/skills/mz-dependencies/SKILL.md":"---\nname: mz-dependencies\ndescription: >\n  Trigger: adding, removing, or updating a Rust dependency, changing crate\n  features, touching Cargo.toml or Cargo.lock, \"cargo update\", \"add a crate\",\n  \"bump a dependency\", or a new dependency license in Materialize.\n---\n\n# Dependency management\n\n## Workspace dependencies\n\nAll third-party versions declared in `[workspace.dependencies]` in root\n`Cargo.toml`. Members use `dep.workspace = true` or\n`dep = { workspace = true, optional = true }`.\n\n* **Add new dep**: add to `[workspace.dependencies]` in root `Cargo.toml` first, then `dep.workspace = true` in member crate.\n* **Never inline version** in member `Cargo.toml` — `bin/lint-cargo` enforces.\n* **Update version**: change once in root `Cargo.toml`.\n* Cargo unifies features workspace-wide, so workspace declaration is union of all features across crates. Members just use `dep.workspace = true`.\n\n## Cargo.lock\n\nNever regenerate full Cargo.lock. When changing deps:\n\n* **Add dep or change features**: run `cargo check` — updates only what changed.\n* **Update specific crate**: `cargo update -p <crate>` (optional `--precise <version>`).\n* **Never bare `cargo update`** — bumps every semver-compatible dep, causes unrelated breakage from transitive changes.\n* **If lock regenerated**, diff before commit (`git diff Cargo.lock | grep '^[+-]version'`) and pin back unintended bumps with `cargo update -p <crate> --precise <old-version>`.\n\n## Licensing\n\nTwo files control license policy, **keep in sync**: `deny.toml` (`[licenses].allow`) and `about.toml` (`accepted`). New dep with new license not already allowed: add SPDX identifier to both.\n",".agents/skills/mz-parallel-workload/SKILL.md":"---\nname: mz-parallel-workload\ndescription: >\n  Extend parallel-workload stress framework: random SQL concurrently to catch\n  panics + unexpected errors (not perf — see mz-benchmark). Trigger:\n  \"parallel workload\", \"parallel-workload\", \"action.py\" re parallel workload,\n  or testing panics/unexpected errors under concurrency. Also \"add this to\n  parallel workload\" or bug that panics under concurrent DDL/DML.\n---\n\n# Extending Parallel Workload\n\nThe parallel-workload framework stress-tests Materialize by running random SQL actions concurrently across multiple threads. It catches **panics and unexpected query errors** - it does not verify result correctness.\n\n## When to Use This Framework\n\nAdd coverage here when a bug manifests as:\n- A panic under concurrent operations\n- An unexpected error from a query that should succeed (or fail gracefully)\n- A crash or connection loss triggered by specific DDL/DML combinations\n\n## How It Works\n\n1. The orchestrator spawns N worker threads, each assigned an **action list** (read, fetch, write, dml_nontrans, or ddl) based on the configured `Complexity`.\n2. Each worker randomly selects actions from its list using **weighted random choice** and executes them in a loop until the runtime expires.\n3. If an action raises a `QueryError`, the worker checks `action.errors_to_ignore(exe)`. If the error matches, it's logged and ignored. If not, the **test fails**.\n4. All threads share a `Database` object that tracks created objects (tables, views, clusters, etc.) with thread-safe locking.\n\n## Adding a New Action\n\n### Step 1: Define the Action Class\n\nAdd your class in `action.py`. Subclass `Action` and implement `run()`:\n\n```python\nclass MyNewAction(Action):\n    def run(self, exe: Executor) -> bool:\n        # Return False to skip (e.g., precondition not met)\n        # Return True after successful execution\n        exe.execute(\"SELECT my_new_feature()\", http=Http.RANDOM)\n        return True\n```\n\n### Step 2: Override `errors_to_ignore()` If Needed\n\nIf your action can produce expected errors (e.g., object was concurrently dropped), extend the base list:\n\n```python\n    def errors_to_ignore(self, exe: Executor) -> list[str]:\n        result = super().errors_to_ignore(exe)\n        result.extend([\n            \"my expected error message\",\n        ])\n        # Conditionally ignore errors based on complexity or scenario\n        if exe.db.complexity == Complexity.DDL:\n            result.extend([\"does not exist\"])\n        return result\n```\n\n### Step 3: Register in an Action List\n\nAdd your action class and weight to the appropriate list at the bottom of `action.py`:\n\n```python\n# In ddl_action_list, read_action_list, write_action_list, etc.\nddl_action_list = ActionList(\n    [\n        ...\n        (MyNewAction, 5),  # weight controls selection frequency\n        ...\n    ],\n    autocommit=True,  # True for DDL, False for transactional actions\n)\n```\n\n**Which list to use:**\n- `read_action_list` - SELECT, SUBSCRIBE, COPY TO (autocommit=False)\n- `fetch_action_list` - DECLARE CURSOR / FETCH (autocommit=False)\n- `write_action_list` - INSERT, COPY FROM (autocommit=False)\n- `dml_nontrans_action_list` - DELETE, UPDATE, INSERT RETURNING (autocommit=True)\n- `ddl_action_list` - CREATE/DROP/ALTER/RENAME (autocommit=True)\n\n## Common Patterns\n\n### Picking a Random Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    with exe.db.lock:\n        if not exe.db.tables:\n            return False\n        table = self.rng.choice(exe.db.tables)\n    # Use table outside db.lock but inside table.lock for mutations\n    with table.lock:\n        if table not in exe.db.tables:\n            return False  # Was dropped while we waited\n        exe.execute(f\"ALTER TABLE {table} ...\", http=Http.RANDOM)\n    return True\n```\n\n### Respecting Object Limits\n\n```python\ndef run(self, exe: Executor) -> bool:\n    if len(exe.db.tables) >= MAX_TABLES:\n        return False  # Skip if at capacity\n    ...\n```\n\nLimits are defined in `database.py`: `MAX_TABLES=5`, `MAX_VIEWS=15`, `MAX_CLUSTERS`, `MAX_SCHEMAS`, etc.\n\n### Creating a New Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    if len(exe.db.views) >= MAX_VIEWS:\n        return False\n    view_id = exe.db.view_id\n    exe.db.view_id += 1\n    try:\n        schema = self.rng.choice(exe.db.schemas)\n    except IndexError:\n        return False\n    with schema.lock:\n        if schema not in exe.db.schemas:\n            return False\n        view = View(self.rng, view_id, base_object, schema, ...)\n        view.create(exe)\n    exe.db.views.append(view)\n    return True\n```\n\n### Dropping a Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    with exe.db.lock:\n        if len(exe.db.views) <= 2:\n            return False  # Keep a minimum\n        view = self.rng.choice(exe.db.views)\n    with view.lock:\n        if view not in exe.db.views:\n            return False\n        exe.execute(f\"DROP VIEW {view}\", http=Http.RANDOM)\n        exe.db.views.remove(view)\n    return True\n```\n\n### Scenario-Specific Actions\n\nSome actions only run in specific scenarios. Override `applicable()` for\nthis, do NOT gate on the scenario inside `run()`: the worker skips\ninapplicable actions without counting an attempt, which keeps the\nend-of-run action coverage check (an action that never succeeds over a\nwhole run fails it) free of false positives.\n\n```python\ndef applicable(self, exe: Executor) -> bool:\n    return exe.db.scenario == Scenario.Rename\n\ndef run(self, exe: Executor) -> bool:\n    ...\n```\n\n### Using Prepared Statements\n\n```python\ndef run(self, exe: Executor) -> bool:\n    query = \"SELECT ...\"\n    if self.rng.choice([True, False]):\n        self.stmt_id += 1\n        self.exe_prepared(query, f\"mystmt{self.stmt_id}\", exe)\n    else:\n        exe.execute(query, http=Http.RANDOM)\n    return True\n```\n\n## Extending an Existing Action\n\nOften the simplest approach is to add new SQL variants to an existing action's `run()` method. For example, to test a new expression type, add it to `expression.py`. To test a new system flag, add it to `FlipFlagsAction.flags_with_values`.\n\n### Adding a System Flag to FlipFlagsAction\n\nIn `FlipFlagsAction.__init__`, add an entry to the `flags_with_values` dictionary:\n\n```python\nself.flags_with_values[\"my_new_flag\"] = [\"true\", \"false\"]\n```\n\n## Running\n\n```bash\n# Default: DDL complexity, regression scenario, 600s runtime\nbin/mzcompose --find parallel-workload run default\n\n# Custom options\nbin/mzcompose --find parallel-workload run default \\\n    --complexity=ddl \\\n    --scenario=regression \\\n    --runtime=300 \\\n    --seed=42\n\n# Available complexities: read, dml, ddl, ddl-only, random\n# Available scenarios: regression, cancel, kill, rename, backup-restore, 0dt-deploy, random\n```\n\n## Important Design Rules\n\n1. **Minimize locking** - Use `exe.db.lock` briefly to pick objects, then use per-object locks. Excessive locking reduces the chance of finding real race conditions. You can instead add an expected error in `errors_to_ignore`\n2. **Return False, don't raise** - If a precondition isn't met (empty list, at capacity), return `False` to skip the action.\n3. **Handle IndexError from `rng.choice`** - Objects can be concurrently removed. Either check length first under lock, or catch `IndexError` and return `False`.\n4. **Track state changes** - If you create an object, append it to the appropriate `exe.db` list. If you drop one, remove it.\n5. **Use `self.rng`** - Always use the action's seeded `random.Random` instance, never `random.choice()` directly. This makes failures reproducible via `--seed`.\n",".agents/skills/mz-limits-test/SKILL.md":"---\nname: mz-limits-test\ndescription: >\n  Add/modify/debug limits test. Trigger: \"limits test\", \"Generator subclass\",\n  \"many objects\", \"scaling test\", or stress-test Materialize with many objects\n  (tables, views, sources, indexes). Also edits in test/limits/mzcompose.py.\n---\n\n# Limits Test Framework\n\nThe limits test stresses Materialize with large numbers of objects, complex queries, and large ingestions. It catches regressions via fixpoint panics, stack overflows, CI timeouts (O(N^M) with N~1000), and OOM panics (2GB memory limit).\n\n## Where It Lives\n\n- **Main file**: `test/limits/mzcompose.py`\n- All Generator subclasses live in that single file\n- **Auto-discovery**: Subclasses of `Generator` are found automatically via `all_subclasses(Generator)` from `materialize.util` - no registration needed\n\n## Generator Base Class\n\n```python\nclass Generator:\n    COUNT: int = 1000      # Number of objects to create (override per test)\n    VERSION: str = \"1.0.0\" # Bump when test logic changes\n    MAX_COUNT: int | None = None  # Upper limit for --find-limit mode\n\n    @classmethod\n    def body(cls) -> None:\n        \"\"\"Override this. Print testdrive commands to stdout.\"\"\"\n        raise NotImplementedError\n\n    # Convenience iterators\n    @classmethod\n    def all(cls) -> range:       # range(1, COUNT+1)\n    @classmethod\n    def no_first(cls) -> range:  # range(2, COUNT+1) - skip first\n    @classmethod\n    def no_last(cls) -> range:   # range(1, COUNT) - skip last\n\n    @classmethod\n    def store_explain_and_run(cls, query: str) -> str | None:\n        \"\"\"Records EXPLAIN query for timing and prints `> {query}`.\"\"\"\n```\n\n`header()` is inherited - it resets the `public` schema and grants privileges. `footer()` prints a blank line. `generate()` calls `header()` → `body()` → `footer()`. `mz_system` is a built-in testdrive connection, so `$ postgres-execute connection=mz_system` works in `body()` without any registration.\n\n## How to Add a New Limits Test\n\n1. Add a new class in `test/limits/mzcompose.py` that extends `Generator`.\n2. Implement the `body()` classmethod. It must `print()` testdrive commands.\n3. Optionally override `COUNT` and `MAX_COUNT`, only if it's causing issues in CI.\n\n### Minimal Template\n\n```python\nclass MyNewFeature(Generator):\n    COUNT = min(Generator.COUNT, 100)  # Lower if operation is slow\n    MAX_COUNT = 800  # Optional: cap for --find-limit mode\n\n    @classmethod\n    def body(cls) -> None:\n        # Bump system limits if needed\n        print(\"$ postgres-execute connection=mz_system\")\n        print(f\"ALTER SYSTEM SET max_tables = {cls.COUNT * 10};\")\n        print(\"$ postgres-execute connection=mz_system\")\n        print(f\"ALTER SYSTEM SET max_objects_per_schema = {cls.COUNT * 10};\")\n\n        # Create objects\n        for i in cls.all():\n            print(f\"> CREATE TABLE t{i} (f1 INTEGER);\")\n            print(f\"> INSERT INTO t{i} VALUES ({i});\")\n\n        # Query and verify\n        for i in cls.all():\n            cls.store_explain_and_run(f\"SELECT * FROM t{i}\")\n            print(f\"{i}\")\n```\n\nYou can run it locally with `bin/mzcompose --find limits down && bin/mzcompose --find limits run main --scenario=MyNewFeature`, add `--find-limit` for the Release Qualification mode.\n",".agents/skills/mz-feature-flags/SKILL.md":"---\nname: mz-feature-flags\ndescription: >\n  Trigger: adding or changing a feature flag, dyncfg, or system parameter\n  default, reviewing a PR that adds one, LaunchDarkly flag setup, or\n  bin/lint-test-flags failures in Materialize.\n---\n\n# Feature flags\n\nChoosing whether a new feature flag defaults on or off in production is a\nproduct decision that needs context an agent rarely has. When adding a flag,\nask a human which default to use. When reviewing a PR, do not question the\ndefault it chose. However, in tests, a new flag should usually be on, so that\nsqllogictest and testdrive exercise the new code path. If\nits production default is off, turn it on for CI in\n`misc/python/materialize/mzcompose/__init__.py`\n(`get_minimal_system_parameters` if tests fail with it off, otherwise\n`get_variable_system_parameters`), which `bin/sqllogictest` and mzcompose's\n`Materialized` service pass to environmentd as system parameter defaults.\nRust integration tests use `TestHarness::with_system_parameter_default`.\nA value listed there overrides the compiled-in default in CI, so changing a\ndefault means changing both. `bin/lint-test-flags` fails unless every new\ndyncfg is registered for parallel workload and mzcompose (see the mz-debug-ci\nskill). A compiled-in default is not evidence of the production value: check\nwhether `test/launchdarkly-flag-consistency/mzcompose.py` lists the flag in\n`INTENTIONAL_LD_OVERRIDES` or `KNOWN_CROSS_ENV_DIVERGENCES`. A session\nvariable takes a system default only if `SESSION_SYSTEM_VARS` in\n`src/sql/src/session/vars.rs` lists it. Otherwise `--system-parameter-default`\nfor it only logs a warning at startup. Observed: the LaunchDarkly MCP\n`create-flag` tool creates boolean flags that serve true in every environment,\nincluding production. A human must set each environment's off variation and\nfallthrough to false.\n",".agents/skills/mz-platform-checks/SKILL.md":"---\nname: mz-platform-checks\ndescription: >\n  Create/modify/debug platform check. Trigger: \"platform check\",\n  \"platform-checks\", \"upgrade check\", \"restart check\", or writing Check class\n  testing feature survival across restarts/upgrades. Also edits in\n  misc/python/materialize/checks/all_checks/.\n---\n\n# Platform Checks\n\nThe Platform Checks framework is \"write once, run everywhere\" - a single test validates a feature across multiple scenarios (restarts, upgrades, etc.).\n\n## Key Concepts\n\n- **Check**: A Python class that tests one feature via testdrive fragments.\n- **Scenario**: The context (e.g., restart, upgrade) in which checks run.\n- **Action**: An individual step within a scenario (e.g., stop a container).\n\n## Where Checks Live\n\nAll checks are in `misc/python/materialize/checks/all_checks/`. The `__init__.py` auto-discovers every `.py` file in that directory, so adding a new file is all that's needed for registration.\n\n## Anatomy of a Check\n\nA check is a class that extends `Check` and implements three methods. Each returns a `Testdrive` object wrapping a testdrive fragment string.\n\n```python\nfrom textwrap import dedent\nfrom materialize.checks.actions import Testdrive\nfrom materialize.checks.checks import Check\n\nclass MyFeature(Check):\n    def initialize(self) -> Testdrive:\n        \"\"\"Run once at the start. Create tables, insert seed data, set up objects.\"\"\"\n        return Testdrive(dedent(\"\"\"\n            > CREATE TABLE my_feature_table (f1 INTEGER);\n            > INSERT INTO my_feature_table VALUES (1), (2), (3);\n        \"\"\"))\n\n    def manipulate(self) -> list[Testdrive]:\n        \"\"\"MUST return a list of exactly 2 Testdrive fragments.\n        Each runs after a restart/upgrade phase. Insert more data, ALTER objects,\n        or create derived objects (e.g., materialized views).\"\"\"\n        return [\n            Testdrive(dedent(\"\"\"\n                > INSERT INTO my_feature_table VALUES (4), (5);\n                > CREATE MATERIALIZED VIEW my_feature_view1 AS\n                  SELECT COUNT(*) AS c FROM my_feature_table;\n            \"\"\")),\n            Testdrive(dedent(\"\"\"\n                > INSERT INTO my_feature_table VALUES (6), (7);\n                > CREATE MATERIALIZED VIEW my_feature_view2 AS\n                  SELECT SUM(f1) AS s FROM my_feature_table;\n            \"\"\")),\n        ]\n\n    def validate(self) -> Testdrive:\n        \"\"\"Run after all initialize + manipulate steps. Verify everything survived.\n        MAY BE CALLED MORE THAN ONCE - must be idempotent. Any objects created\n        here must be TEMPORARY or explicitly dropped.\"\"\"\n        return Testdrive(dedent(\"\"\"\n            > SELECT * FROM my_feature_view1;\n            7\n            > SELECT * FROM my_feature_view2;\n            28\n        \"\"\"))\n```\n\n## Execution Order in a Scenario\n\n```\nStartMz → initialize() → [restart/upgrade] → manipulate(phase=0)\n  → [restart/upgrade] → manipulate(phase=1) → [restart/upgrade] → validate()\n```\n\nBoth manipulate phases always run. validate() may run multiple times.\n\n## Decorators and Version Gating\n\n### Disable a check\n```python\nfrom materialize.checks.checks import disabled\n\n@disabled(ignore_reason=\"due to SQL-450\")\nclass MyBrokenCheck(Check):\n    ...\n```\n\n### Non-idempotent external interactions\nIf a check ingests non-UPSERT data into Kafka/Postgres (i.e., replaying manipulate would duplicate data), annotate it:\n```python\nfrom materialize.checks.checks import externally_idempotent\n\n@externally_idempotent(False)\nclass MyKafkaCheck(Check):\n    ...\n```\n\n### Version-gated checks\nFor features that only exist from a certain version, override `_can_run`:\n```python\nfrom materialize.checks.executors import Executor\nfrom materialize.mz_version import MzVersion\n\nclass MyNewFeature(Check):\n    def _can_run(self, e: Executor) -> bool:\n        return self.base_version >= MzVersion.parse_mz(\"v0.80.0-dev\")\n```\n\n### Version-conditional testdrive lines\nUse `[version>=VVMMPP]` or `[version<VVMMPP]` prefixes for syntax that changed between versions (e.g., v0.92.0 → `9200`):\n```\n>[version<9200] CREATE SOURCE s FROM LOAD GENERATOR COUNTER (SCALE FACTOR 0.01)\n>[version>=9200] CREATE SOURCE s FROM LOAD GENERATOR COUNTER\n```\n\n## Tips for Good Checks\n\n1. **Naming**: Prefix ALL object names with a unique string (e.g., `my_feature_`) to avoid collisions with other checks running in the same Mz instance.\n2. **manipulate() returns exactly 2 elements**: The framework asserts this.\n3. **validate() is idempotent**: It may run multiple times. Use TEMPORARY objects or DROP at end.\n4. **Create objects in manipulate, not just initialize**: This tests that DDL works after restarts/upgrades.\n5. **Create derived objects on both old and new base objects**: e.g., a materialized view on a table from initialize AND on a table from manipulate.\n6. **Exercise full SQL syntax breadth**: Test every WITH option, every variant of the feature.\n7. **Ingest data in every phase**: Ensure data flows during restarts/upgrades.\n8. **Use `dedent()` on all testdrive strings**: Required for proper testdrive parsing.\n\n## Helper Methods on Check\n\n- `self.base_version`: The MzVersion the test started from (useful for upgrade scenarios).\n- `self.current_version`: The MzVersion currently running.\n- `self._kafka_broker()`: Returns the broker connection string for testdrive.\n- `self._default_cluster()`: Returns `\"quickstart\"`.\n- `self.rng`: A seeded `Random` instance for deterministic randomness.\n\n## Running\n\n```bash\n# Run a specific check against a specific scenario\nbin/mzcompose --find platform-checks run default --scenario=RestartEntireMz --check=MyFeature\n\n# Run with no restarts (useful for debugging the check itself)\nbin/mzcompose --find platform-checks run default --scenario=NoRestartNoUpgrade --check=MyFeature\n\n# Run all checks (default)\nbin/mzcompose --find platform-checks run default --scenario=RestartEntireMz\n```\n\n## Debugging CI Failures\n\n1. Find the scenario name near the top of the log: `Running ... --scenario=RestartEnvironmentdClusterdStorage`\n2. Find the failing check just before the error: `Running validate() from <materialize.checks.threshold.Threshold ...>`\n3. Reproduce locally: `bin/mzcompose --find platform-checks run default --scenario=RestartEnvironmentdClusterdStorage --check=Threshold`\n4. Try `--scenario=NoRestartNoUpgrade` to isolate whether the failure is restart/upgrade-related.\n",".agents/skills/mz-pr-review/SKILL.md":"---\nname: mz-pr-review\ndescription: >\n  Local code review of current branch vs Materialize standards. Trigger:\n  \"review my code\", \"review my changes\", \"check my diff\", \"does this look ok\",\n  \"what do you think of this PR\", \"code review\", or look over changes before\n  merging. Also PR number + wants feedback on quality, style, correctness.\n  For changes under `console/`, use mz-console-review instead.\nargument-hint: [base-branch]\nallowed-tools: [Bash, Read, Grep, Glob, Task]\n---\n\nPerform a local code review of the current branch's changes against Materialize project standards.\n\n## Steps\n\n1. Parse arguments from: $ARGUMENTS — a PR number, base branch, or nothing.\n2. Get the diff using the first method that works:\n   - **PR number given** (e.g. `123`): `gh pr diff 123`\n   - **git available**: `git diff <base>...HEAD` (default base: `main`)\n   - **jj available**: `jj diff -r <revset>` (default: diff from trunk)\n3. Get the file list from the same diff (add `--stat` for git, `--stat` for jj, or `gh pr diff 123 --stat` for PR).\n4. Review the diff against the checklists below.\n5. Present findings organized as: **Blocking**, **Strong suggestions**, **Nits**.\n\n## Review checklist\n\nThe overall developer guide for reviewing changes is defined in `doc/developer/guide-changes.md`, always read and follow its guidance.\n\n### Tests\n- Every behavior change has at least one new or modified test.\n- SQL/query behavior → look for `.slt` in `test/sqllogictest/`.\n- Wire/protocol behavior → look for `.pt` in `test/pgtest/`.\n- Rust logic/types/APIs → add or extend a Rust unit test in the crate (e.g. `#[cfg(test)]` or `tests/`); run with `cargo test -p mz-<crate>`.\n- Prefer testing observable behavior (SQL results, wire protocol) over implementation details.\n- Red flag: behavior change with no test changes.\n- For more testing guidelines, read `doc/developer/guide-testing.md`\n\n### LIR schema registry\nApplies when the diff changes the serde-visible shape of a type the LIR\nschema reaches: `src/compute-types/src/plan.rs`,\n`src/compute-types/src/plan/**`, a `*Func` payload, or a plan-reachable type\nin `mz-expr` or `mz-repr`.\n- `src/compute-types/tests/snapshots/lir_v{N}.json` is regenerated in the\n  same PR, and the diff matches the intended format change. Red flag: a\n  serde-visible change to a plan type with no snapshot diff.\n- If `LIR_VERSION` in `src/compute-types/src/plan.rs` had shipped, it is\n  bumped and the old `lir_v{N}.json` is left untouched rather than rewritten.\n- `Row` and `EvalError` never appear directly in plan types, only as\n  `StableRow` / `StableEvalError`, enforced by\n  `lir_schema_contains_only_stable_types`.\n\n### Scalar function registry\nApplies when the diff touches `UnaryFunc`, `BinaryFunc`, or `VariadicFunc`\n(`src/expr/src/scalar/func.rs`, `src/expr/src/scalar/func/**`, `#[sqlfunc]`\nbodies).\n- The registry snapshots under `src/compute-types/tests/snapshots/` are\n  regenerated in the same PR. A property change moves `func_registry.json`\n  and the current version's entry in `func_registry_digests.json`, a\n  `#[sqlfunc]` declaration or body change moves `func_registry_source.json`\n  alone, and a change to a hand-written variant's body moves neither.\n- Every new variant with a payload has a `Sample` in\n  `src/expr/src/scalar/func/registry.rs`, with a labeled second sample when a\n  property depends on the payload.\n- A removed function, a changed property, or a `body_fingerprint` change that\n  alters results for some input bumps `LIR_VERSION` in\n  `src/compute-types/src/plan.rs` if that version has shipped, and leaves the\n  old digest in `func_registry_digests.json` untouched. Additions need no\n  bump.\n- The PR description names the added, removed, and changed functions and,\n  for body changes, states whether results change.\n\n### Code style (Rust)\n- **Imports:** `std` → external crates → `crate::`; one `use` per module; prefer `crate::` over `super::` in non-test code.\n- **Errors:** Structured with `thiserror`; no bare `anyhow!(\"...\")`. `Display` should not print full error chain.\n- **Async:** Use `ore::task::spawn` / `spawn_blocking`, not raw `tokio::spawn`.\n- **Tests:** `#[mz_ore::test]`; panic in tests rather than returning `Result`.\n\n### Code style (SQL)\n- Keywords capitalized (`SELECT`, `FROM`); identifiers lowercase.\n- No space between function name and `(`.\n\n### Error messages\n- Primary: short, factual, lowercase first letter, no trailing punctuation.\n- Detail/hint: complete sentences, capitalized, period.\n- No \"unable\", \"bad\", \"illegal\", \"unknown\"; say what kind of object.\n\n### Sensitive data handling\n- Types holding passwords, keys, tokens, or credentials should use `mz_ore::secure::{SecureString, SecureVec}` or `zeroize::Zeroizing<T>` (from `mz_ore::secure`).\n- Sensitive types should **not** derive `Clone` or `Debug` (use custom `Debug` that redacts).\n- Stack-local buffers holding derived keys, nonces, or HMAC outputs should be wrapped in `Zeroizing<T>`.\n- See `doc/developer/generated/ore/secure.md` for full guidance and `src/ssh-util/src/keys.rs` for a reference implementation.\n\n### Architecture\n- **Simplicity:** No incidental complexity; simplify redundant logic.\n- **No special casing:** Prefer composable design over extra booleans/branches.\n- **Encapsulation:** sql-parser = grammar only (no semantic validation); sql = planning + semantics.\n- **Dependencies:** New crates must be justified.\n- For more design guidelines read: `doc/developer/best-practices.md`\n\n### Polish\n- No leftover `// XXX`, `// FIXME`, `dbg!`, `println!`, or commented-out code.\n- No unrelated formatting changes in untouched code.\n- New public items should have doc comments.\n\n### Release notes\n\nRelease notes are auto-generated per release from PR descriptions (the [`/mz-release-notes` skill](https://github.com/MaterializeInc/mz-skills/blob/main/.agents/skills/mz-release-notes/SKILL.md));\nauthors don't write them. So for a user-visible change, check that the PR *description* states the\nuser-observable effect in user-facing terms — not only implementation detail — so the classifier includes it.\n\n## One semantic change rule\n\nThe PR should do one thing. If it spans multiple CODEOWNERS areas (e.g. sql-parser + sql planner), consider suggesting a split.\n\n## Rules\n\n- Review the code, not the author. Explain the *why* behind suggestions.\n- Use **nit:** for preferences where reasonable people could disagree.\n- If the PR improves overall codebase health and blocking items are addressed, say so.\n- Do NOT make any changes — this is read-only review.\n",".agents/skills/mz-profile/SKILL.md":"# mz-profile\n\nPath: `.agents/skills/mz-profile/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-profile/SKILL.md)",".agents/skills/mz-query-perf/SKILL.md":"# mz-query-perf\n\nPath: `.agents/skills/mz-query-perf/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-perf/SKILL.md)",".agents/skills/mz-query-tracing/SKILL.md":"# mz-query-tracing\n\nPath: `.agents/skills/mz-query-tracing/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-tracing/SKILL.md)",".agents/skills/mz-release-signoff/SKILL.md":"# mz-release-signoff\n\nPath: `.agents/skills/mz-release-signoff/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/SKILL.md)",".agents/skills/mz-run/SKILL.md":"# mz-run\n\nPath: `.agents/skills/mz-run/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-run/SKILL.md)",".agents/skills/mz-test/SKILL.md":"# mz-test\n\nPath: `.agents/skills/mz-test/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-test/SKILL.md)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/AGENTS.md)","CLAUDE.md":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/CLAUDE.md)","console/AGENTS.md":"# console - AI Agent Protocol & Instructions\n\nPath: `console/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/AGENTS.md)","console/CLAUDE.md":"# console - Claude Agent Guidelines & System Prompt\n\nPath: `console/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/CLAUDE.md)",".agents/skills/README.md":"# Skills Skill\n\nPath: `.agents/skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/README.md)",".claude/commands/update-docs.md":"# Command: /update-docs\n\nPath: `.claude/commands/update-docs.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.claude/commands/update-docs.md)",".agents/skills/mz-console-review/cost-and-structure.md":"# Mz-console-review Skill\n\nPath: `.agents/skills/mz-console-review/cost-and-structure.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-console-review/cost-and-structure.md)",".agents/skills/mz-debug-ci/bk-fallback.md":"# Mz-debug-ci Skill\n\nPath: `.agents/skills/mz-debug-ci/bk-fallback.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/bk-fallback.md)",".agents/skills/mz-debug-ci/rare-failures.md":"# Mz-debug-ci Skill\n\nPath: `.agents/skills/mz-debug-ci/rare-failures.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/rare-failures.md)","ci/plugins/cloudtest/README.md":"# cloudtest Documentation\n\nPath: `ci/plugins/cloudtest/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/cloudtest/README.md)","ci/plugins/mzcompose/README.md":"# mzcompose Documentation\n\nPath: `ci/plugins/mzcompose/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/mzcompose/README.md)","ci/plugins/scratch-aws-access/README.md":"# scratch-aws-access Documentation\n\nPath: `ci/plugins/scratch-aws-access/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/scratch-aws-access/README.md)",".agents/skills/mz-release-signoff/references/adapter.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/adapter.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/adapter.md)",".agents/skills/mz-release-signoff/references/compute.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/compute.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/compute.md)",".agents/skills/mz-release-signoff/references/persist.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/persist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/persist.md)",".agents/skills/mz-release-signoff/references/reference-dashboards.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/reference-dashboards.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/reference-dashboards.md)",".agents/skills/mz-release-signoff/references/sources-and-sinks.md":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/sources-and-sinks.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/sources-and-sinks.md)","doc/developer/generated/compute/extensions/_module.md":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/_module.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/_module.md)","doc/developer/generated/compute/extensions/arrange.md":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/arrange.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/arrange.md)","doc/developer/generated/compute/extensions/reduce.md":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/reduce.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/reduce.md)","doc/developer/generated/compute/extensions/temporal_bucket.md":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/temporal_bucket.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/temporal_bucket.md)","doc/developer/generated/mz-deploy/cli/commands/_module.md":"# Command: /module\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/_module.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/_module.md)","doc/developer/generated/mz-deploy/cli/commands/abort.md":"# Command: /abort\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/abort.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/abort.md)","doc/developer/generated/mz-deploy/cli/commands/apply_all.md":"# Command: /apply_all\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_all.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_all.md)","doc/developer/generated/mz-deploy/cli/commands/apply_connections.md":"# Command: /apply_connections\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_connections.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_connections.md)","doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md":"# Command: /apply_network_policies\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md)","doc/developer/generated/mz-deploy/cli/commands/apply_objects.md":"# Command: /apply_objects\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_objects.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_objects.md)","doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md":"# Command: /apply_secrets\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md)","doc/developer/generated/mz-deploy/cli/commands/apply_sources.md":"# Command: /apply_sources\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_sources.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_sources.md)","doc/developer/generated/mz-deploy/cli/commands/apply_tables.md":"# Command: /apply_tables\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_tables.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_tables.md)","doc/developer/generated/mz-deploy/cli/commands/clean.md":"# Command: /clean\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/clean.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/clean.md)","doc/developer/generated/mz-deploy/cli/commands/clusters.md":"# Command: /clusters\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/clusters.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/clusters.md)","doc/developer/generated/mz-deploy/cli/commands/compile.md":"# Command: /compile\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/compile.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/compile.md)","doc/developer/generated/mz-deploy/cli/commands/debug.md":"# Command: /debug\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/debug.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/debug.md)","doc/developer/generated/mz-deploy/cli/commands/delete.md":"# Command: /delete\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/delete.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/delete.md)","doc/developer/generated/mz-deploy/cli/commands/describe.md":"# Command: /describe\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/describe.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/describe.md)","doc/developer/generated/mz-deploy/cli/commands/dev.md":"# Command: /dev\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/dev.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/dev.md)","doc/developer/generated/mz-deploy/cli/commands/explain.md":"# Command: /explain\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/explain.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/explain.md)","doc/developer/generated/mz-deploy/cli/commands/grants.md":"# Command: /grants\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/grants.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/grants.md)","doc/developer/generated/mz-deploy/cli/commands/list.md":"# Command: /list\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/list.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/list.md)","doc/developer/generated/mz-deploy/cli/commands/lock.md":"# Command: /lock\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/lock.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/lock.md)","doc/developer/generated/mz-deploy/cli/commands/log.md":"# Command: /log\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/log.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/log.md)","doc/developer/generated/mz-deploy/cli/commands/mcp.md":"# Command: /mcp\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/mcp.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/mcp.md)","doc/developer/generated/mz-deploy/cli/commands/new_project.md":"# Command: /new_project\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/new_project.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/new_project.md)","doc/developer/generated/mz-deploy/cli/commands/profile.md":"# Command: /profile\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/profile.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/profile.md)","doc/developer/generated/mz-deploy/cli/commands/promote.md":"# Command: /promote\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/promote.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/promote.md)","doc/developer/generated/mz-deploy/cli/commands/roles.md":"# Command: /roles\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/roles.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/roles.md)","doc/developer/generated/mz-deploy/cli/commands/setup.md":"# Command: /setup\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/setup.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/setup.md)","doc/developer/generated/mz-deploy/cli/commands/setup_schema.md":"# Command: /setup_schema\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/setup_schema.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/setup_schema.md)","doc/developer/generated/mz-deploy/cli/commands/sql.md":"# Command: /sql\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/sql.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/sql.md)","doc/developer/generated/mz-deploy/cli/commands/stage.md":"# Command: /stage\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/stage.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/stage.md)","doc/developer/generated/mz-deploy/cli/commands/test.md":"# Command: /test\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/test.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/test.md)","doc/developer/generated/mz-deploy/cli/commands/wait.md":"# Command: /wait\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/wait.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/wait.md)","doc/developer/generated/mz-deploy/cli/commands/test/lower.md":"# Command: /lower\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/test/lower.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/test/lower.md)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/AGENTS.md)","tokens":300,"sizeBytes":160},{"name":"AGENTS.md","path":"console/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/AGENTS.md","title":"console - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# console - AI Agent Protocol & Instructions\n\nPath: `console/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/AGENTS.md)","tokens":300,"sizeBytes":186},{"name":"SKILL.md","path":".agents/skills/mz-adapter-guide/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-adapter-guide/SKILL.md","title":"mz-adapter-guide","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-adapter-guide\ndescription: >\n  Correctness invariants + architecture: adapter, coordinator, pgwire, peek\n  paths, timestamp oracle. Trigger: questions about these subsystems —\n  \"how does coordinator work\", \"what are read holds\", \"explain peek path\",\n  \"how does timestamp selection work\", \"why does this query block\". Also\n  when editing OR reviewing code in src/adapter/, src/pgwire/,\n  src/timestamp-oracle/ (reviewing a PR, diff, or commit that touches these\n  paths counts).\n---\n\n# Adapter Guide Skill\n\nWhen working on or reviewing the adapter layer or related crates, always read\nand follow the guidance in `doc/developer/guide-adapter.md`. That document\ncontains correctness invariants, architectural notes, a list of optimizations\nthat have been tried and rejected for correctness reasons, and a checklist for\nreviewing adapter changes. Read it before making changes and before reviewing\nthem.\n\nRead `doc/developer/guide-adapter.md` now.\n\nWhen working on the timestamp oracle (`src/timestamp-oracle/`), also read the\nmodule-level docs in `src/timestamp-oracle/src/lib.rs` and\n`src/timestamp-oracle/src/batching_oracle.rs` for the `TimestampOracle` trait\ncontract and the batching/linearizability invariants.\n\nWhen you or the user discover that an approach is incorrect or heading in a\nwrong direction, suggest distilling the learning into\n`doc/developer/guide-adapter.md`. Add it to whichever section fits best — a\nnew correctness invariant, a new subsection under an existing invariant, a\nrejected optimization, or a new top-level section if needed. Describe what was\ntried, why it's wrong, and any relevant context. This keeps the guide up to\ndate and prevents future re-discovery of the same pitfalls.\n","frontmatter":{"name":"mz-adapter-guide","description":"Correctness invariants + architecture: adapter, coordinator, pgwire, peek paths, timestamp oracle. Trigger: questions about these subsystems — \"how does coordinator work\", \"what are read holds\", \"explain peek path\", \"how does timestamp selection work\", \"why does this query block\". Also when editing OR reviewing code in src/adapter/, src/pgwire/, src/timestamp-oracle/ (reviewing a PR, diff, or commit that touches these paths counts).\n"},"isInternal":false,"tokens":389,"sizeBytes":1730},{"name":"SKILL.md","path":".agents/skills/mz-benchmark/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-benchmark/SKILL.md","title":"mz-benchmark","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-benchmark\ndescription: >\n  Add/modify/debug Materialize perf benchmark scenarios. Three frameworks:\n  Feature Benchmark (single-op micro), Scalability Test (SQL throughput under\n  concurrency), Parallel Benchmark (sustained latency via scenarios.py).\n  Trigger: \"benchmark\", \"feature benchmark\", \"scalability test\",\n  \"parallel benchmark\", \"performance regression\", \"micro-benchmark\", \"TPS\",\n  \"latency test\", or edits in feature_benchmark/scenarios/,\n  scalability/workload/workloads/, parallel_benchmark/scenarios.py.\n  Note: measurement, not panic-stress (see mz-parallel-workload).\n---\n\n# Benchmark Frameworks\n\nMaterialize has some benchmark frameworks targetting local Docker, each suited to different performance concerns. Choose the right one based on what you're measuring.\n\n## Decision Guide\n\n| Concern | Framework | Best For |\n|---------|-----------|----------|\n| Single-operation latency | **Feature Benchmark** | \"How fast is this SELECT / INSERT / CREATE INDEX?\" |\n| SQL throughput under concurrency | **Scalability Test** | \"How many QPS can we sustain at 1/4/16/64/256 concurrent clients?\" |\n| Sustained performance over time | **Parallel Benchmark** | \"Does latency degrade over a 2-minute mixed workload?\" |\n\nA Rust `cargo bench` target must reference the production allocator with a\n`mz-alloc-default` dev-dependency and `use mz_alloc_default as _;` (see\n`src/repr/benches/row.rs`). Without it the bench runs on the system allocator,\nnot jemalloc.\n\nScalability Test and Parallel Benchmark connect with psycopg 3 and leave\n`prepare_threshold` at its default of 5, so a query switches to a prepared\nstatement on its sixth execution per connection. Set it explicitly and warm\nconnections up when per-query latency matters.\n\n---\n\n## 1. Feature Benchmark (Micro-benchmarks)\n\nMeasures wall-clock time of individual SQL operations without concurrency. Uses testdrive fragments. Great for regression-testing specific code paths.\n\n### How to Add a Scenario\n\n1. Add a class in one of the `scenarios/*.py` files. Extend `Scenario` (default, runs in CI), `ScenarioBig` (longer, opt-in), or `ScenarioDisabled`.\n2. Implement `benchmark()` returning a `Td(...)` with timing markers (`/* A */` and `/* B */`).\n3. Optionally implement `shared()`, `init()`, `before()` if setup is required.\n\n### Scenario Template\n\n```python\nfrom materialize.feature_benchmark.action import TdAction\nfrom materialize.feature_benchmark.measurement_source import MeasurementSource, Td\nfrom materialize.feature_benchmark.scenario import Scenario\n\nclass MyCategory(Scenario):\n    \"\"\"Group parent - won't run (not a leaf).\"\"\"\n    pass\n\nclass MySpecificBenchmark(MyCategory):\n    \"\"\"Leaf class - this runs.\"\"\"\n\n    def init(self) -> list[TdAction]:\n        return [\n            self.table_ten(),  # Creates table 'ten' with values 0-9\n            TdAction(\"\"\"\n                > CREATE TABLE t1 (f1 INTEGER);\n                > INSERT INTO t1 SELECT {unique_values} FROM {join};\n                > SELECT COUNT(*) FROM t1;\n                {n}\n            \"\"\".format(\n                unique_values=self.unique_values(),\n                join=self.join(),\n                n=self.n(),\n            )),\n        ]\n\n    def benchmark(self) -> MeasurementSource:\n        return Td(\"\"\"\n> SELECT 1;\n  /* A */\n1\n\n> SELECT COUNT(*) FROM t1 WHERE f1 % 2 = 0;\n  /* B */\n{half_n}\n\"\"\".format(half_n=self.n() // 2))\n```\n\n### Timing Markers\n\n- `/* A */` - Start marker. Measurement begins AFTER this completes.\n- `/* B */` - End marker. Measurement ends when this completes.\n- Measures: time from end of A to end of B.\n- For a single query, use `SELECT 1; /* A */` as a dummy start to not measure the overhead of Testdrive starting up.\n\n### Scenario Phases (execution order)\n\n1. **`shared()`** - Runs once for BOTH instances (e.g., create Kafka topics)\n2. **`init()`** - Runs once per instance (create tables, populate data)\n3. **`before()`** - Runs before EACH iteration (restart, cleanup)\n4. **`benchmark()`** - Runs repeatedly, measured\n\nThe result is the minimum (`MinAggregation`) after discarding the first 3\nmeasurements. `ProbForMin(threshold=0.90)` in\n`test/feature-benchmark/mzcompose.py` can end a scenario after 6 measurements,\nso the verdict can rest on 3 values and one outlier can flip it. Read the raw\nper-iteration values before trusting a regression or improvement.\n\nThe measured cluster `cluster_default` is an unmanaged replica on the\n`clusterd` container, configured by the `Clusterd(...)` service in\n`create_clusterd_service`. That function maps `enable_unified_cluster` onto\nthe service, but other parameters that shape the replica process do not reach\nit. For example, `enable_timely_zero_copy` has no effect because `Clusterd`\nhardcodes its timely config.\n\n### Helpers\n\n- `self.n()` - Returns 10^SCALE (default SCALE=6 → 1,000,000)\n- `self.table_ten()` / `self.view_ten()` - Creates table/view with values 0-9\n- `self.unique_values()` - SQL expression generating N unique values via cartesian product\n- `self.join()` - SQL FROM clause for cartesian product joins\n- `self.keyschema()` / `self.schema()` - Kafka schema helpers\n\n### Tips\n\n- **Aim for ~1 second** query time. Too fast = noise; too slow = Unbearably slow CI.\n- **Test td fragments locally** with standalone testdrive before running the full benchmark.\n- **Use `SCALE`** to control data volume: `SCALE = 5` → 100K rows.\n- **Use `FIXED_SCALE = True`** if `--scale` should not affect your scenario. This is usually done when a scenario doesn't scale well, add an explanation.\n- **Use `RELATIVE_THRESHOLD`** to customize regression sensitivity per scenario.\n\n### Running\n\n```bash\nbin/mzcompose --find feature-benchmark run default --root-scenario=MyCategory\nbin/mzcompose --find feature-benchmark run default --root-scenario=MySpecificBenchmark\n```\n\n---\n\n## 2. Scalability Test (SQL Throughput Under Concurrency)\n\nMeasures TPS (transactions per second) at increasing concurrency levels (1, 2, 4, 8, ..., 256 clients). Compares Materialize against Postgres.\n\nThe load is closed-loop: `concurrency` threads each issue the next operation\nwhen the previous one completes (`benchmark_executor.py`). TPS is therefore\ncapped at concurrency divided by mean latency, and a flattening curve can be\nthe client cap rather than a server knee. Use Parallel Benchmark's `OpenLoop`\nto measure behavior at a fixed offered load, and report p99 and max with it.\n\n### How to Add a Workload\n\n1. Create a new file or add to an existing file in `workload/workloads/`.\n2. Extend a marker class (`DmlDqlWorkload`, `DdlWorkload`, or `ConnectionWorkload`).\n3. Implement `operations()` returning a list of `Operation` objects.\n\n### Workload Template (Simple DML/DQL)\n\n```python\nfrom materialize.scalability.operation.operations.operations import (\n    SelectOne,\n    InsertDefaultValues,\n    SelectCountInMv,\n)\nfrom materialize.scalability.workload.workload_markers import DmlDqlWorkload\n\nclass MyWorkload(DmlDqlWorkload):\n    def operations(self) -> list[\"Operation\"]:\n        return [InsertDefaultValues(), SelectCountInMv()]\n```\n\n### Workload Template (DDL with Data Exchange)\n\n```python\nfrom materialize.scalability.operation.operation_data import OperationData\nfrom materialize.scalability.operation.operations.operations import (\n    CreateTableX, PopulateTableX, SelectStarFromTableX, DropTableX,\n)\nfrom materialize.scalability.operation.scalability_operation import OperationChainWithDataExchange\nfrom materialize.scalability.workload.workload import WorkloadWithContext\nfrom materialize.scalability.workload.workload_markers import DdlWorkload\n\nclass MyDdlWorkload(WorkloadWithContext, DdlWorkload):\n    def amend_data_before_execution(self, data: OperationData) -> None:\n        data.push(\"table_seed\", data.get(\"worker_id\"))\n\n    def operations(self) -> list[\"Operation\"]:\n        return [\n            OperationChainWithDataExchange([\n                CreateTableX(), PopulateTableX(), SelectStarFromTableX(), DropTableX(),\n            ])\n        ]\n```\n\n### Custom Operations\n\n```python\nfrom materialize.scalability.operation.scalability_operation import SimpleSqlOperation\n\nclass MyCustomSelect(SimpleSqlOperation):\n    def sql_statement(self) -> str:\n        return \"SELECT COUNT(*) FROM t1 WHERE f1 > 500;\"\n```\n\n### Built-in Operations\n\n`SelectOne`, `SelectStar`, `SelectLimit`, `SelectCount`, `SelectCountInMv`, `SelectUnionAll`, `Update`, `InsertDefaultValues`, `CreateTableX`, `PopulateTableX`, `DropTableX`, `CreateMvOnTableX`, `DropMvOfTableX`, `Connect`, `Disconnect`.\n\n### Database Schema (auto-created)\n\nFor each workload, the framework creates: `CREATE TABLE t1 (f1 INTEGER DEFAULT 1)`, inserts a row, and creates `CREATE MATERIALIZED VIEW mv1 AS SELECT count(*) AS count FROM t1`.\n\n### Running\n\n```bash\nbin/mzcompose --find scalability run default --workload MyWorkload\nbin/mzcompose --find scalability run default --workload-group-marker DmlDqlWorkload\nbin/mzcompose --find scalability run default --min-concurrency 1 --max-concurrency 128\n```\n\n---\n\n## 3. Parallel Benchmark (Sustained Performance)\n\nMeasures latency distributions (p50/p95/p99) and QPS over a sustained load period (default 120s). Supports mixed workloads with both open-loop and closed-loop concurrency. Detects latency degradation over time via slope analysis.\n\n### How to Add a Scenario\n\n1. Add a class in `scenarios.py` extending `Scenario`.\n2. In `__init__`, call `self.init(phases=[...])` with `TdPhase` (setup) and `LoadPhase` (benchmark).\n\n### Scenario Template\n\n```python\nfrom materialize.parallel_benchmark.framework import (\n    ClosedLoop,\n    LoadPhase,\n    OpenLoop,\n    Periodic,\n    ReuseConnQuery,\n    Scenario,\n    StandaloneQuery,\n    TdPhase,\n)\n\nclass MyScenario(Scenario):\n    def __init__(self, c, conn_infos):\n        self.init(\n            phases=[\n                # Setup phase (testdrive)\n                TdPhase(\"\"\"\n                    > CREATE TABLE bench_t (f1 INTEGER);\n                    > INSERT INTO bench_t SELECT generate_series(1, 10000);\n                    > CREATE MATERIALIZED VIEW bench_mv AS\n                      SELECT COUNT(*) AS c FROM bench_t;\n                    > SELECT c FROM bench_mv;\n                    10000\n                \"\"\"),\n                # Benchmark phase\n                LoadPhase(\n                    duration=120,  # seconds\n                    actions=[\n                        OpenLoop(\n                            action=StandaloneQuery(\n                                \"SELECT c FROM bench_mv\",\n                                conn_infos[\"materialized\"],\n                            ),\n                            dist=Periodic(per_second=100),\n                            report_regressions=True,\n                        ),\n                        ClosedLoop(\n                            action=ReuseConnQuery(\n                                \"INSERT INTO bench_t VALUES (1)\",\n                                conn_infos[\"materialized\"],\n                            ),\n                            report_regressions=True,\n                        ),\n                    ],\n                ),\n            ],\n            guarantees={\n                \"SELECT c FROM bench_mv (standalone)\": {\n                    \"qps\": 10,\n                    \"p99\": 500,\n                },\n            },\n        )\n```\n\n### Action Types\n\n| Class | Behavior | Use When |\n|-------|----------|----------|\n| `StandaloneQuery` | New connection per operation | Testing connection overhead + query |\n| `ReuseConnQuery` | Persistent single connection | Testing pure query latency |\n| `PooledQuery` | Draws from connection pool | Testing pooled throughput (set `conn_pool_size > 0`) |\n\n### Load Patterns\n\n| Class | Behavior |\n|-------|----------|\n| `OpenLoop(dist=Periodic(per_second=N))` | Fixed-rate: N operations/second regardless of latency |\n| `OpenLoop(dist=Gaussian(mean, stddev))` | Variable-rate: sleep drawn from gaussian distribution |\n| `ClosedLoop(...)` | Sequential: next operation starts when previous completes |\n\n### Guarantees and Regression Thresholds\n\n```python\n# Absolute guarantees (fail if not met)\nguarantees={\n    \"SELECT 1 (standalone)\": {\"qps\": 15, \"p99\": 400, \"max\": 500},\n}\n\n# Relative thresholds (fail if regression vs baseline)\nregression_thresholds={\n    \"SELECT 1 (pooled)\": {\"qps\": 0.8, \"p99\": 1.3},  # QPS must be >=80% of baseline\n}\n```\n\nAction names are auto-generated as `\"{query} ({connection_type})\"`.\n\n### Available Metrics\n\n`queries`, `qps`, `min`, `max`, `avg`, `p50`, `p95`, `p99`, `p99.9`, ..., `p99.999999`, `std`, `slope`.\n\n### Disabling a Scenario\n\n```python\nfrom materialize.parallel_benchmark.framework import disabled\n\n@disabled(\"Reason for disabling\")\nclass MyScenario(Scenario):\n    ...\n```\n\n### Running\n\n```bash\nbin/mzcompose --find parallel-benchmark run default --scenario MyScenario\nbin/mzcompose --find parallel-benchmark run default --scenario MyScenario --load-phase-duration 60\n```\n","frontmatter":{"name":"mz-benchmark","description":"Add/modify/debug Materialize perf benchmark scenarios. Three frameworks: Feature Benchmark (single-op micro), Scalability Test (SQL throughput under concurrency), Parallel Benchmark (sustained latency via scenarios.py). Trigger: \"benchmark\", \"feature benchmark\", \"scalability test\", \"parallel benchmark\", \"performance regression\", \"micro-benchmark\", \"TPS\", \"latency test\", or edits in feature_benchmark/scenarios/, scalability/workload/workloads/, parallel_benchmark/scenarios.py. Note: measurement, not panic-stress (see mz-parallel-workload).\n"},"isInternal":false,"tokens":2969,"sizeBytes":12903},{"name":"SKILL.md","path":".agents/skills/mz-commit/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-commit/SKILL.md","title":"mz-commit","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-commit\ndescription: >\n  Trigger: \"commit\", \"prepare commit\", \"create PR\", \"push\", \"open pull request\",\n  or mentions committing, pre-commit checks, pull requests in Materialize. Also\n  \"ship it\", \"ready to merge\". For code review use mz-pr-review.\n---\n\n# Committing in Materialize\n\nRead `doc/developer/guide-changes.md` for the full conventions on submitting and reviewing changes.\n\n## Pre-commit checklist\n\nBefore committing, run these and fix any warnings:\n\n1. `bin/fmt` (formats all `.rs`, `.py`, and `.proto` files; takes no file arguments)\n2. `bin/lint` (can error if tools are missing; use `bin/ci-builder run stable bin/lint` as an alternative)\n3. `cargo clippy --all-targets -- -D warnings`\n\n`bin/lint` and clippy do not run rustdoc. When the diff adds imports or edits\ndoc comments, run the Doctests check on the changed crates (command in the\nmz-debug-ci skill). When a `metric!` definition changed, `bin/lint`\nregenerates `doc/user/data/metrics.yml`. Commit the result, or the \"Lint and\nrustfmt\" job fails on the diff.\n\nAfter rebasing onto upstream/main, run `cargo check --workspace --all-targets`\neven when the rebase had no conflicts. Upstream changes can still break the\nbranch, for example a dependency your branch needs that upstream removed as\nunused (Nightly's `bin/unused-deps` reports those), a duplicate item, or a new\nenum variant your match does not cover. CI checks this in the\n\"Merge skew cargo check\" job.\n\nDo not manually update `*.snap` files.\nUse `cargo test` followed by `cargo insta accept` to update snapshot files.\nRewrite datadriven test expectations with `REWRITE=1 cargo test ...`.\n\n### LIR plan changes: regenerate the schema registry\n\nThe stable LIR format lets optimized plans be stored durably and reused, so\ntwo snapshot tests in `mz-compute-types` pin what a stored plan looks like\nand what it means. Both key off `LIR_VERSION` in\n`src/compute-types/src/plan.rs`.\n\n`lir_schema` traces the serde schema of everything reachable from\n`LirRelationExpr` into `tests/snapshots/lir_v{LIR_VERSION}.json`. Run it\nwhenever the diff changes the serde-visible shape of a type the trace\nreaches: anything in `src/compute-types/src/plan.rs` or\n`src/compute-types/src/plan/`, a `*Func` payload, or a plan-reachable type\nin `mz-expr` or `mz-repr` such as `AggregateFunc`, `TableFunc`,\n`SqlScalarType` or `DateTimeUnits`:\n\n```\ncargo test -p mz-compute-types --test lir_schema\n```\n\nRegenerate with `REWRITE=1 cargo test -p mz-compute-types --test lir_schema`\nand review the diff. It is the reviewable record of the format change. If\nthe current `LIR_VERSION` has shipped (see below), bump it first so the old\n`lir_v{N}.json` stays in place for migration tooling and the rewrite targets\na fresh file.\n\n### Scalar function changes: regenerate the function registry\n\nThe schema registry sees a plan's shape, not what its functions compute.\n`func_registry` covers that gap for `UnaryFunc`, `BinaryFunc`, and\n`VariadicFunc`. Run it whenever the diff touches\n`src/expr/src/scalar/func.rs` or `src/expr/src/scalar/func/` (including\n`impls/` and `#[sqlfunc]` bodies):\n\n```\ncargo test -p mz-compute-types --test func_registry\n```\n\n* It fails when a function is added, removed, or has a changed property\n  (nullability, error behavior, monotonicity, output type, sqlfunc\n  signature) or a changed `#[sqlfunc]` body. If the failure names removed or\n  changed functions, or a body change that alters results, settle the\n  `LIR_VERSION` question below **before** regenerating. A rewrite at the old\n  version overwrites that version's entry in `func_registry_digests.json`\n  instead of adding a new one, no later check catches it, and that entry is\n  the only key back into git history for what the shipped version's\n  functions meant. Once the version is settled, regenerate the checked-in\n  snapshots and review the diff under `src/compute-types/tests/snapshots/`:\n\n  ```\n  REWRITE=1 cargo test -p mz-compute-types --test func_registry\n  ```\n\n  Never hand-edit `func_registry*.json`. The test rebuilds the registry and\n  compares it against all three files, so an edit fails it.\n* **New function with a payload** (any variant that carries data, e.g. a\n  target type, a regex, a scale): the registry build panics naming the\n  variant. Add a representative `Sample` to `unary_samples()`,\n  `binary_samples()`, or `variadic_samples()` in\n  `src/expr/src/scalar/func/registry.rs`. If a property depends on the\n  payload (e.g. a cast only errors when it has a scale), add a second sample\n  with `.labeled(\"...\")` covering the other branch. Payload-free variants and\n  plain `#[sqlfunc]` functions need no sample.\n* **Removed or changed function, or a body change that alters results**: if\n  the current `LIR_VERSION` has shipped in a release, bump it so stored plans\n  are replanned, and only then regenerate. An unshipped version is\n  regenerated in place. Treat the version as shipped when the latest release tag carries the\n  same `LIR_VERSION`:\n\n  ```\n  git fetch --tags\n  git show $(git tag --list 'v*' --sort=-v:refname | grep -v -- '-rc' | head -1):src/compute-types/src/plan.rs | grep 'pub const LIR_VERSION'\n  ```\n\n  A bump is a judgment call, so confirm it with the user before making it.\n  Additions never need a bump. When unsure whether a body change alters\n  results, say so in the PR description rather than deciding silently.\n\nCommit the regenerated snapshots together with the function change, and call\nout the registry diff (added, removed, changed functions) in the PR\ndescription.\n\n## PR titles and commit messages\n\nMaterialize uses squash merging, so the PR title becomes the commit subject on `main`.\n\n* Use imperative mood: \"Fix X\" not \"Fixed X\" or \"Fixes X\".\n* Be specific: \"Fix panic in catalog sync when controller restarts\" not \"Fix bug\".\n* Prefix with area if helpful: `adapter: `, `storage: `, `compute: `, `sql: `.\n\nWrite a thorough PR description explaining the rationale for the change.\nMention which tests were added or modified in the pull request description, but do not list which tests were run.\nAdd release notes for user-visible changes (should complete \"This release will...\").\n\n## Issue tracking: Linear only for new issues\n\nNew issues are filed in Linear, never in the `database-issues` GitHub repo. `database-issues` is legacy. Its open issues are still valid to read, link, and close, so an existing `database-issues#NNNN` reference in code or a comment stays as it is.\n\nReference the Linear issue by its its key, e.g. `Closes: SQL-450`. Don't include the full URL containing the issue title.\n\nWhen a change closes a legacy GitHub issue, `Fixes database-issues#NNNN` works for auto-closing.\n\n## Cargo.lock discipline\n\nNever regenerate the entire Cargo.lock — bare `cargo update` bumps every semver-compatible dep and introduces unrelated breakage (e.g., `os_info` pulling in `objc2` on macOS, `chrono-tz` changing timezone data, `serde_path_to_error` changing error formats).\n\n* **Adding a dep or changing features**: just `cargo check`. It updates only what's needed.\n* **Updating one crate**: `cargo update -p <crate>` (add `--precise <ver>` to pin).\n* **After any Cargo.lock change**, review the diff:\n  ```\n  git diff Cargo.lock | grep '^[+-]version' | head -40\n  ```\n  Pin back anything that moved unexpectedly: `cargo update -p <crate> --precise <old-version>`.\n* **After rebase conflicts in Cargo.lock**: resolve by taking HEAD's version then running `cargo check` (not `cargo update`). This preserves existing pins while adding only what the new commits require.\n\n## Git conventions\n\n* Work against the `main` branch of `MaterializeInc/materialize`.\n* Push branches to your fork.\n* Pull requests target `main` on `MaterializeInc/materialize`.\n* Each PR should contain one semantic change.\n* Put non-trivial user docs for a new feature in a separate PR. See \"Adding documentation for new features\" in `doc/user/README.md`.\n\n## Splitting large branches\n\nThe repo is squash-merge-only: every PR lands as exactly one commit on `main`, however many commits it had internally. Keep each commit/PR under ~500 changed lines, split by concern, not by the chronology of how the code was written.\n\n* Prefer GitHub's native stacked-PR support to land a chain of dependent PRs, where it's set up for the branch. Where it isn't, fall back to sequential landing, one PR at a time: cut a PR from the front, merge, `git fetch upstream && git rebase upstream/main` the remaining tail, repeat. Squash-only means merging an earlier PR always replaces its commits with one new commit, so any later branch still built on the old commits needs a rebase either way; sequential landing keeps that to one clean rebase per merge instead of compounding across an unmanaged chain.\n* Land foundational or low-conflict commits (design docs, scaffolding, new deps) first. The longer a tail branch lives, the more likely a concurrent unrelated PR touches the same files and turns a clean rebase into a real conflict.\n* To split one large diff into smaller commits, or squash a long messy history into fewer reviewable units, as an alternative to interactive rebase: `git reset --soft <base>` stages the full diff, then stage per group with `git add <files>` for a file-level split, or use `git checkout <sha> -- .` to reproduce an exact historical checkpoint's tree for a chronological squash. (Claude Code's Bash tool has no interactive terminal, so agents need this non-interactive form; use whichever you prefer by hand.) `git checkout <sha> -- .` only adds or updates paths present in `<sha>`, it never removes files that shouldn't exist yet at that checkpoint: compute the removal set first with `comm -23 <(git ls-files|sort) <(git ls-tree -r --name-only <sha>|sort)` and `git rm -f` those, or the checkpoint commit silently carries files from later history. Verify every checkpoint with `git diff HEAD <sha>` (must be empty) before moving to the next. Reattach already-clean commits on top with `git rebase --onto <new-branch> <old-base>` (also non-interactive).\n","frontmatter":{"name":"mz-commit","description":"Trigger: \"commit\", \"prepare commit\", \"create PR\", \"push\", \"open pull request\", or mentions committing, pre-commit checks, pull requests in Materialize. Also \"ship it\", \"ready to merge\". For code review use mz-pr-review.\n"},"isInternal":false,"tokens":2462,"sizeBytes":10012},{"name":"cost-and-structure.md","path":".agents/skills/mz-console-review/cost-and-structure.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-console-review/cost-and-structure.md","title":"Mz-console-review Skill","category":"anthropic-skill","format":"markdown","content":"# Mz-console-review Skill\n\nPath: `.agents/skills/mz-console-review/cost-and-structure.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-console-review/cost-and-structure.md)","tokens":300,"sizeBytes":241},{"name":"SKILL.md","path":".agents/skills/mz-console-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-console-review/SKILL.md","title":"mz-console-review","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-console-review\ndescription: >\n  Review a Console (`console/`) PR or diff the way Console reviewers do: run the\n  change to verify claims, check it against the patterns and helpers the app\n  already has, and post findings as one GitHub review. Trigger: \"review this\n  console PR\", \"review my console changes\", \"does this console change look ok\",\n  a github.com/MaterializeInc/materialize/pull/<N> link whose files are under\n  `console/`, or a request to look over a React/TypeScript diff in this repo.\n  For Rust changes use mz-pr-review instead.\nargument-hint: [pr-number]\nallowed-tools: [Bash, Read, Grep, Glob]\n---\n\n# Console code review\n\n`mz-pr-review` covers the Rust checklists, none of which apply under `console/`.\nThe canonical Console rules live in `console/CLAUDE.md` (component conventions,\nimport restrictions, TypeScript and React patterns, testing philosophy) and in\n`console/doc/` (`guide-testing.md`, `guide-tanstack-table.md`,\n`mz-backwards-compatibility.md`). Read the sections the diff touches. Do not\nrestate them in comments, cite them.\n\nThis skill is what Console reviewers actually flag. Section 4 is distilled from\nroughly 570 reviewer comments on about 150 Console PRs from 2025 and 2026, and is\nordered by how often each kind of feedback came up. Sections 5 and 6 are not from\nthat tally. They cover cost and structure that reviewers rarely catch by\nreading, measured on `mz_catalog_server` and in render-count probes.\n\n## 1. Get the diff, and make it runnable\n\nRemotes are usually `upstream` = `MaterializeInc/materialize` and `origin` = a\npersonal fork, so a PR branch will not resolve against `origin`:\n\n```bash\ngit fetch upstream pull/<N>/head:pr-<N>-review\n```\n\nTo run anything you need the files inside `console/`, because `node_modules`\nlives at `console/node_modules` and a fresh worktree does not have it. Check the\nfiles into a tree with no local edits under `<path>`, run, then clean up. The\nrestore removes files the PR added and reverts the ones it changed, and it also\ndiscards any local edits under `<path>`:\n\n```bash\ngit checkout pr-<N>-review -- console/src/<path>\n(cd console && npx vitest run src/<path>)\ngit restore --source=HEAD --staged --worktree -- console/src/<path>\ngit branch -D pr-<N>-review\n```\n\nConfirm `git status --short` is empty before you finish.\n\n## 2. Verify every behavioral claim before writing it\n\nRun the suite and state the result (\"30/30 pass\"). Probe anything you intend to\nassert. A claim about draw order, nullability, or event handling that you reasoned\nout but did not execute is a claim you will get wrong.\n\nVitest hides `console.log` in passing tests. Assert against a sentinel to see a\nvalue: `expect(actual).toEqual(\"MARKER\")`.\n\njsdom makes graph and pointer assertions silently vacuous:\n\n| Symptom | Cause | Fix in the probe |\n|---|---|---|\n| Geometric assertions pass trivially | `ParentSize` measures nothing, width is 0 | `vi.mock(\"@visx/responsive/lib/components/ParentSize\", ...)` with a fixed size |\n| Pointer handler commits `NaN` | no `createSVGPoint` / `getScreenCTM`, so visx `localPoint` fails | `vi.mock(\"@visx/event\", () => ({ localPoint: vi.fn(() => ({ x, y })) }))` |\n\nMeasure colors instead of judging them: `d3.lab(color).l` gives perceived\nlightness. `d3` is already a dependency.\n\nFor SQL claims, compile the builder and read the SQL. Kysely's `.select` appends,\nit does not replace, so a \"fallback\" select can emit a duplicate column.\n\n## 3. Read the PR before the code\n\nReviewers raise these before any line comment:\n\n* **Motivation is traceable.** A linked issue (a Linear key such as `Closes: CNS-192`, or `Fixes database-issues#N` for a legacy GitHub issue), a Slack thread, or a design doc. If a reviewer cannot tell why the change exists, ask for context before reviewing.\n* **UI changes link the Figma design** in the description.\n* **Stacked PRs are linked** in each description, or the work is one PR with logical commits. Review semantic commits one at a time when the author asks.\n* **Commit messages are imperative and descriptive** per `doc/developer/guide-changes.md`, not \"addressed feedback\".\n* **A removed compatibility shim states its floor.** If the PR deletes a version map or gate, the description names the minimum supported environment version.\n* **WIP code is marked** with `TODO` (spelled `TODO`, not `TO-DO`), including leftover `console.log`.\n* **New pages and routes sit behind a LaunchDarkly flag.** The point is developer experience: it lets the work merge incrementally. Self-managed flags default to `true` in `src/config/flexibleDeploymentFlags.ts`, so a flag meant to stay off there must be set to `false` explicitly.\n\n## 4. What reviewers flag, most frequent first\n\n### 4.1 Remove and simplify\n\nThe single most common request. Look for:\n\n* Unused variables, atoms, exports, props, and query keys. Variables that are always `true`.\n* Redundant fallbacks (`?? 0` behind a value that cannot be null) and conditionals that render nothing.\n* Wrapper hooks that re-expose library options as custom `enable*` props. Keep wrappers thin, since the library's own options are better documented.\n* Features with no requirement behind them (column pinning, LRU layers). Ask \"is there a requirement for this?\"\n* A mini rules engine where two `if` statements would do.\n* Extra state flags (`destroyed`, `initialized`) that every method must check, and piles of refs. Stale refs are a bug source.\n* A copied error state when the mutation hook already exposes `error`.\n* `!!x` inside an expression that is already boolean, and several early returns that can merge into one.\n\n### 4.2 SQL and catalog correctness\n\n* **Nullable columns.** `disk_bytes` and friends are nullable. Coalesce, and fix the cast. When dividing, check the *denominator* for zero, not each operand.\n* **`ORDER BY ... DESC` puts nulls first.** Add `nulls last` where the column is nullable.\n* **User objects are `id LIKE 'u%'`**, via `isSystemId` in `~/api/materialize`. Not `name NOT LIKE 'mz_%'`, and not a bare `startsWith(\"u\")`.\n* **Statistics tables keep rows for dropped replicas** for up to a day. Aggregates like `bool_and(snapshot_committed)` must filter to extant replicas.\n* **Sources:** filter to top-level sources the way `sourceList.ts` does. Subsources and progress collections re-enter through joins on `mz_hydration_statuses`. Newer sources expose subsources as tables.\n* **Latest vs max.** Is the query returning the most recent sample or the largest one, and is that what the UI claims?\n* **A windowed flag must not drive a point-in-time answer.** \"Unreadable at some point in the window\" is not \"unreadable now\". Check which columns and predicates a window-wide flag reaches.\n* **Multi-process skew.** Summing usage over processes hides a single process about to OOM. Prefer `MAX`, and prefer existing catalog views over recomputing their math client-side.\n* **Two surfaces, two answers.** When a PR touches one of several queries for the same metric (list vs detail page), check the staleness bounds and column semantics match. A column can mean different things in the old and new relation.\n* Factor a repeated join or subquery into `src/api/materialize/expressionBuilders.ts` and left-join it, rather than `GROUP BY` just to pull one column.\n* Aggregate with `jsonb_agg` / `list_agg` and format on the client, instead of `STRING_AGG` or `pg_size_pretty` in SQL.\n* Raw SQL goes through the `sql` tagged template. Qualified names are structured (`{ databaseName, schemaName, name }` quoted with `sql.id()` or `quoteIdentifier`), never a dotted string, which breaks on names containing periods.\n* Comment catalog behavior the docs do not cover (for example `object_sub_id IS NULL` meaning the relation itself), and rewrite clever predicates for intent (`BOOL_AND(x IS NOT NULL)` over count arithmetic).\n\n### 4.3 Reuse before re-rolling\n\nBefore accepting any new helper, grep for what it replaces. Reviewers name these most:\n\n| Re-rolled | Already exists |\n|---|---|\n| clamp, pluralize, capitalize, null-guard | `clamp`, `pluralize`, `capitalizeSentence`, `notNullOrUndefined` in `~/util` |\n| Percent and byte text | `formatPercentage(fraction, decimals)` (takes a fraction. Call it directly and handle null inline, since a local `formatPercent` wrapper reads as a re-roll), `formatBytesShort`, `humanizeBytes`, `formatMemoryUsage` in `~/utils/format` |\n| Sorting | d3 `sort(iterable, ...accessors)` (multi-key ties, returns a copy), `handleSortOrder` in `~/utils/sort`, `sortingFunctions` (`nullsLast`, `numericNullsLast`) in `components/Table/tableColumnBuilders.ts`. The same comparator written twice becomes one shared helper |\n| `60_000`-style time constants | `millisecondsInMinute`, `millisecondsInHour` from `date-fns/constants` (not the `date-fns` root in v4) |\n| Object type text | `kebabToTitleCase`, `kebabToSentenceCase`, `kebabToScreamingSpaceCase` (SQL keywords such as `MATERIALIZED VIEW`) in `~/util`, `formatObjectType` in `platform/clusters/format.ts` |\n| Route strings built by hand | path helpers in `~/platform/routeHelpers` (`regionPath`, `useBuildWorkflowGraphPath`, ...). Add one there when it is missing |\n| Logic copied from a neighboring page | extract one helper and switch both call sites to it, so the copies cannot drift |\n| System-object check | `isSystemId` in `~/api/materialize` |\n| Duration formatting, lag caps | `formatInterval`, `formatDurationForAxis` in `~/utils/format` (fractional units are reserved for seconds). `formatDurationForAxis` is for axes: it rounds to whole minutes and caps at 24h, so 90s reads \"2m\" in text. The 24h cap `MAX_LAG_MS` is module-private there and `FreshnessGraph.tsx` declares its own copy, so export one instead of adding a third |\n| An anchor with custom hover/underline | `TextLink` from `~/components/TextLink` |\n| Status colors copied from a pill | export `colorSchemeToStyles` from `StatusPill.tsx` |\n| Layout props threaded one by one | the `containerProps` pattern, see `Gauge.tsx` |\n| A new cluster-sizes query | `useAvailableClusterSizes` / `fetchAvailableClusterSizes` |\n| A new \"all objects\" fetch | `useAllObjects` and the global subscribes (`useGlobalUpsertSubscribe`) |\n| A new filter dropdown | mirror `SchemaFilter.tsx` / `DatabaseFilter.tsx` (reserved \"all\" id, caller sets width) |\n| A new DAG layout | `dagreGraphHelpers.ts` and the workflow graph components |\n| A `useEffect` + timeout debounce | `lodash.debounce` (see `ObjectExplorer.tsx`) |\n| Previous-value or latest-value refs, or a callback read inside a debounce or timer | `usePrevious`, `useLatestRef` from `@chakra-ui/react` (see `useAutomaticallyConnectSocket.ts`) |\n| Tab active state from `location.pathname` | `NavLink`'s `isActive`, see `PageTabStrip` |\n| Set diffing by hand | `Set.prototype.difference` |\n| Secret masking | export `formatAppPassword` from `~/queries/frontegg` |\n| A per-tenant localStorage key | export `buildCacheKey` from `platform/shell/commandCache.ts` |\n| Chart axes, grid, tooltip, legend, cursor | `buildXYGraphLayoutProps` and friends in `~/utils/graph`, `~/components/graphComponents`, `useChartLegend`, `useCursorState`, `colors.lineGraph` |\n| visx plumbing | `Line` from `@visx/shape` for straight lines (as `GraphLineCursor` does), `Group` from `@visx/group` instead of repeating offset arithmetic on each child, `ParentSizeProvidedProps` to type a `ParentSize` child's props, `useDrag` from `~/hooks/useDrag` for draggable handles |\n| Several small components for one feature | one `xComponents.tsx` file, like `formComponents.tsx` |\n\nWhen a new component duplicates an existing one, say which pages it lands on and\nwhat those pages have today.\n\n### 4.4 Tests\n\nValue over volume. Look for:\n\n* **Tests that assert on query-builder string output, SQL snapshots, CSS attributes, or class names.** Replace with SQL tests (`*.test.sql.ts`) that create real objects and assert results, or with user-visible assertions.\n* **Missing corner cases.** Ask for the harder fixture: a deeper role hierarchy, overlapping buckets, multiple inherited roles, users that can log in. For graph fixtures, an ASCII diagram in a comment helps.\n* **Assert the outcome, not the call.** After a `GRANT`, query `mz_internal.mz_show_all_privileges`.\n* **Fixtures that do not look like real data.** Mock only the relation under test, and make values realistic.\n* **`vi.mock` / `vi.spyOn` on app modules.** Reviewers dislike them (unstable, break on circular imports). Prefer MSW via `buildSqlQueryHandlerV2`, with per-call query keys when one mutation issues several statements, or dependency injection.\n* **Redundant happy-path slices.** One happy-path test plus separate corner cases beats ten tests that all break together. Multiple assertions per integration test are fine.\n* Unnecessary setup (`GRANT CREATEROLE` when the test already logs in as a superuser).\n* Regex over `toContain` for generated code, `!` over `?.` inside tests so they fail early, no `if` branches around expects, constants hoisted out of `describe` in `SCREAMING_SNAKE_CASE`.\n* Every bug found in review gets a regression test. New helpers and classes get unit tests.\n* A test that asserts a function's arity or a mock's call shape breaks when someone adds an optional parameter. Assert what the user sees instead.\n\n### 4.5 Types and nullability\n\n* No `any`, no casts that the types do not need (`as unknown as T`).\n* Ask why each field is nullable when every caller defines it.\n* Fields that are always null together become one nested object (`cluster: { id, name } | null`).\n* Two booleans that can conflict (`usersOnly`, `rolesOnly`) become a discriminated union.\n* Boolean props are optional (`canIssue?: boolean`) so `<X canIssue />` works. An error prop is `error?: boolean`, not `unknown`.\n* Type a required key as required instead of throwing when it is missing.\n* `===` over `==`. Prefer `undefined` over `null` for optional values.\n\n### 4.6 Naming\n\n* Units are suffixes: `durationMs`, not `ms`.\n* Booleans are present tense (`hasActiveSubscription`). Components are nouns, not verbs (`LicenseKeyCtaContent`, not `GetLicenseKey`).\n* `Inner` suffix only for a suspense boundary split, `Content` for a block of content.\n* `build...Query` is reserved for SQL builders. Transforms are `transform...`, fetchers `fetch...`, DOM listener setup `register...`.\n* Do not alias a name as an id (`parent.name as parentId`). SQL aliases are camelCase, named for the general concept (`relationComment`, not `tableComment`).\n* Callback props keep parity with DOM events (`onPointMove`, `onClick`), not new verbs like `onPointSelect`.\n* A variant that changes styling is a `variant` prop, not inferred from which callbacks were passed.\n* Icons end in `Icon`. Version gates are named constants (`MCP_OAUTH_MIN_VERSION`), not inline strings.\n* Loop variables say what they index (`bucketIndex`, not `j`).\n\n### 4.7 Code comments\n\nThe rules are in the root `CLAUDE.md` \"Code comments\" section. What reviewers\nflag in practice: comments with fluff, details that make no sense out of context\n(\"avoids alphanumeric sorting issues\"), and comments that no longer match the\ncode. They ask *for* comments on undocumented catalog behavior, on unconventional\nuses (react-query used only to get loading/error state for an initializer), on\nnon-obvious algorithms, and on props whose meaning is subtle. It is not necessary\nto describe the evolution of the code by referring to code that has been removed\n(\"use quicksort because bubble sort is inefficient\").\n\n### 4.8 Loading, error, and empty states\n\n* Every `useQuery` renders distinct loading, error (`isError`), and empty UI. A mutation failure is visible to the user, not only `console.error`.\n* No state falls through to an endless spinner (a disabled region is an error, not \"loading\").\n* Loading is a spinner inside the card, not \"Loading...\" text, which shifts layout on every refetch. Empty states render inside the card, not as a blank page.\n* Prefer `useQuery` with explicit states over new `useSuspenseQuery`.\n* **The global subscribes start empty.** `useAllClusters` and `useAllObjects` return `data: []` until `snapshotComplete`, so a page that reads empty as \"none\" shows \"No clusters\" or a false success on a cold load. Gate on `snapshotComplete`, and render the subscribe's error.\n* An entity with nothing to report gets one empty state, not a green \"0 of 0 within target\" summary over an empty chart.\n\n### 4.9 Data layer\n\n* Query keys start with `buildRegionQueryKey(...)` (`~/api/buildQueryKeySchema`), so switching region or organization never serves another region's cache.\n* Queries live under `src/api/materialize/...`, not in `platform/`.\n* `staleTime` / `gcTime` are deliberate. If data never goes stale, use `Infinity`, and invalidate on logout or mutation.\n* Refresh derived data by putting its query key in the namespace that mutations already invalidate, rather than polling (a 60s poll means 60s-stale ownership).\n* No new `useSql*` hook calls. `console/CLAUDE.md` (\"React Query\") has the read and write pattern.\n* Global subscribes are hoisted to the route that owns the section, or navigating remounts them and the view jitters.\n* Validate user-supplied parameters at the `build...Query` layer (see the start-time check in `queryHistoryList.ts`), not only in the UI.\n* Anything persisted to localStorage is scoped by organization and region, and cleared on region switch. An unscoped key leaks one tenant's catalog into another session.\n\n### 4.10 React hooks and state\n\n* No `useEffect` to sync props into state or to react to a user action. Do it in the event handler, or initialize state in the modal's `onOpen`.\n* Effect dependencies hold the primitive that matters (the JWT string, not the whole query result), so work reruns only when it should.\n* TanStack table `columns` are static. Dynamic data flows through `data`, or cells remount and tooltips vanish.\n* One owner per lifecycle: a connection manager owns connect, disconnect, and retry, and the hook is a thin shell. Shell output changes go through the `websocketFsm.ts` state machine.\n* For Jotai, a derived write-only atom with an early return, not returning the previous object and hoping it skips a render.\n* A helper that mutates variables outside its scope is harder to read than the same code inline with a comment.\n\n### 4.11 Routing and URL state\n\n* View modes and modal flows (graph vs table, edit role) are subroutes, so a reload keeps the user's place.\n* Filter and lookback state belongs in the URL, not localStorage, when it should survive reload and share.\n* Do not branch on `location.pathname` when `<Route>` already branches.\n* A URL-selected entity that does not exist (a dropped cluster in an old link) must not silently show a different one. Write the fallback into the URL, or say it was not found.\n* Imports use `~/` unless the file shares a parent directory.\n\n### 4.12 Versions, flags, and deployment modes\n\n* A query that reads a new column or relation breaks older environments. Gate it with `useEnvironmentGate` and a named version constant, per `console/doc/mz-backwards-compatibility.md`. Self-managed can run a new Console against much older environments.\n* Test across versions with emulator images on another port (`misc/docker/README.md` in the Console tree) or a local kind cluster.\n* Cloud-only initialization goes in `CloudAppInitializer`, not `AppInitializer` (flexible deployments and impersonation).\n* `forceShow` on a nav item breaks when the environment is disabled.\n* Superuser checks through `usePrivileges` hit `has_table_privilege` and load environmentd. Use `useIsSuperUser`.\n* An `AppConfigSwitch` whose branches render the same thing is noise.\n\n### 4.13 Visual and UX QA\n\nReviewers run the branch and attach screenshots or recordings. If you can run it\n(`console/CLAUDE.md` has the dev commands), do. Otherwise list these for the human\nreviewer:\n\n* Overflow, especially in modals, filters, and form footers.\n* Dark mode. Any literal color (`colors.green[50]`, hex, `hsl(`) is suspect, since it does not flip.\n* Spacing on the 4px grid in relative units (`3`, not `\"12px\"`). Valid `textStyle` values come from `src/theme/typography.ts`. Valid Chakra tokens for `zIndex`. Chakra style props over `style`.\n* **Types do not catch invalid style values.** Chakra passes `alignItems=\"left\"` straight through (Chrome computes it as `normal`, which `CSS.supports(\"align-items\", \"left\")` confirms), and an undefined `textStyle` such as `text-ui-sm` falls back to the default without an error.\n* Consistency with existing patterns: the app's purple color scheme, existing button hover behavior, filter chips like query history, an Apply button for unit inputs. \"We don't do this anywhere else\" is a common and valid objection.\n* Layout shift on refetch, jitter on navigation, flicker on load.\n* Thresholds that agree with each other (a value turns orange at 5s but the filter starts at a minute).\n* Copy: title case in titles, correct terminology (\"role with LOGIN attribute\", not \"privilege\"), where to save a generated config, a link to the docs page.\n* Semantic elements (`ul`/`li` for lists).\n* **Mouse-only click targets.** A clickable `Tr`, `HStack` or legend pill with `onClick` cannot be reached from the keyboard. Make it a button (`as=\"button\"`, `type=\"button\"`) with `aria-pressed` for toggles (see `connectComponents.tsx`), and prefer `:focus-visible` over an always-on focus ring.\n* One placeholder for a missing value across a page (`-` or `—`, not both).\n\n### 4.14 Security and session traps\n\nRare but high severity, and most often caught by automated QA review after merge:\n\n* Tenant or region state that survives a switch: caches, module-level singletons, persisted rows seeded with `snapshotComplete: true`.\n* Auth flows where the client clock or cached token decides a redirect. Redirects should be server-authoritative, or users loop.\n* Two concurrent sessions (password and OIDC) where queries silently run as the wrong account.\n* State-changing `GET`s, and protections that rely on another system's cookie config (`SameSite`) instead of checking `Origin` / `Sec-Fetch-Site`.\n* `btoa` on non-ASCII input. Service accounts where the code assumes a user email.\n\n### 4.15 Charts and graphs\n\nGraph PRs collect the same gotchas. Check each:\n\n* **Reuse first.** The 4.3 rows for graph helpers and visx plumbing. A block near identical to `FreshnessGraph`, `UtilizationGraph` or `ThresholdLineGraph` should come from the same pieces.\n* **Hover redraws only the cursor and tooltip.** Mark layers (lines, bars, rows) are `React.memo` with stable props: scales and layouts memoized on width and domain, series memoized. A memo only helps if callers hoist their arrays. One built inline reruns every render.\n* **Volatile inputs get their own layer.** Anything derived from `useAllObjects` or `useAllClusters` gets a new identity on any change anywhere in the environment. Draw it as its own memoized layer, or key its memo by content, so an unrelated DDL does not redraw the chart.\n* **Which point is hovered.** `findNearestDatum` returns the nearest bucket start, which near a bar's right edge is the next bar. For bucketed bars, find the bucket that contains the time.\n* **Decorative layers keep `pointerEvents=\"none\"`** and sit under the event overlay, or a full-width strip swallows hover.\n* **Labels state only what the data says.** A max-per-bucket series is a peak, not an average. Name the unit precisely (\"% of replica CPU allocation\"), and do not label an inferred state as recorded.\n* **Wide windows stay readable.** Bars narrower than a few pixels alias into stripes. Merge buckets client-side, keeping each metric's peak.\n* **Colors.** Series take `colors.lineGraph`. A generated palette varies perceived lightness (`d3.hcl` / `d3.lab`), not HSL lightness.\n* **Axis formatters are for axes.** `formatDurationForAxis` and tick formatters round. Headline and table values need the unrounded formatter.\n* **Refresh cost.** A chart that polls re-reads its whole window on every refetch. See 5.1 in `cost-and-structure.md`.\n\n### 4.16 Design docs\n\nFor `doc/design/` PRs, reviewers ask for concrete ranges and limits, error states\n(an unresponsive cluster), reuse of existing global subscribes and components,\nconsistency when two polled queries resolve at different times (a subscribe\navoids it), client-side formatting, and a scoped first version (superuser only)\nbefore the general case.\n\n## 5. Check what the change costs\n\nWhen the diff changes a query builder, a subscribe, a `refetchInterval` or\n`staleTime`, or anything that runs at pointer or keystroke rate, read section 5\nof `cost-and-structure.md` in this skill's directory and work through it. The\n`mz_catalog_server` cost and the render cost it checks do not show up in a read\nof the diff.\n\n## 6. Audit hooks and component structure\n\nWhen the diff adds or moves a `useEffect`, `useMemo`, `useCallback`, or\n`React.memo`, or splits a component, read section 6 of `cost-and-structure.md`.\n\n## 7. Write the comment the way a Console reviewer writes it\n\nFindings are triaged with the person you are reviewing for first. Present the\nlist, let them cut and rephrase, then post. Do not post a review off your own draft.\n\nComments are short, hedged, and collaborative. State the claim, give at most one\nclause of why, name the alternative, stop. Typical openers: `Nit:`, `I think we\nshould`, `Maybe we can`, `Could we reuse`, `Let's just`, `I'd recommend`,\n`I wonder if`, `Kinda feels like`, `Why is X nullable here?`, `How does this look\nin dark mode?`.\n\nReal comments at the right length:\n\n> nit: We usually like to use `<>` for react fragments\n\n> Can most likely reuse the TextLink component\n\n> Why is objectId nullable here? Usage of this hook shows me that it's always defined\n\n> Kinda feels like these should be grouped into a `cluster` object, especially if both of them can be null\n\n> Let's remove this comment.\n\nKeep measurements and probe output out of the comment. They belong in your reply\nto the person you are reviewing for. A bug claim is the exception: attach the\nrepro (a screenshot, a recording, or a short diff that triggers it), because that\nis what makes it actionable.\n\nPoint at the existing pattern with a permalink rather than describing it. Offer\npseudocode or a suggestion block when the fix is not obvious. Say when something\nis not blocking: \"Doesn't need to be blocking but I suggest in a followup PR,\"\n\"We can do this as a followup.\"\n\n## 8. Post as a single review\n\nOne review, not N comments, so the author gets one notification:\n\n```bash\ngh api repos/MaterializeInc/materialize/pulls/<N>/reviews \\\n  --method POST --input review.json\n```\n\n`review.json` is `{commit_id, body, event: \"COMMENT\", comments: [...]}`. Use\n`event: \"COMMENT\"` unless the person asked to approve or request changes. Each\ninline comment is `{path, line, side: \"RIGHT\", body}`, and a multi-line anchor adds\n`start_line` and `start_side`. General findings go in `body`.\n\nPermalink every code reference and pin the SHA:\n\n* Code already on main gets the PR's **base** SHA (`gh pr view <N> --json baseRefOid`).\n* Code inside the PR gets the **head** SHA.\n* Verify each cited line resolves before posting: `git show \"<sha>:<path>\" | sed -n '<line>p'`. When the SHA is in a shell variable, brace it (`\"${sha}:${path}\"`). zsh reads `$sha:c` as a modifier and drops the colon, even inside quotes.\n\nAfterwards, confirm what landed:\n\n```bash\ngh api \"repos/MaterializeInc/materialize/pulls/<N>/comments?per_page=100\" \\\n  --jq '[.[] | select(.pull_request_review_id == <id>)] | length'\n```\n\nBefore posting, check for a pending review. GitHub allows one pending review per\nperson per PR, so a stray one blocks a new review, and one pinned to an older\ncommit cannot take new comments because their lines resolve against that commit:\n\n```bash\ngh api \"repos/MaterializeInc/materialize/pulls/<N>/reviews?per_page=100\" \\\n  --jq '[.[] | select(.state == \"PENDING\")]'\n```\n\n* **Inline comments must fall inside a diff hunk.** Read the ranges with `gh api \"repos/MaterializeInc/materialize/pulls/<N>/files\" --jq '.[].patch' | grep '^@@'`. Anything outside goes on the file (`subjectType: FILE`) or in the body.\n* **Replies and new threads in one review** need GraphQL: `addPullRequestReview` with `commitOID`, then `addPullRequestReviewThread` and `addPullRequestReviewThreadReply` with that `pullRequestReviewId`, then `submitPullRequestReview` with `event: COMMENT`.\n* **Pending comments are invisible to REST**, where `pulls/comments/<id>` returns 404. Read and edit them through GraphQL (`updatePullRequestReviewComment`).\n* **Link the priorities.** When a review mixes bugs and nits, list the bugs in the body with `#discussion_r<id>` links so the author fixes those first.\n* **Pace the batches.** When the author has just received a large review, post the blocking items and hold structural suggestions until they respond.\n\n## 9. Re-review after the author pushes\n\n* **Diff from the last SHA you reviewed, not from the base:** `git fetch upstream pull/<N>/head:pr-<N>-r2 && git diff <last-reviewed-sha> pr-<N>-r2`.\n* **Sort each earlier comment into fixed, partly fixed, or open by reading the code at the new head.** Replies do not count, and a fix without a reply is fine. Thread state comes from GraphQL `reviewThreads { isResolved isOutdated }`.\n* **Probe every fix for the bug it might introduce.** Removing an effect left a debounce that could strand a drag handle, and scoring unreadable objects as breaching reached a \"right now\" column. Both passed the existing tests.\n* **When the author pushes back with numbers, check they measured what you suggested,** and time a plan label rather than trusting it. `lookup` against `differential join` said nothing about which was faster. Reply in the thread with your own measurement, since numbers belong in that reply.\n* **Re-run the suite on the new head,** and say so in the body.\n\n## Output\n\nGroup findings as **Blocking**, **Strong suggestions**, **Nits**, and mark\nfollow-up candidates. Open the summary with one specific sentence on what the PR\ndoes well (\"Loving the tests!\", \"Great work making this page dynamic, and it uses\na lot of our existing patterns\"), then the verdict (\"Small nits but otherwise\nlooking really good\", \"Once my comments are addressed, LGTM\"). Name the test\nresult. Flag anything you could not verify rather than asserting it.\n","frontmatter":{"name":"mz-console-review","description":"Review a Console (`console/`) PR or diff the way Console reviewers do: run the change to verify claims, check it against the patterns and helpers the app already has, and post findings as one GitHub review. Trigger: \"review this console PR\", \"review my console changes\", \"does this console change look ok\", a github.com/MaterializeInc/materialize/pull/<N> link whose files are under `console/`, or a request to look over a React/TypeScript diff in this repo. For Rust changes use mz-pr-review instead.\n","argument-hint":["pr-number"],"allowed-tools":["Bash","Read","Grep","Glob"],"allowedTools":["Bash","Read","Grep","Glob"]},"allowedTools":["Bash","Read","Grep","Glob"],"isInternal":false,"tokens":7358,"sizeBytes":30381},{"name":"SKILL.md","path":".agents/skills/mz-dbt-release/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-dbt-release/SKILL.md","title":"mz-dbt-release","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-dbt-release\ndescription: >\n  Cut a dbt-materialize PyPI release: bump the version in `__version__.py`\n  and `setup.py`, date the `Unreleased` CHANGELOG entry, and open the\n  release PR with a `Ship: <url>` body. Trigger: \"cut a dbt release\",\n  \"release dbt-materialize\", \"release the dbt adapter\", \"ship\n  dbt-materialize vX.Y.Z\", \"publish dbt-materialize to PyPI\", \"bump\n  dbt-materialize version\", \"new dbt adapter version\". Use this skill\n  even if the user just says \"ship the dbt adapter\" or pastes a feature\n  PR and asks for \"the next dbt release\" without naming version mechanics.\n---\n\n# Cutting a dbt-materialize release\n\nA release PR bumps the published version of the `dbt-materialize` PyPI\npackage. It is mechanical: three files change, one commit, one PR.\n\nThe adapter lives in `misc/dbt-materialize/`. PyPI publication is handled by\nCI after the PR merges.\n\n## Files to touch\n\nExactly three files:\n\n| File | Change |\n|---|---|\n| `misc/dbt-materialize/dbt/adapters/materialize/__version__.py` | Bump `version = \"X.Y.Z\"` |\n| `misc/dbt-materialize/setup.py` | Bump `version=\"X.Y.Z\"` (kwarg in `setup(...)`) |\n| `misc/dbt-materialize/CHANGELOG.md` | Insert `## X.Y.Z - YYYY-MM-DD` between `## Unreleased` and the existing bullets |\n\nBoth version strings must stay in sync — the comments in each file say so.\n\n## Picking the next version\n\n* Default: bump the **patch** component (`1.9.9 → 1.9.10`).\n* The **minor** component tracks the required `dbt-postgres` minor. Only bump\n  minor when upgrading to a new `dbt-postgres` minor (e.g. `1.9.x → 1.10.0`).\n* Read the current version from `__version__.py`.\n\n## Confirming what's being released\n\nThe `## Unreleased` section of `CHANGELOG.md` is the source of truth for what\nships. If it's empty, there is nothing to release. Cross-check with:\n\n```\ngit log <prev-release-sha>..HEAD -- misc/dbt-materialize/\n```\n\nIf a commit since the previous release touched `misc/dbt-materialize/` but is\nnot represented under `## Unreleased`, stop and ask whether it should be\nincluded — don't silently invent a changelog entry.\n\n## CHANGELOG edit\n\nThe release does **not** rewrite the unreleased bullets; it just dates them.\nInsert a new dated heading immediately after `## Unreleased`, leaving\n`## Unreleased` itself empty:\n\n```\n## Unreleased\n\n## 1.9.10 - 2026-05-20\n\n* Support unmanaged clusters in `deploy_init`. ...\n```\n\nUse today's date in `YYYY-MM-DD`.\n\n## Branch, commit, PR\n\n* Branch name: `dbt-release-X.Y.Z`, branched from current `main`.\n* Commit subject: `dbt-materialize: release vX.Y.Z`\n* Commit body / PR body: a single `Ship: <url>` line pointing at the PR that\n  triggered this release (the feature/fix PR being shipped). If multiple PRs\n  are being shipped, pick the headline one — the changelog already enumerates\n  them.\n* PR title: same as the commit subject.\n* PR targets `MaterializeInc/materialize:main`.\n\nExample:\n\n```\ngh pr create --repo MaterializeInc/materialize --base main \\\n  --head <your-fork>:dbt-release-1.9.10 \\\n  --title \"dbt-materialize: release v1.9.10\" \\\n  --body \"Ship: https://github.com/MaterializeInc/materialize/pull/36625\"\n```\n\n## What you do NOT run\n\nThis PR touches only Python version strings and Markdown. Skip:\n\n* `cargo check`, `cargo clippy`, Cargo.lock work — no Rust changes.\n* Test suites — there are no tests to add for a version bump.\n\n`bin/fmt` and `bin/lint` cover `misc/dbt-materialize/*.py`, but a\none-character version bump almost never triggers them. Run them if\nyou've made an accidental wider change; otherwise they're optional.\n\n## Verifying before pushing\n\n```\ngit diff --stat\n```\n\nShould show exactly three files, predominantly a `+4 / -2` shape (one\nline each in the two version files, two added lines in `CHANGELOG.md`).\nA few extra lines in `CHANGELOG.md` are fine if a bullet was reformatted\nwhile finalising it; an unexpected fourth file means you've gone\noff-script.\n\n## Quick reference\n\n```\n# 1. Branch\ngit checkout -b dbt-release-X.Y.Z\n\n# 2. Edit the three files (see table above)\n\n# 3. Verify\ngit diff --stat   # expect 3 files, +4/-2\n\n# 4. Commit\ngit commit -am \"dbt-materialize: release vX.Y.Z\n\nShip: <feature-pr-url>\"\n\n# 5. Push and open PR\ngit push -u origin dbt-release-X.Y.Z\ngh pr create --repo MaterializeInc/materialize --base main \\\n  --head <your-fork>:dbt-release-X.Y.Z \\\n  --title \"dbt-materialize: release vX.Y.Z\" \\\n  --body \"Ship: <feature-pr-url>\"\n```\n\n## Historical reference\n\nRecent release commits (find via `git log --oneline -- misc/dbt-materialize/dbt/adapters/materialize/__version__.py`):\n\n* v1.9.10 — PR #36636\n* v1.9.9 — PR #36609\n* v1.9.8 — PR #36529\n\nInspect any of them with `git show <sha>` to see the exact diff shape.\n","frontmatter":{"name":"mz-dbt-release","description":"Cut a dbt-materialize PyPI release: bump the version in `__version__.py` and `setup.py`, date the `Unreleased` CHANGELOG entry, and open the release PR with a `Ship: <url>` body. Trigger: \"cut a dbt release\", \"release dbt-materialize\", \"release the dbt adapter\", \"ship dbt-materialize vX.Y.Z\", \"publish dbt-materialize to PyPI\", \"bump dbt-materialize version\", \"new dbt adapter version\". Use this skill even if the user just says \"ship the dbt adapter\" or pastes a feature PR and asks for \"the next dbt release\" without naming version mechanics.\n"},"isInternal":false,"tokens":1312,"sizeBytes":4701},{"name":"bk-fallback.md","path":".agents/skills/mz-debug-ci/bk-fallback.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/bk-fallback.md","title":"Mz-debug-ci Skill","category":"anthropic-skill","format":"markdown","content":"# Mz-debug-ci Skill\n\nPath: `.agents/skills/mz-debug-ci/bk-fallback.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/bk-fallback.md)","tokens":300,"sizeBytes":209},{"name":"rare-failures.md","path":".agents/skills/mz-debug-ci/rare-failures.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/rare-failures.md","title":"Mz-debug-ci Skill","category":"anthropic-skill","format":"markdown","content":"# Mz-debug-ci Skill\n\nPath: `.agents/skills/mz-debug-ci/rare-failures.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/rare-failures.md)","tokens":300,"sizeBytes":213},{"name":"SKILL.md","path":".agents/skills/mz-debug-ci/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/SKILL.md","title":"mz-debug-ci","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-debug-ci\ndescription: >\n  Investigate CI failures on PR via gh + Buildkite MCP or bk CLI. Trigger: failing checks,\n  Buildkite failures, CI issues — \"why is CI red\", \"build broken\",\n  \"checks failing\", \"what went wrong in CI\", \"nightly broke\",\n  \"tests failing on this PR\", or pasted Buildkite URL. Also PR number +\n  why failing.\nargument-hint: <PR number, GitHub PR URL, or Buildkite build URL>\n---\n\nInvestigate CI failures for a Materialize PR or Buildkite build.\n\nIf the input is a Buildkite build URL rather than a PR (a scheduled nightly on\nmain, a release-qualification build), start directly at \"Listing a\nbuild's failed jobs directly\", taking `<PIPELINE>` and `<BUILD_NUMBER>`\nstraight from the URL:\n`https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>`,\nthen continue from Step 2.\n\n## Prerequisites\n\nThis skill requires `gh` (GitHub CLI), authenticated, plus one of two ways\nto reach Buildkite: the Buildkite MCP server's tools (preferred, next\nsection) or `bk` (Buildkite CLI), authenticated. If any of these turns out\nto be missing or unauthenticated, **stop immediately** and tell the user\nwhat to fix (`bk configure` or `bk auth login` for the Buildkite CLI, or the\nMCP setup below). Do not attempt to use the REST API directly or any other\nworkaround.\n\n## Buildkite MCP: preferred when available\n\nWhen the Buildkite MCP server's tools are available in the session (tool\nnames like `get_build_failure_summary`, `search_logs`, ...), prefer them\nover `bk` everywhere below: structured JSON with the\njob-state semantics already applied, and no log cleanup needed. The server documents its\nown tools; this section adds only what it does not know:\n\n- `org_slug` is `materialize`. `get_build_failure_summary` replaces \"Listing\n  a build's failed jobs directly\", Step 2, and often Step 3;\n  `search_logs`/`read_logs`/`tail_logs` replace the `bk job log` pipelines;\n  the artifact and annotation tools replace the corresponding `bk` commands.\n  Everything Materialize-specific (annotation contents, `bin/ci-failures`,\n  `bin/ci-shards`, Steps 4-5) applies unchanged.\n- The summary's job list is bounded by the response's `job_limit` (10 on the\n  hosted server, where a larger `max_jobs` is silently clamped), with terminal\n  problem jobs sorted before downstream broken ones. On `jobs_truncated:\n  true`, get the definitive failed-job list from `list_jobs` with\n  `state: \"failed,timed_out\"` and `include_retried_jobs: false`. That last\n  parameter is what preserves triage semantics: the server defaults it to\n  true, which counts every failed attempt of a retried job separately,\n  including jobs that passed on retry. The summary bounds annotation content\n  too: on `content_truncated: true`, fetch the full set with\n  `list_annotations`.\n\nIf the MCP is absent, read `bk-fallback.md` in this skill's directory for\nthe `bk` command recipes and continue with those. At the end, mention to\nthe user that they can set the server up (hosted read-only endpoint:\n`claude mcp add --scope user --transport http buildkite https://mcp.buildkite.com/mcp/readonly`;\nit saves tool calls but costs somewhat more tokens).\n\n## Search existing failures by pattern\n\nSearch for already recorded failures in CI with a short, stable error\nsubstring. The output is JSON whose `content` blobs are huge, so project it\ndown, and read the total from `.meta.totalRowCount`, which ignores the row\ncap and is the \"how chronic is this?\" number:\n\n```bash\nbin/ci-failures 'foobar' 2>/dev/null | jq -r '.meta.totalRowCount,\n  (.data[] | [.build_date[0:10], .build_identifier, .test_suite, .issue] | @tsv)'\n```\n\nThe positional pattern matches failure content. `--search <text>` replaces\nthe positional pattern and matches across build, test, issue, content, and\nbranch at once. Narrow results with `--branch main`, `--version`, `--issue`,\n`--test` (the Buildkite job name), `--build`, `--start-date`, or\n`--end-date`; `--size` raises the row cap (default 31, max 100).\n\n## Step 1: Find the build\n\nParse `$ARGUMENTS` to get the PR number (a plain number or a\n`https://github.com/MaterializeInc/materialize/pull/<PR_NUMBER>` URL), then\nlist the failing checks:\n\n```bash\ngh pr checks <PR_NUMBER> 2>&1\n```\n\nFailing rows link to\n`https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>#<JOB_ID>`,\nthe identifiers used throughout this skill.\n\n### Listing a build's failed jobs directly\n\nBuildkite mirrors each job's result into GitHub as a\n`buildkite/<pipeline>/<job>` commit status (never-dispatched jobs post\nnothing), but `gh pr checks` reads the PR's current head commit only. A build\ntherefore shows up there only if it ran on exactly that commit. For everything\nelse (scheduled nightlies on main, release-qualification builds, a PR nightly\nfrom before the latest push), list the genuinely failed jobs via the API:\n`list_jobs` as described in the MCP section, or with `bk` the listing recipe\nin `bk-fallback.md`. Either way, also grab the build's commit and branch:\nStep 5's known-vs-new checks need the commit, and neighboring-build lookups\nneed the branch.\n\nJob-state semantics matter here:\n- The failure states are `failed` and `timed_out`, the same set as\n  `BUILDKITE_RELEVANT_FAILED_BUILD_STEP_STATES` in\n  `misc/python/materialize/buildkite_insights/buildkite_api/buildkite_constants.py`.\n- `broken` is not a failure. Buildkite marks a job `broken` when its\n  configuration prevents it from running, for example a branch filter or an\n  `if:` condition that evaluates false, so the job never dispatches. Every\n  build contains broken jobs (a routine main nightly has dozens), so counting\n  them massively over-counts failures.\n- Jobs with `soft_failed: true` are allowed to fail without making the build\n  red; no current CI step sets it, so filtering it is defensive.\n- `waiting_failed` is not a failure either: the job's dependency (usually a\n  build step) failed, so it never started. Its presence is informative,\n  though. A PR build full of `waiting_failed` jobs ran no tests at all, so\n  report that fixing the visible failures is necessary but no proof the\n  build then goes green.\n- Retried jobs (automatic or manual) appear only as their latest attempt, so\n  a job that failed and then passed on retry counts as passed. That is the\n  right verdict for build triage; fetch the earlier attempts only when an\n  in-depth investigation needs them. A manual retry that failed again is\n  itself a signal: someone already tried to shake the failure off and it\n  reproduced.\n\nTo find a PR's builds that are not on its current head in the first place (an\nolder nightly, a pre-push run), get the branch with\n`gh pr view <PR_NUMBER> --json headRefName` and filter Buildkite builds by\nbranch; the MCP's `list_builds` takes a `branch:` filter, and fork branches\nare named `<owner>:<branch>` on Buildkite.\n\n## Step 2: Check annotations first\n\n**Before diving into logs**, fetch the build annotations. They contain pre-extracted error messages, stack traces, and links to known flaky test issues — this saves significant time compared to grepping through raw logs. With the MCP they arrive in `get_build_failure_summary` (or `list_annotations`); with `bk`, use the annotations recipe in `bk-fallback.md`.\n\nBesides the error itself, an error annotation (`style` `\"error\"`) carries\nknown-issue links (Linear keys like `CPU-170`, or legacy\n`database-issues/#NNNN`) and the job's main-branch history, a flaky-test\nindicator.\n\nThings to know when reconciling annotations against jobs:\n\n- Error annotations persist from failed attempts even when a retry later\n  passed, so a build can have more error annotations than failed jobs. A\n  `bin/ci-annotate-errors` annotation's `context` field is `<JOB_ID>-error`,\n  and that job id can belong to a retried attempt that the default job\n  listing does not contain. (Other producers use other contexts, e.g.\n  `images-not-public` from the build step.)\n- Only jobs running through the mzcompose/cloudtest plugins get annotations\n  and `bin/ci-failures` rows (both come from `bin/ci-annotate-errors`). Plain\n  shell steps (lint, Security advisories, and similar checks) produce neither,\n  so a real failure there has no annotation at all. Go straight to its job\n  log.\n- Info-style annotations (\"<job> succeeded with known error logs\") mean the\n  job passed and every logged error matched an open known issue; they are\n  not failures. When a job went red via \"Test succeeded, but unknown errors\n  found in logs\" (see Step 3), the unknown and potential-regression entries\n  in its annotation are what turned it red. The reverse also exists: a\n  failed job turns green (\"<job> would have failed with known error logs\")\n  when all its logged errors match issues marked `ci-ignore-failure: true`.\n\n## Step 3: Fetch logs when needed\n\nOnly fetch full logs when annotations don't provide enough detail. On PR test\nbuilds, read compile and lint job logs first (clippy, lint-and-rustfmt): they\noften explain every downstream failure.\n\nFetch and search logs with the MCP's `search_logs`/`read_logs`/`tail_logs`,\nor with `bk` use the job-log recipes in `bk-fallback.md`. The log tail can be a\nred herring: some jobs print long non-error output after the actual error\n(cargo deny's dependency tree runs hundreds of lines past the advisory), so\nwhen the tail shows no error, search or grep for the error instead of\ntailing more.\n\nNOTE: for mzcompose-based jobs (testdrive, SQLsmith/SQLancer, platform checks,\n...) the job console log holds only the harness's output. The services' own\noutput (environmentd/clusterd panics and errors) goes to `services.log` and\nsimilar files inside the job's log artifacts. A panic that is absent from\nthe job's console log can still be the failure, so never conclude \"this\nerror did not happen in this run\" from it alone. Check the annotations or\n`bin/ci-failures` instead, both are fed by `bin/ci-annotate-errors`, which\nscans the uploaded log artifacts (`services.log`, `run.log`, junit XML, ...)\nat the end of each job. The same scan can fail a job whose own workflow\npassed: the console log then reads `Test succeeded, but unknown errors found\nin logs, marking as failed`, and the cause is whatever the scan found in the\nartifacts, not a test failure. Alternatively, download the log artifacts and\ngrep them directly.\n\n### Artifacts\n\nJobs upload artifacts (junit XML, service logs, coredumps, ...). Use the\nMCP's `list_artifacts_for_build`/`list_artifacts_for_job`/`get_artifact`,\nor with `bk` use the artifacts recipes in `bk-fallback.md`.\n\n### Shard contents\n\nSharded jobs (SLT, testdrive, platform checks, feature benchmark, ...) record\nthe workflows/files/scenarios they ran in build meta-data. `bin/ci-shards`\nshows it:\n\n```bash\n# Mapping of every sharded job to what it ran, with a link per job whose\n# `#` fragment is the `<JOB_ID>` for fetching job logs (omit links with --no-url)\nbin/ci-shards https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>\n\n# Which job(s) ran a specific item (test file, scenario, or workflow,\n# `workflow_foo_bar` function names are matched as `foo-bar`)\nbin/ci-shards https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER> numeric.td\n```\n\n## Step 4: Categorize failures\n\nUse these Materialize-specific patterns to diagnose:\n\n### Clippy errors\nCode lint issues in changed files. Fix the code, not the lint config.\n\n### `check-test-flags` lint failure\nA new dyncfg (`Config::new(\"name\", ...)`) was introduced but not registered in the required places. Reproduce with `bin/lint-test-flags`.\n- `misc/python/materialize/parallel_workload/action.py` (`FlipFlagsAction.flags_with_values` or `uninteresting_flags`)\n- `misc/python/materialize/mzcompose/__init__.py` (get_variable_system_parameters / get_minimal_system_parameters / UNINTERESTING_SYSTEM_PARAMETERS)\n\n### Doctests failure\nThe Doctests job runs `bin/doc --document-private-items` (`ci/test/lint-doc.sh`), which sets `RUSTDOCFLAGS=-D warnings` and uses the pinned nightly. Reproduce on one crate with `bin/doc --document-private-items -p <crate>`. A common cause is a public doc comment linking a private item (`rustdoc::private_intra_doc_links`), which fails even with `--document-private-items`. `cargo doc` stops at the first failing crate, so a fix can reveal errors in the next crate.\n\n### Merge skew cargo check failure\nThis PR-only job (`ci/test/check-merge-with-target.sh`) merges the PR with its target branch and runs `cargo check --all-targets`. When it is red while the PR's own build and clippy jobs pass, the branch compiles alone but not merged with current main. Rebase and fix as the mz-commit skill describes.\n\n### Testdrive cascades\nAfter one test crashes environmentd, all subsequent tests in that shard fail with `Name or service not known` or `connection closed`. **Only the first failure in a shard matters** — everything after it is a cascade. Look for the first `error:` or `FAIL` in the log.\n\nTestdrive shards with the same number (e.g., `testdrive-10` and `testdrive-with-alloydb-10`) run the same tests — if both fail, it's likely to be the same root cause.\n\n### SLT failures\nCheck whether it's wrong output (behavioral change) vs. connection error (crash/timeout). Wrong output means the query semantics changed. An `InconsistentViewOutcome` failure comes from CI's `--auto-index-selects` mode; the mz-test skill explains the mechanism and the exemption lists.\n\n### Timeouts\nA `timed_out` job hit its step budget (`timeout_in_minutes` in\n`ci/<pipeline>/pipeline.template.yml`). The annotation records only the fact\n(\"test timed out\"), never the cause. Find the cause in the console log: the\nlast completed unit of work, plus anything marked as still running when the\njob was canceled (nextest prints `SLOW [>1200.000s]` lines naming the stuck\ntests). If no such marker appears, compare wall-clock against the job's last\npassing run to tell a gradually outgrown budget from a hung test.\n\n### Infrastructure flakes\nObserved: failures in unrelated jobs while downloading from `github.com/.../releases/download/` were GitHub CDN outages. Retry the job instead of debugging it.\n\n### Rarer classes\n\nIf the failure matches one of these, read `rare-failures.md` in this\nskill's directory for its triage pattern: `New regression against\n<version>` (feature benchmark), a cargo-fuzz crash, a Miri failure, a\nlimits-test failure, or `ImagesNotPublicError`.\n\n## Step 5: Summarize\n\nGroup failures by **root cause**, not by job name — typically many failing\njobs share just 1-2 root causes. Per root cause, report a description, the\naffected jobs, and what to fix.\n\nDistinguish between issues that are clearly caused by the change under test vs. pre-existing flaky tests. The annotations often link to known flaky test issues — use these to identify pre-existing flakes vs. new regressions.\n\nMore ways to establish known vs new:\n\n- Check whether main already fixed it. Any build can run code that is behind\n  main: a nightly investigated the morning after, but also a fresh PR based\n  on an older main. From an up-to-date main checkout run\n  `git log <BUILD_COMMIT>..HEAD -- <suspect-file>` or\n  `git log -S '<error token>'`. A fix can also sit in a not-yet-merged PR,\n  invisible to git log: search open PRs for the step name or error token\n  (`gh pr list --state open --search '<...>'`); a PR's file list shows\n  which of the build's root causes it covers. Do this for every failure,\n  and name any hit in the summary so nobody authors a duplicate. When\n  citing a later build as evidence\n  of a fix, confirm the specific job's state there is `passed`: a build can\n  be green because the job was `broken` and never ran.\n- Known-issue tracking lives in Linear. Annotations and `bin/ci-failures`\n  cite issue keys like `CPU-170` or `SS-361`, which resolve to\n  `https://linear.app/materializeinc/issue/<KEY>`. Legacy references point at\n  GitHub `MaterializeInc/database-issues` instead.\n- Annotations and `bin/ci-failures` classify a failure three ways; for the\n  latter two, check the signature's history with `bin/ci-failures` before\n  treating the failure as new. A known-issue link means it matched an open\n  tracked issue. `\"UNKNOWN ERROR\"` means it matched no tracked issue, not\n  that the failure is new: a long streak under that label is a chronic\n  untracked flake worth filing. An issue reference tagged\n  `(POTENTIAL REGRESSION)` means it matched a tracked issue that is now\n  closed: a known signature with stale tracking, not by itself evidence of\n  a new regression; suggest to the user that the closed issue may need\n  reopening. The issue matching is a content heuristic and often wrong, so\n  verify that the matched issue actually describes the problem: search the\n  tracker for the error text, since the right issue may already exist open,\n  or the problem may need a totally new one.\n","frontmatter":{"name":"mz-debug-ci","description":"Investigate CI failures on PR via gh + Buildkite MCP or bk CLI. Trigger: failing checks, Buildkite failures, CI issues — \"why is CI red\", \"build broken\", \"checks failing\", \"what went wrong in CI\", \"nightly broke\", \"tests failing on this PR\", or pasted Buildkite URL. Also PR number + why failing.\n","argument-hint":"<PR number, GitHub PR URL, or Buildkite build URL>"},"isInternal":false,"tokens":4114,"sizeBytes":16817},{"name":"SKILL.md","path":".agents/skills/mz-dependencies/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-dependencies/SKILL.md","title":"mz-dependencies","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-dependencies\ndescription: >\n  Trigger: adding, removing, or updating a Rust dependency, changing crate\n  features, touching Cargo.toml or Cargo.lock, \"cargo update\", \"add a crate\",\n  \"bump a dependency\", or a new dependency license in Materialize.\n---\n\n# Dependency management\n\n## Workspace dependencies\n\nAll third-party versions declared in `[workspace.dependencies]` in root\n`Cargo.toml`. Members use `dep.workspace = true` or\n`dep = { workspace = true, optional = true }`.\n\n* **Add new dep**: add to `[workspace.dependencies]` in root `Cargo.toml` first, then `dep.workspace = true` in member crate.\n* **Never inline version** in member `Cargo.toml` — `bin/lint-cargo` enforces.\n* **Update version**: change once in root `Cargo.toml`.\n* Cargo unifies features workspace-wide, so workspace declaration is union of all features across crates. Members just use `dep.workspace = true`.\n\n## Cargo.lock\n\nNever regenerate full Cargo.lock. When changing deps:\n\n* **Add dep or change features**: run `cargo check` — updates only what changed.\n* **Update specific crate**: `cargo update -p <crate>` (optional `--precise <version>`).\n* **Never bare `cargo update`** — bumps every semver-compatible dep, causes unrelated breakage from transitive changes.\n* **If lock regenerated**, diff before commit (`git diff Cargo.lock | grep '^[+-]version'`) and pin back unintended bumps with `cargo update -p <crate> --precise <old-version>`.\n\n## Licensing\n\nTwo files control license policy, **keep in sync**: `deny.toml` (`[licenses].allow`) and `about.toml` (`accepted`). New dep with new license not already allowed: add SPDX identifier to both.\n","frontmatter":{"name":"mz-dependencies","description":"Trigger: adding, removing, or updating a Rust dependency, changing crate features, touching Cargo.toml or Cargo.lock, \"cargo update\", \"add a crate\", \"bump a dependency\", or a new dependency license in Materialize.\n"},"isInternal":false,"tokens":389,"sizeBytes":1649},{"name":"SKILL.md","path":".agents/skills/mz-feature-flags/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-feature-flags/SKILL.md","title":"mz-feature-flags","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-feature-flags\ndescription: >\n  Trigger: adding or changing a feature flag, dyncfg, or system parameter\n  default, reviewing a PR that adds one, LaunchDarkly flag setup, or\n  bin/lint-test-flags failures in Materialize.\n---\n\n# Feature flags\n\nChoosing whether a new feature flag defaults on or off in production is a\nproduct decision that needs context an agent rarely has. When adding a flag,\nask a human which default to use. When reviewing a PR, do not question the\ndefault it chose. However, in tests, a new flag should usually be on, so that\nsqllogictest and testdrive exercise the new code path. If\nits production default is off, turn it on for CI in\n`misc/python/materialize/mzcompose/__init__.py`\n(`get_minimal_system_parameters` if tests fail with it off, otherwise\n`get_variable_system_parameters`), which `bin/sqllogictest` and mzcompose's\n`Materialized` service pass to environmentd as system parameter defaults.\nRust integration tests use `TestHarness::with_system_parameter_default`.\nA value listed there overrides the compiled-in default in CI, so changing a\ndefault means changing both. `bin/lint-test-flags` fails unless every new\ndyncfg is registered for parallel workload and mzcompose (see the mz-debug-ci\nskill). A compiled-in default is not evidence of the production value: check\nwhether `test/launchdarkly-flag-consistency/mzcompose.py` lists the flag in\n`INTENTIONAL_LD_OVERRIDES` or `KNOWN_CROSS_ENV_DIVERGENCES`. A session\nvariable takes a system default only if `SESSION_SYSTEM_VARS` in\n`src/sql/src/session/vars.rs` lists it. Otherwise `--system-parameter-default`\nfor it only logs a warning at startup. Observed: the LaunchDarkly MCP\n`create-flag` tool creates boolean flags that serve true in every environment,\nincluding production. A human must set each environment's off variation and\nfallthrough to false.\n","frontmatter":{"name":"mz-feature-flags","description":"Trigger: adding or changing a feature flag, dyncfg, or system parameter default, reviewing a PR that adds one, LaunchDarkly flag setup, or bin/lint-test-flags failures in Materialize.\n"},"isInternal":false,"tokens":433,"sizeBytes":1852},{"name":"SKILL.md","path":".agents/skills/mz-limits-test/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-limits-test/SKILL.md","title":"mz-limits-test","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-limits-test\ndescription: >\n  Add/modify/debug limits test. Trigger: \"limits test\", \"Generator subclass\",\n  \"many objects\", \"scaling test\", or stress-test Materialize with many objects\n  (tables, views, sources, indexes). Also edits in test/limits/mzcompose.py.\n---\n\n# Limits Test Framework\n\nThe limits test stresses Materialize with large numbers of objects, complex queries, and large ingestions. It catches regressions via fixpoint panics, stack overflows, CI timeouts (O(N^M) with N~1000), and OOM panics (2GB memory limit).\n\n## Where It Lives\n\n- **Main file**: `test/limits/mzcompose.py`\n- All Generator subclasses live in that single file\n- **Auto-discovery**: Subclasses of `Generator` are found automatically via `all_subclasses(Generator)` from `materialize.util` - no registration needed\n\n## Generator Base Class\n\n```python\nclass Generator:\n    COUNT: int = 1000      # Number of objects to create (override per test)\n    VERSION: str = \"1.0.0\" # Bump when test logic changes\n    MAX_COUNT: int | None = None  # Upper limit for --find-limit mode\n\n    @classmethod\n    def body(cls) -> None:\n        \"\"\"Override this. Print testdrive commands to stdout.\"\"\"\n        raise NotImplementedError\n\n    # Convenience iterators\n    @classmethod\n    def all(cls) -> range:       # range(1, COUNT+1)\n    @classmethod\n    def no_first(cls) -> range:  # range(2, COUNT+1) - skip first\n    @classmethod\n    def no_last(cls) -> range:   # range(1, COUNT) - skip last\n\n    @classmethod\n    def store_explain_and_run(cls, query: str) -> str | None:\n        \"\"\"Records EXPLAIN query for timing and prints `> {query}`.\"\"\"\n```\n\n`header()` is inherited - it resets the `public` schema and grants privileges. `footer()` prints a blank line. `generate()` calls `header()` → `body()` → `footer()`. `mz_system` is a built-in testdrive connection, so `$ postgres-execute connection=mz_system` works in `body()` without any registration.\n\n## How to Add a New Limits Test\n\n1. Add a new class in `test/limits/mzcompose.py` that extends `Generator`.\n2. Implement the `body()` classmethod. It must `print()` testdrive commands.\n3. Optionally override `COUNT` and `MAX_COUNT`, only if it's causing issues in CI.\n\n### Minimal Template\n\n```python\nclass MyNewFeature(Generator):\n    COUNT = min(Generator.COUNT, 100)  # Lower if operation is slow\n    MAX_COUNT = 800  # Optional: cap for --find-limit mode\n\n    @classmethod\n    def body(cls) -> None:\n        # Bump system limits if needed\n        print(\"$ postgres-execute connection=mz_system\")\n        print(f\"ALTER SYSTEM SET max_tables = {cls.COUNT * 10};\")\n        print(\"$ postgres-execute connection=mz_system\")\n        print(f\"ALTER SYSTEM SET max_objects_per_schema = {cls.COUNT * 10};\")\n\n        # Create objects\n        for i in cls.all():\n            print(f\"> CREATE TABLE t{i} (f1 INTEGER);\")\n            print(f\"> INSERT INTO t{i} VALUES ({i});\")\n\n        # Query and verify\n        for i in cls.all():\n            cls.store_explain_and_run(f\"SELECT * FROM t{i}\")\n            print(f\"{i}\")\n```\n\nYou can run it locally with `bin/mzcompose --find limits down && bin/mzcompose --find limits run main --scenario=MyNewFeature`, add `--find-limit` for the Release Qualification mode.\n","frontmatter":{"name":"mz-limits-test","description":"Add/modify/debug limits test. Trigger: \"limits test\", \"Generator subclass\", \"many objects\", \"scaling test\", or stress-test Materialize with many objects (tables, views, sources, indexes). Also edits in test/limits/mzcompose.py.\n"},"isInternal":false,"tokens":812,"sizeBytes":3235},{"name":"SKILL.md","path":".agents/skills/mz-parallel-workload/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-parallel-workload/SKILL.md","title":"mz-parallel-workload","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-parallel-workload\ndescription: >\n  Extend parallel-workload stress framework: random SQL concurrently to catch\n  panics + unexpected errors (not perf — see mz-benchmark). Trigger:\n  \"parallel workload\", \"parallel-workload\", \"action.py\" re parallel workload,\n  or testing panics/unexpected errors under concurrency. Also \"add this to\n  parallel workload\" or bug that panics under concurrent DDL/DML.\n---\n\n# Extending Parallel Workload\n\nThe parallel-workload framework stress-tests Materialize by running random SQL actions concurrently across multiple threads. It catches **panics and unexpected query errors** - it does not verify result correctness.\n\n## When to Use This Framework\n\nAdd coverage here when a bug manifests as:\n- A panic under concurrent operations\n- An unexpected error from a query that should succeed (or fail gracefully)\n- A crash or connection loss triggered by specific DDL/DML combinations\n\n## How It Works\n\n1. The orchestrator spawns N worker threads, each assigned an **action list** (read, fetch, write, dml_nontrans, or ddl) based on the configured `Complexity`.\n2. Each worker randomly selects actions from its list using **weighted random choice** and executes them in a loop until the runtime expires.\n3. If an action raises a `QueryError`, the worker checks `action.errors_to_ignore(exe)`. If the error matches, it's logged and ignored. If not, the **test fails**.\n4. All threads share a `Database` object that tracks created objects (tables, views, clusters, etc.) with thread-safe locking.\n\n## Adding a New Action\n\n### Step 1: Define the Action Class\n\nAdd your class in `action.py`. Subclass `Action` and implement `run()`:\n\n```python\nclass MyNewAction(Action):\n    def run(self, exe: Executor) -> bool:\n        # Return False to skip (e.g., precondition not met)\n        # Return True after successful execution\n        exe.execute(\"SELECT my_new_feature()\", http=Http.RANDOM)\n        return True\n```\n\n### Step 2: Override `errors_to_ignore()` If Needed\n\nIf your action can produce expected errors (e.g., object was concurrently dropped), extend the base list:\n\n```python\n    def errors_to_ignore(self, exe: Executor) -> list[str]:\n        result = super().errors_to_ignore(exe)\n        result.extend([\n            \"my expected error message\",\n        ])\n        # Conditionally ignore errors based on complexity or scenario\n        if exe.db.complexity == Complexity.DDL:\n            result.extend([\"does not exist\"])\n        return result\n```\n\n### Step 3: Register in an Action List\n\nAdd your action class and weight to the appropriate list at the bottom of `action.py`:\n\n```python\n# In ddl_action_list, read_action_list, write_action_list, etc.\nddl_action_list = ActionList(\n    [\n        ...\n        (MyNewAction, 5),  # weight controls selection frequency\n        ...\n    ],\n    autocommit=True,  # True for DDL, False for transactional actions\n)\n```\n\n**Which list to use:**\n- `read_action_list` - SELECT, SUBSCRIBE, COPY TO (autocommit=False)\n- `fetch_action_list` - DECLARE CURSOR / FETCH (autocommit=False)\n- `write_action_list` - INSERT, COPY FROM (autocommit=False)\n- `dml_nontrans_action_list` - DELETE, UPDATE, INSERT RETURNING (autocommit=True)\n- `ddl_action_list` - CREATE/DROP/ALTER/RENAME (autocommit=True)\n\n## Common Patterns\n\n### Picking a Random Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    with exe.db.lock:\n        if not exe.db.tables:\n            return False\n        table = self.rng.choice(exe.db.tables)\n    # Use table outside db.lock but inside table.lock for mutations\n    with table.lock:\n        if table not in exe.db.tables:\n            return False  # Was dropped while we waited\n        exe.execute(f\"ALTER TABLE {table} ...\", http=Http.RANDOM)\n    return True\n```\n\n### Respecting Object Limits\n\n```python\ndef run(self, exe: Executor) -> bool:\n    if len(exe.db.tables) >= MAX_TABLES:\n        return False  # Skip if at capacity\n    ...\n```\n\nLimits are defined in `database.py`: `MAX_TABLES=5`, `MAX_VIEWS=15`, `MAX_CLUSTERS`, `MAX_SCHEMAS`, etc.\n\n### Creating a New Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    if len(exe.db.views) >= MAX_VIEWS:\n        return False\n    view_id = exe.db.view_id\n    exe.db.view_id += 1\n    try:\n        schema = self.rng.choice(exe.db.schemas)\n    except IndexError:\n        return False\n    with schema.lock:\n        if schema not in exe.db.schemas:\n            return False\n        view = View(self.rng, view_id, base_object, schema, ...)\n        view.create(exe)\n    exe.db.views.append(view)\n    return True\n```\n\n### Dropping a Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    with exe.db.lock:\n        if len(exe.db.views) <= 2:\n            return False  # Keep a minimum\n        view = self.rng.choice(exe.db.views)\n    with view.lock:\n        if view not in exe.db.views:\n            return False\n        exe.execute(f\"DROP VIEW {view}\", http=Http.RANDOM)\n        exe.db.views.remove(view)\n    return True\n```\n\n### Scenario-Specific Actions\n\nSome actions only run in specific scenarios. Override `applicable()` for\nthis, do NOT gate on the scenario inside `run()`: the worker skips\ninapplicable actions without counting an attempt, which keeps the\nend-of-run action coverage check (an action that never succeeds over a\nwhole run fails it) free of false positives.\n\n```python\ndef applicable(self, exe: Executor) -> bool:\n    return exe.db.scenario == Scenario.Rename\n\ndef run(self, exe: Executor) -> bool:\n    ...\n```\n\n### Using Prepared Statements\n\n```python\ndef run(self, exe: Executor) -> bool:\n    query = \"SELECT ...\"\n    if self.rng.choice([True, False]):\n        self.stmt_id += 1\n        self.exe_prepared(query, f\"mystmt{self.stmt_id}\", exe)\n    else:\n        exe.execute(query, http=Http.RANDOM)\n    return True\n```\n\n## Extending an Existing Action\n\nOften the simplest approach is to add new SQL variants to an existing action's `run()` method. For example, to test a new expression type, add it to `expression.py`. To test a new system flag, add it to `FlipFlagsAction.flags_with_values`.\n\n### Adding a System Flag to FlipFlagsAction\n\nIn `FlipFlagsAction.__init__`, add an entry to the `flags_with_values` dictionary:\n\n```python\nself.flags_with_values[\"my_new_flag\"] = [\"true\", \"false\"]\n```\n\n## Running\n\n```bash\n# Default: DDL complexity, regression scenario, 600s runtime\nbin/mzcompose --find parallel-workload run default\n\n# Custom options\nbin/mzcompose --find parallel-workload run default \\\n    --complexity=ddl \\\n    --scenario=regression \\\n    --runtime=300 \\\n    --seed=42\n\n# Available complexities: read, dml, ddl, ddl-only, random\n# Available scenarios: regression, cancel, kill, rename, backup-restore, 0dt-deploy, random\n```\n\n## Important Design Rules\n\n1. **Minimize locking** - Use `exe.db.lock` briefly to pick objects, then use per-object locks. Excessive locking reduces the chance of finding real race conditions. You can instead add an expected error in `errors_to_ignore`\n2. **Return False, don't raise** - If a precondition isn't met (empty list, at capacity), return `False` to skip the action.\n3. **Handle IndexError from `rng.choice`** - Objects can be concurrently removed. Either check length first under lock, or catch `IndexError` and return `False`.\n4. **Track state changes** - If you create an object, append it to the appropriate `exe.db` list. If you drop one, remove it.\n5. **Use `self.rng`** - Always use the action's seeded `random.Random` instance, never `random.choice()` directly. This makes failures reproducible via `--seed`.\n","frontmatter":{"name":"mz-parallel-workload","description":"Extend parallel-workload stress framework: random SQL concurrently to catch panics + unexpected errors (not perf — see mz-benchmark). Trigger: \"parallel workload\", \"parallel-workload\", \"action.py\" re parallel workload, or testing panics/unexpected errors under concurrency. Also \"add this to parallel workload\" or bug that panics under concurrent DDL/DML.\n"},"isInternal":false,"tokens":1829,"sizeBytes":7568},{"name":"SKILL.md","path":".agents/skills/mz-platform-checks/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-platform-checks/SKILL.md","title":"mz-platform-checks","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-platform-checks\ndescription: >\n  Create/modify/debug platform check. Trigger: \"platform check\",\n  \"platform-checks\", \"upgrade check\", \"restart check\", or writing Check class\n  testing feature survival across restarts/upgrades. Also edits in\n  misc/python/materialize/checks/all_checks/.\n---\n\n# Platform Checks\n\nThe Platform Checks framework is \"write once, run everywhere\" - a single test validates a feature across multiple scenarios (restarts, upgrades, etc.).\n\n## Key Concepts\n\n- **Check**: A Python class that tests one feature via testdrive fragments.\n- **Scenario**: The context (e.g., restart, upgrade) in which checks run.\n- **Action**: An individual step within a scenario (e.g., stop a container).\n\n## Where Checks Live\n\nAll checks are in `misc/python/materialize/checks/all_checks/`. The `__init__.py` auto-discovers every `.py` file in that directory, so adding a new file is all that's needed for registration.\n\n## Anatomy of a Check\n\nA check is a class that extends `Check` and implements three methods. Each returns a `Testdrive` object wrapping a testdrive fragment string.\n\n```python\nfrom textwrap import dedent\nfrom materialize.checks.actions import Testdrive\nfrom materialize.checks.checks import Check\n\nclass MyFeature(Check):\n    def initialize(self) -> Testdrive:\n        \"\"\"Run once at the start. Create tables, insert seed data, set up objects.\"\"\"\n        return Testdrive(dedent(\"\"\"\n            > CREATE TABLE my_feature_table (f1 INTEGER);\n            > INSERT INTO my_feature_table VALUES (1), (2), (3);\n        \"\"\"))\n\n    def manipulate(self) -> list[Testdrive]:\n        \"\"\"MUST return a list of exactly 2 Testdrive fragments.\n        Each runs after a restart/upgrade phase. Insert more data, ALTER objects,\n        or create derived objects (e.g., materialized views).\"\"\"\n        return [\n            Testdrive(dedent(\"\"\"\n                > INSERT INTO my_feature_table VALUES (4), (5);\n                > CREATE MATERIALIZED VIEW my_feature_view1 AS\n                  SELECT COUNT(*) AS c FROM my_feature_table;\n            \"\"\")),\n            Testdrive(dedent(\"\"\"\n                > INSERT INTO my_feature_table VALUES (6), (7);\n                > CREATE MATERIALIZED VIEW my_feature_view2 AS\n                  SELECT SUM(f1) AS s FROM my_feature_table;\n            \"\"\")),\n        ]\n\n    def validate(self) -> Testdrive:\n        \"\"\"Run after all initialize + manipulate steps. Verify everything survived.\n        MAY BE CALLED MORE THAN ONCE - must be idempotent. Any objects created\n        here must be TEMPORARY or explicitly dropped.\"\"\"\n        return Testdrive(dedent(\"\"\"\n            > SELECT * FROM my_feature_view1;\n            7\n            > SELECT * FROM my_feature_view2;\n            28\n        \"\"\"))\n```\n\n## Execution Order in a Scenario\n\n```\nStartMz → initialize() → [restart/upgrade] → manipulate(phase=0)\n  → [restart/upgrade] → manipulate(phase=1) → [restart/upgrade] → validate()\n```\n\nBoth manipulate phases always run. validate() may run multiple times.\n\n## Decorators and Version Gating\n\n### Disable a check\n```python\nfrom materialize.checks.checks import disabled\n\n@disabled(ignore_reason=\"due to SQL-450\")\nclass MyBrokenCheck(Check):\n    ...\n```\n\n### Non-idempotent external interactions\nIf a check ingests non-UPSERT data into Kafka/Postgres (i.e., replaying manipulate would duplicate data), annotate it:\n```python\nfrom materialize.checks.checks import externally_idempotent\n\n@externally_idempotent(False)\nclass MyKafkaCheck(Check):\n    ...\n```\n\n### Version-gated checks\nFor features that only exist from a certain version, override `_can_run`:\n```python\nfrom materialize.checks.executors import Executor\nfrom materialize.mz_version import MzVersion\n\nclass MyNewFeature(Check):\n    def _can_run(self, e: Executor) -> bool:\n        return self.base_version >= MzVersion.parse_mz(\"v0.80.0-dev\")\n```\n\n### Version-conditional testdrive lines\nUse `[version>=VVMMPP]` or `[version<VVMMPP]` prefixes for syntax that changed between versions (e.g., v0.92.0 → `9200`):\n```\n>[version<9200] CREATE SOURCE s FROM LOAD GENERATOR COUNTER (SCALE FACTOR 0.01)\n>[version>=9200] CREATE SOURCE s FROM LOAD GENERATOR COUNTER\n```\n\n## Tips for Good Checks\n\n1. **Naming**: Prefix ALL object names with a unique string (e.g., `my_feature_`) to avoid collisions with other checks running in the same Mz instance.\n2. **manipulate() returns exactly 2 elements**: The framework asserts this.\n3. **validate() is idempotent**: It may run multiple times. Use TEMPORARY objects or DROP at end.\n4. **Create objects in manipulate, not just initialize**: This tests that DDL works after restarts/upgrades.\n5. **Create derived objects on both old and new base objects**: e.g., a materialized view on a table from initialize AND on a table from manipulate.\n6. **Exercise full SQL syntax breadth**: Test every WITH option, every variant of the feature.\n7. **Ingest data in every phase**: Ensure data flows during restarts/upgrades.\n8. **Use `dedent()` on all testdrive strings**: Required for proper testdrive parsing.\n\n## Helper Methods on Check\n\n- `self.base_version`: The MzVersion the test started from (useful for upgrade scenarios).\n- `self.current_version`: The MzVersion currently running.\n- `self._kafka_broker()`: Returns the broker connection string for testdrive.\n- `self._default_cluster()`: Returns `\"quickstart\"`.\n- `self.rng`: A seeded `Random` instance for deterministic randomness.\n\n## Running\n\n```bash\n# Run a specific check against a specific scenario\nbin/mzcompose --find platform-checks run default --scenario=RestartEntireMz --check=MyFeature\n\n# Run with no restarts (useful for debugging the check itself)\nbin/mzcompose --find platform-checks run default --scenario=NoRestartNoUpgrade --check=MyFeature\n\n# Run all checks (default)\nbin/mzcompose --find platform-checks run default --scenario=RestartEntireMz\n```\n\n## Debugging CI Failures\n\n1. Find the scenario name near the top of the log: `Running ... --scenario=RestartEnvironmentdClusterdStorage`\n2. Find the failing check just before the error: `Running validate() from <materialize.checks.threshold.Threshold ...>`\n3. Reproduce locally: `bin/mzcompose --find platform-checks run default --scenario=RestartEnvironmentdClusterdStorage --check=Threshold`\n4. Try `--scenario=NoRestartNoUpgrade` to isolate whether the failure is restart/upgrade-related.\n","frontmatter":{"name":"mz-platform-checks","description":"Create/modify/debug platform check. Trigger: \"platform check\", \"platform-checks\", \"upgrade check\", \"restart check\", or writing Check class testing feature survival across restarts/upgrades. Also edits in misc/python/materialize/checks/all_checks/.\n"},"isInternal":false,"tokens":1489,"sizeBytes":6369},{"name":"SKILL.md","path":".agents/skills/mz-pr-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-pr-review/SKILL.md","title":"mz-pr-review","category":"anthropic-skill","format":"markdown","content":"---\nname: mz-pr-review\ndescription: >\n  Local code review of current branch vs Materialize standards. Trigger:\n  \"review my code\", \"review my changes\", \"check my diff\", \"does this look ok\",\n  \"what do you think of this PR\", \"code review\", or look over changes before\n  merging. Also PR number + wants feedback on quality, style, correctness.\n  For changes under `console/`, use mz-console-review instead.\nargument-hint: [base-branch]\nallowed-tools: [Bash, Read, Grep, Glob, Task]\n---\n\nPerform a local code review of the current branch's changes against Materialize project standards.\n\n## Steps\n\n1. Parse arguments from: $ARGUMENTS — a PR number, base branch, or nothing.\n2. Get the diff using the first method that works:\n   - **PR number given** (e.g. `123`): `gh pr diff 123`\n   - **git available**: `git diff <base>...HEAD` (default base: `main`)\n   - **jj available**: `jj diff -r <revset>` (default: diff from trunk)\n3. Get the file list from the same diff (add `--stat` for git, `--stat` for jj, or `gh pr diff 123 --stat` for PR).\n4. Review the diff against the checklists below.\n5. Present findings organized as: **Blocking**, **Strong suggestions**, **Nits**.\n\n## Review checklist\n\nThe overall developer guide for reviewing changes is defined in `doc/developer/guide-changes.md`, always read and follow its guidance.\n\n### Tests\n- Every behavior change has at least one new or modified test.\n- SQL/query behavior → look for `.slt` in `test/sqllogictest/`.\n- Wire/protocol behavior → look for `.pt` in `test/pgtest/`.\n- Rust logic/types/APIs → add or extend a Rust unit test in the crate (e.g. `#[cfg(test)]` or `tests/`); run with `cargo test -p mz-<crate>`.\n- Prefer testing observable behavior (SQL results, wire protocol) over implementation details.\n- Red flag: behavior change with no test changes.\n- For more testing guidelines, read `doc/developer/guide-testing.md`\n\n### LIR schema registry\nApplies when the diff changes the serde-visible shape of a type the LIR\nschema reaches: `src/compute-types/src/plan.rs`,\n`src/compute-types/src/plan/**`, a `*Func` payload, or a plan-reachable type\nin `mz-expr` or `mz-repr`.\n- `src/compute-types/tests/snapshots/lir_v{N}.json` is regenerated in the\n  same PR, and the diff matches the intended format change. Red flag: a\n  serde-visible change to a plan type with no snapshot diff.\n- If `LIR_VERSION` in `src/compute-types/src/plan.rs` had shipped, it is\n  bumped and the old `lir_v{N}.json` is left untouched rather than rewritten.\n- `Row` and `EvalError` never appear directly in plan types, only as\n  `StableRow` / `StableEvalError`, enforced by\n  `lir_schema_contains_only_stable_types`.\n\n### Scalar function registry\nApplies when the diff touches `UnaryFunc`, `BinaryFunc`, or `VariadicFunc`\n(`src/expr/src/scalar/func.rs`, `src/expr/src/scalar/func/**`, `#[sqlfunc]`\nbodies).\n- The registry snapshots under `src/compute-types/tests/snapshots/` are\n  regenerated in the same PR. A property change moves `func_registry.json`\n  and the current version's entry in `func_registry_digests.json`, a\n  `#[sqlfunc]` declaration or body change moves `func_registry_source.json`\n  alone, and a change to a hand-written variant's body moves neither.\n- Every new variant with a payload has a `Sample` in\n  `src/expr/src/scalar/func/registry.rs`, with a labeled second sample when a\n  property depends on the payload.\n- A removed function, a changed property, or a `body_fingerprint` change that\n  alters results for some input bumps `LIR_VERSION` in\n  `src/compute-types/src/plan.rs` if that version has shipped, and leaves the\n  old digest in `func_registry_digests.json` untouched. Additions need no\n  bump.\n- The PR description names the added, removed, and changed functions and,\n  for body changes, states whether results change.\n\n### Code style (Rust)\n- **Imports:** `std` → external crates → `crate::`; one `use` per module; prefer `crate::` over `super::` in non-test code.\n- **Errors:** Structured with `thiserror`; no bare `anyhow!(\"...\")`. `Display` should not print full error chain.\n- **Async:** Use `ore::task::spawn` / `spawn_blocking`, not raw `tokio::spawn`.\n- **Tests:** `#[mz_ore::test]`; panic in tests rather than returning `Result`.\n\n### Code style (SQL)\n- Keywords capitalized (`SELECT`, `FROM`); identifiers lowercase.\n- No space between function name and `(`.\n\n### Error messages\n- Primary: short, factual, lowercase first letter, no trailing punctuation.\n- Detail/hint: complete sentences, capitalized, period.\n- No \"unable\", \"bad\", \"illegal\", \"unknown\"; say what kind of object.\n\n### Sensitive data handling\n- Types holding passwords, keys, tokens, or credentials should use `mz_ore::secure::{SecureString, SecureVec}` or `zeroize::Zeroizing<T>` (from `mz_ore::secure`).\n- Sensitive types should **not** derive `Clone` or `Debug` (use custom `Debug` that redacts).\n- Stack-local buffers holding derived keys, nonces, or HMAC outputs should be wrapped in `Zeroizing<T>`.\n- See `doc/developer/generated/ore/secure.md` for full guidance and `src/ssh-util/src/keys.rs` for a reference implementation.\n\n### Architecture\n- **Simplicity:** No incidental complexity; simplify redundant logic.\n- **No special casing:** Prefer composable design over extra booleans/branches.\n- **Encapsulation:** sql-parser = grammar only (no semantic validation); sql = planning + semantics.\n- **Dependencies:** New crates must be justified.\n- For more design guidelines read: `doc/developer/best-practices.md`\n\n### Polish\n- No leftover `// XXX`, `// FIXME`, `dbg!`, `println!`, or commented-out code.\n- No unrelated formatting changes in untouched code.\n- New public items should have doc comments.\n\n### Release notes\n\nRelease notes are auto-generated per release from PR descriptions (the [`/mz-release-notes` skill](https://github.com/MaterializeInc/mz-skills/blob/main/.agents/skills/mz-release-notes/SKILL.md));\nauthors don't write them. So for a user-visible change, check that the PR *description* states the\nuser-observable effect in user-facing terms — not only implementation detail — so the classifier includes it.\n\n## One semantic change rule\n\nThe PR should do one thing. If it spans multiple CODEOWNERS areas (e.g. sql-parser + sql planner), consider suggesting a split.\n\n## Rules\n\n- Review the code, not the author. Explain the *why* behind suggestions.\n- Use **nit:** for preferences where reasonable people could disagree.\n- If the PR improves overall codebase health and blocking items are addressed, say so.\n- Do NOT make any changes — this is read-only review.\n","frontmatter":{"name":"mz-pr-review","description":"Local code review of current branch vs Materialize standards. Trigger: \"review my code\", \"review my changes\", \"check my diff\", \"does this look ok\", \"what do you think of this PR\", \"code review\", or look over changes before merging. Also PR number + wants feedback on quality, style, correctness. For changes under `console/`, use mz-console-review instead.\n","argument-hint":["base-branch"],"allowed-tools":["Bash","Read","Grep","Glob","Task"],"allowedTools":["Bash","Read","Grep","Glob","Task"]},"allowedTools":["Bash","Read","Grep","Glob","Task"],"isInternal":false,"tokens":1645,"sizeBytes":6548},{"name":"SKILL.md","path":".agents/skills/mz-profile/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-profile/SKILL.md","title":"mz-profile","category":"anthropic-skill","format":"markdown","content":"# mz-profile\n\nPath: `.agents/skills/mz-profile/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-profile/SKILL.md)","tokens":300,"sizeBytes":188},{"name":"SKILL.md","path":".agents/skills/mz-query-perf/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-perf/SKILL.md","title":"mz-query-perf","category":"anthropic-skill","format":"markdown","content":"# mz-query-perf\n\nPath: `.agents/skills/mz-query-perf/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-perf/SKILL.md)","tokens":300,"sizeBytes":197},{"name":"SKILL.md","path":".agents/skills/mz-query-tracing/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-tracing/SKILL.md","title":"mz-query-tracing","category":"anthropic-skill","format":"markdown","content":"# mz-query-tracing\n\nPath: `.agents/skills/mz-query-tracing/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-tracing/SKILL.md)","tokens":300,"sizeBytes":206},{"name":"adapter.md","path":".agents/skills/mz-release-signoff/references/adapter.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/adapter.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/adapter.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/adapter.md)","tokens":300,"sizeBytes":236},{"name":"compute.md","path":".agents/skills/mz-release-signoff/references/compute.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/compute.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/compute.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/compute.md)","tokens":300,"sizeBytes":236},{"name":"persist.md","path":".agents/skills/mz-release-signoff/references/persist.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/persist.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/persist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/persist.md)","tokens":300,"sizeBytes":236},{"name":"reference-dashboards.md","path":".agents/skills/mz-release-signoff/references/reference-dashboards.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/reference-dashboards.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/reference-dashboards.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/reference-dashboards.md)","tokens":300,"sizeBytes":262},{"name":"sources-and-sinks.md","path":".agents/skills/mz-release-signoff/references/sources-and-sinks.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/sources-and-sinks.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/sources-and-sinks.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/sources-and-sinks.md)","tokens":300,"sizeBytes":256},{"name":"SKILL.md","path":".agents/skills/mz-release-signoff/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/SKILL.md","title":"mz-release-signoff","category":"anthropic-skill","format":"markdown","content":"# mz-release-signoff\n\nPath: `.agents/skills/mz-release-signoff/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/SKILL.md)","tokens":300,"sizeBytes":212},{"name":"SKILL.md","path":".agents/skills/mz-run/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-run/SKILL.md","title":"mz-run","category":"anthropic-skill","format":"markdown","content":"# mz-run\n\nPath: `.agents/skills/mz-run/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-run/SKILL.md)","tokens":300,"sizeBytes":176},{"name":"SKILL.md","path":".agents/skills/mz-test/SKILL.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-test/SKILL.md","title":"mz-test","category":"anthropic-skill","format":"markdown","content":"# mz-test\n\nPath: `.agents/skills/mz-test/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-test/SKILL.md)","tokens":300,"sizeBytes":179},{"name":"README.md","path":".agents/skills/README.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/README.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"# Skills Skill\n\nPath: `.agents/skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/README.md)","tokens":300,"sizeBytes":170},{"name":"_module.md","path":"doc/developer/generated/compute/extensions/_module.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/_module.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/_module.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/_module.md)","tokens":300,"sizeBytes":232},{"name":"arrange.md","path":"doc/developer/generated/compute/extensions/arrange.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/arrange.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/arrange.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/arrange.md)","tokens":300,"sizeBytes":232},{"name":"reduce.md","path":"doc/developer/generated/compute/extensions/reduce.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/reduce.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/reduce.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/reduce.md)","tokens":300,"sizeBytes":230},{"name":"temporal_bucket.md","path":"doc/developer/generated/compute/extensions/temporal_bucket.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/temporal_bucket.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/temporal_bucket.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/temporal_bucket.md)","tokens":300,"sizeBytes":248},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/CLAUDE.md)","tokens":300,"sizeBytes":167},{"name":"CLAUDE.md","path":"console/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/CLAUDE.md","title":"console - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# console - Claude Agent Guidelines & System Prompt\n\nPath: `console/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/CLAUDE.md)","tokens":300,"sizeBytes":193},{"name":"README.md","path":"ci/plugins/cloudtest/README.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/cloudtest/README.md","title":"cloudtest Documentation","category":"plugin-manifest","format":"markdown","content":"# cloudtest Documentation\n\nPath: `ci/plugins/cloudtest/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/cloudtest/README.md)","tokens":300,"sizeBytes":193},{"name":"README.md","path":"ci/plugins/mzcompose/README.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/mzcompose/README.md","title":"mzcompose Documentation","category":"plugin-manifest","format":"markdown","content":"# mzcompose Documentation\n\nPath: `ci/plugins/mzcompose/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/mzcompose/README.md)","tokens":300,"sizeBytes":193},{"name":"README.md","path":"ci/plugins/scratch-aws-access/README.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/scratch-aws-access/README.md","title":"scratch-aws-access Documentation","category":"plugin-manifest","format":"markdown","content":"# scratch-aws-access Documentation\n\nPath: `ci/plugins/scratch-aws-access/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/ci/plugins/scratch-aws-access/README.md)","tokens":300,"sizeBytes":220},{"name":"update-docs.md","path":".claude/commands/update-docs.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.claude/commands/update-docs.md","title":"Command: /update-docs","category":"command-prompt","format":"markdown","content":"# Command: /update-docs\n\nPath: `.claude/commands/update-docs.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.claude/commands/update-docs.md)","tokens":300,"sizeBytes":193},{"name":"_module.md","path":"doc/developer/generated/mz-deploy/cli/commands/_module.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/_module.md","title":"Command: /module","category":"command-prompt","format":"markdown","content":"# Command: /module\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/_module.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/_module.md)","tokens":300,"sizeBytes":240},{"name":"abort.md","path":"doc/developer/generated/mz-deploy/cli/commands/abort.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/abort.md","title":"Command: /abort","category":"command-prompt","format":"markdown","content":"# Command: /abort\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/abort.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/abort.md)","tokens":300,"sizeBytes":235},{"name":"apply_all.md","path":"doc/developer/generated/mz-deploy/cli/commands/apply_all.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_all.md","title":"Command: /apply_all","category":"command-prompt","format":"markdown","content":"# Command: /apply_all\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_all.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_all.md)","tokens":300,"sizeBytes":247},{"name":"apply_connections.md","path":"doc/developer/generated/mz-deploy/cli/commands/apply_connections.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_connections.md","title":"Command: /apply_connections","category":"command-prompt","format":"markdown","content":"# Command: /apply_connections\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_connections.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_connections.md)","tokens":300,"sizeBytes":271},{"name":"apply_network_policies.md","path":"doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md","title":"Command: /apply_network_policies","category":"command-prompt","format":"markdown","content":"# Command: /apply_network_policies\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_network_policies.md)","tokens":300,"sizeBytes":286},{"name":"apply_objects.md","path":"doc/developer/generated/mz-deploy/cli/commands/apply_objects.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_objects.md","title":"Command: /apply_objects","category":"command-prompt","format":"markdown","content":"# Command: /apply_objects\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_objects.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_objects.md)","tokens":300,"sizeBytes":259},{"name":"apply_secrets.md","path":"doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md","title":"Command: /apply_secrets","category":"command-prompt","format":"markdown","content":"# Command: /apply_secrets\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_secrets.md)","tokens":300,"sizeBytes":259},{"name":"apply_sources.md","path":"doc/developer/generated/mz-deploy/cli/commands/apply_sources.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_sources.md","title":"Command: /apply_sources","category":"command-prompt","format":"markdown","content":"# Command: /apply_sources\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_sources.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_sources.md)","tokens":300,"sizeBytes":259},{"name":"apply_tables.md","path":"doc/developer/generated/mz-deploy/cli/commands/apply_tables.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_tables.md","title":"Command: /apply_tables","category":"command-prompt","format":"markdown","content":"# Command: /apply_tables\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/apply_tables.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/apply_tables.md)","tokens":300,"sizeBytes":256},{"name":"clean.md","path":"doc/developer/generated/mz-deploy/cli/commands/clean.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/clean.md","title":"Command: /clean","category":"command-prompt","format":"markdown","content":"# Command: /clean\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/clean.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/clean.md)","tokens":300,"sizeBytes":235},{"name":"clusters.md","path":"doc/developer/generated/mz-deploy/cli/commands/clusters.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/clusters.md","title":"Command: /clusters","category":"command-prompt","format":"markdown","content":"# Command: /clusters\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/clusters.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/clusters.md)","tokens":300,"sizeBytes":244},{"name":"compile.md","path":"doc/developer/generated/mz-deploy/cli/commands/compile.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/compile.md","title":"Command: /compile","category":"command-prompt","format":"markdown","content":"# Command: /compile\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/compile.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/compile.md)","tokens":300,"sizeBytes":241},{"name":"debug.md","path":"doc/developer/generated/mz-deploy/cli/commands/debug.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/debug.md","title":"Command: /debug","category":"command-prompt","format":"markdown","content":"# Command: /debug\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/debug.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/debug.md)","tokens":300,"sizeBytes":235},{"name":"delete.md","path":"doc/developer/generated/mz-deploy/cli/commands/delete.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/delete.md","title":"Command: /delete","category":"command-prompt","format":"markdown","content":"# Command: /delete\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/delete.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/delete.md)","tokens":300,"sizeBytes":238},{"name":"describe.md","path":"doc/developer/generated/mz-deploy/cli/commands/describe.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/describe.md","title":"Command: /describe","category":"command-prompt","format":"markdown","content":"# Command: /describe\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/describe.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/describe.md)","tokens":300,"sizeBytes":244},{"name":"dev.md","path":"doc/developer/generated/mz-deploy/cli/commands/dev.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/dev.md","title":"Command: /dev","category":"command-prompt","format":"markdown","content":"# Command: /dev\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/dev.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/dev.md)","tokens":300,"sizeBytes":229},{"name":"explain.md","path":"doc/developer/generated/mz-deploy/cli/commands/explain.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/explain.md","title":"Command: /explain","category":"command-prompt","format":"markdown","content":"# Command: /explain\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/explain.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/explain.md)","tokens":300,"sizeBytes":241},{"name":"grants.md","path":"doc/developer/generated/mz-deploy/cli/commands/grants.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/grants.md","title":"Command: /grants","category":"command-prompt","format":"markdown","content":"# Command: /grants\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/grants.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/grants.md)","tokens":300,"sizeBytes":238},{"name":"list.md","path":"doc/developer/generated/mz-deploy/cli/commands/list.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/list.md","title":"Command: /list","category":"command-prompt","format":"markdown","content":"# Command: /list\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/list.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/list.md)","tokens":300,"sizeBytes":232},{"name":"lock.md","path":"doc/developer/generated/mz-deploy/cli/commands/lock.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/lock.md","title":"Command: /lock","category":"command-prompt","format":"markdown","content":"# Command: /lock\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/lock.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/lock.md)","tokens":300,"sizeBytes":232},{"name":"log.md","path":"doc/developer/generated/mz-deploy/cli/commands/log.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/log.md","title":"Command: /log","category":"command-prompt","format":"markdown","content":"# Command: /log\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/log.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/log.md)","tokens":300,"sizeBytes":229},{"name":"mcp.md","path":"doc/developer/generated/mz-deploy/cli/commands/mcp.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/mcp.md","title":"Command: /mcp","category":"command-prompt","format":"markdown","content":"# Command: /mcp\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/mcp.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/mcp.md)","tokens":300,"sizeBytes":229},{"name":"new_project.md","path":"doc/developer/generated/mz-deploy/cli/commands/new_project.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/new_project.md","title":"Command: /new_project","category":"command-prompt","format":"markdown","content":"# Command: /new_project\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/new_project.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/new_project.md)","tokens":300,"sizeBytes":253},{"name":"profile.md","path":"doc/developer/generated/mz-deploy/cli/commands/profile.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/profile.md","title":"Command: /profile","category":"command-prompt","format":"markdown","content":"# Command: /profile\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/profile.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/profile.md)","tokens":300,"sizeBytes":241},{"name":"promote.md","path":"doc/developer/generated/mz-deploy/cli/commands/promote.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/promote.md","title":"Command: /promote","category":"command-prompt","format":"markdown","content":"# Command: /promote\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/promote.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/promote.md)","tokens":300,"sizeBytes":241},{"name":"roles.md","path":"doc/developer/generated/mz-deploy/cli/commands/roles.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/roles.md","title":"Command: /roles","category":"command-prompt","format":"markdown","content":"# Command: /roles\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/roles.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/roles.md)","tokens":300,"sizeBytes":235},{"name":"setup_schema.md","path":"doc/developer/generated/mz-deploy/cli/commands/setup_schema.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/setup_schema.md","title":"Command: /setup_schema","category":"command-prompt","format":"markdown","content":"# Command: /setup_schema\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/setup_schema.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/setup_schema.md)","tokens":300,"sizeBytes":256},{"name":"setup.md","path":"doc/developer/generated/mz-deploy/cli/commands/setup.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/setup.md","title":"Command: /setup","category":"command-prompt","format":"markdown","content":"# Command: /setup\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/setup.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/setup.md)","tokens":300,"sizeBytes":235},{"name":"sql.md","path":"doc/developer/generated/mz-deploy/cli/commands/sql.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/sql.md","title":"Command: /sql","category":"command-prompt","format":"markdown","content":"# Command: /sql\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/sql.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/sql.md)","tokens":300,"sizeBytes":229},{"name":"stage.md","path":"doc/developer/generated/mz-deploy/cli/commands/stage.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/stage.md","title":"Command: /stage","category":"command-prompt","format":"markdown","content":"# Command: /stage\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/stage.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/stage.md)","tokens":300,"sizeBytes":235},{"name":"test.md","path":"doc/developer/generated/mz-deploy/cli/commands/test.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/test.md","title":"Command: /test","category":"command-prompt","format":"markdown","content":"# Command: /test\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/test.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/test.md)","tokens":300,"sizeBytes":232},{"name":"lower.md","path":"doc/developer/generated/mz-deploy/cli/commands/test/lower.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/test/lower.md","title":"Command: /lower","category":"command-prompt","format":"markdown","content":"# Command: /lower\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/test/lower.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/test/lower.md)","tokens":300,"sizeBytes":245},{"name":"wait.md","path":"doc/developer/generated/mz-deploy/cli/commands/wait.md","rawUrl":"https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/wait.md","title":"Command: /wait","category":"command-prompt","format":"markdown","content":"# Command: /wait\n\nPath: `doc/developer/generated/mz-deploy/cli/commands/wait.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/mz-deploy/cli/commands/wait.md)","tokens":300,"sizeBytes":232}],"systemPromptSnippet":"<yakaai_skills repo=\"MaterializeInc/materialize\">\n<!-- File: AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/AGENTS.md)\n\n<!-- File: console/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# console - AI Agent Protocol & Instructions\n\nPath: `console/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/console/AGENTS.md)\n\n<!-- File: .agents/skills/mz-adapter-guide/SKILL.md (Tokens: ~389 | Category: anthropic-skill) -->\n---\nname: mz-adapter-guide\ndescription: >\n  Correctness invariants + architecture: adapter, coordinator, pgwire, peek\n  paths, timestamp oracle. Trigger: questions about these subsystems —\n  \"how does coordinator work\", \"what are read holds\", \"explain peek path\",\n  \"how does timestamp selection work\", \"why does this query block\". Also\n  when editing OR reviewing code in src/adapter/, src/pgwire/,\n  src/timestamp-oracle/ (reviewing a PR, diff, or commit that touches these\n  paths counts).\n---\n\n# Adapter Guide Skill\n\nWhen working on or reviewing the adapter layer or related crates, always read\nand follow the guidance in `doc/developer/guide-adapter.md`. That document\ncontains correctness invariants, architectural notes, a list of optimizations\nthat have been tried and rejected for correctness reasons, and a checklist for\nreviewing adapter changes. Read it before making changes and before reviewing\nthem.\n\nRead `doc/developer/guide-adapter.md` now.\n\nWhen working on the timestamp oracle (`src/timestamp-oracle/`), also read the\nmodule-level docs in `src/timestamp-oracle/src/lib.rs` and\n`src/timestamp-oracle/src/batching_oracle.rs` for the `TimestampOracle` trait\ncontract and the batching/linearizability invariants.\n\nWhen you or the user discover that an approach is incorrect or heading in a\nwrong direction, suggest distilling the learning into\n`doc/developer/guide-adapter.md`. Add it to whichever section fits best — a\nnew correctness invariant, a new subsection under an existing invariant, a\nrejected optimization, or a new top-level section if needed. Describe what was\ntried, why it's wrong, and any relevant context. This keeps the guide up to\ndate and prevents future re-discovery of the same pitfalls.\n\n<!-- File: .agents/skills/mz-benchmark/SKILL.md (Tokens: ~2969 | Category: anthropic-skill) -->\n---\nname: mz-benchmark\ndescription: >\n  Add/modify/debug Materialize perf benchmark scenarios. Three frameworks:\n  Feature Benchmark (single-op micro), Scalability Test (SQL throughput under\n  concurrency), Parallel Benchmark (sustained latency via scenarios.py).\n  Trigger: \"benchmark\", \"feature benchmark\", \"scalability test\",\n  \"parallel benchmark\", \"performance regression\", \"micro-benchmark\", \"TPS\",\n  \"latency test\", or edits in feature_benchmark/scenarios/,\n  scalability/workload/workloads/, parallel_benchmark/scenarios.py.\n  Note: measurement, not panic-stress (see mz-parallel-workload).\n---\n\n# Benchmark Frameworks\n\nMaterialize has some benchmark frameworks targetting local Docker, each suited to different performance concerns. Choose the right one based on what you're measuring.\n\n## Decision Guide\n\n| Concern | Framework | Best For |\n|---------|-----------|----------|\n| Single-operation latency | **Feature Benchmark** | \"How fast is this SELECT / INSERT / CREATE INDEX?\" |\n| SQL throughput under concurrency | **Scalability Test** | \"How many QPS can we sustain at 1/4/16/64/256 concurrent clients?\" |\n| Sustained performance over time | **Parallel Benchmark** | \"Does latency degrade over a 2-minute mixed workload?\" |\n\nA Rust `cargo bench` target must reference the production allocator with a\n`mz-alloc-default` dev-dependency and `use mz_alloc_default as _;` (see\n`src/repr/benches/row.rs`). Without it the bench runs on the system allocator,\nnot jemalloc.\n\nScalability Test and Parallel Benchmark connect with psycopg 3 and leave\n`prepare_threshold` at its default of 5, so a query switches to a prepared\nstatement on its sixth execution per connection. Set it explicitly and warm\nconnections up when per-query latency matters.\n\n---\n\n## 1. Feature Benchmark (Micro-benchmarks)\n\nMeasures wall-clock time of individual SQL operations without concurrency. Uses testdrive fragments. Great for regression-testing specific code paths.\n\n### How to Add a Scenario\n\n1. Add a class in one of the `scenarios/*.py` files. Extend `Scenario` (default, runs in CI), `ScenarioBig` (longer, opt-in), or `ScenarioDisabled`.\n2. Implement `benchmark()` returning a `Td(...)` with timing markers (`/* A */` and `/* B */`).\n3. Optionally implement `shared()`, `init()`, `before()` if setup is required.\n\n### Scenario Template\n\n```python\nfrom materialize.feature_benchmark.action import TdAction\nfrom materialize.feature_benchmark.measurement_source import MeasurementSource, Td\nfrom materialize.feature_benchmark.scenario import Scenario\n\nclass MyCategory(Scenario):\n    \"\"\"Group parent - won't run (not a leaf).\"\"\"\n    pass\n\nclass MySpecificBenchmark(MyCategory):\n    \"\"\"Leaf class - this runs.\"\"\"\n\n    def init(self) -> list[TdAction]:\n        return [\n            self.table_ten(),  # Creates table 'ten' with values 0-9\n            TdAction(\"\"\"\n                > CREATE TABLE t1 (f1 INTEGER);\n                > INSERT INTO t1 SELECT {unique_values} FROM {join};\n                > SELECT COUNT(*) FROM t1;\n                {n}\n            \"\"\".format(\n                unique_values=self.unique_values(),\n                join=self.join(),\n                n=self.n(),\n            )),\n        ]\n\n    def benchmark(self) -> MeasurementSource:\n        return Td(\"\"\"\n> SELECT 1;\n  /* A */\n1\n\n> SELECT COUNT(*) FROM t1 WHERE f1 % 2 = 0;\n  /* B */\n{half_n}\n\"\"\".format(half_n=self.n() // 2))\n```\n\n### Timing Markers\n\n- `/* A */` - Start marker. Measurement begins AFTER this completes.\n- `/* B */` - End marker. Measurement ends when this completes.\n- Measures: time from end of A to end of B.\n- For a single query, use `SELECT 1; /* A */` as a dummy start to not measure the overhead of Testdrive starting up.\n\n### Scenario Phases (execution order)\n\n1. **`shared()`** - Runs once for BOTH instances (e.g., create Kafka topics)\n2. **`init()`** - Runs once per instance (create tables, populate data)\n3. **`before()`** - Runs before EACH iteration (restart, cleanup)\n4. **`benchmark()`** - Runs repeatedly, measured\n\nThe result is the minimum (`MinAggregation`) after discarding the first 3\nmeasurements. `ProbForMin(threshold=0.90)` in\n`test/feature-benchmark/mzcompose.py` can end a scenario after 6 measurements,\nso the verdict can rest on 3 values and one outlier can flip it. Read the raw\nper-iteration values before trusting a regression or improvement.\n\nThe measured cluster `cluster_default` is an unmanaged replica on the\n`clusterd` container, configured by the `Clusterd(...)` service in\n`create_clusterd_service`. That function maps `enable_unified_cluster` onto\nthe service, but other parameters that shape the replica process do not reach\nit. For example, `enable_timely_zero_copy` has no effect because `Clusterd`\nhardcodes its timely config.\n\n### Helpers\n\n- `self.n()` - Returns 10^SCALE (default SCALE=6 → 1,000,000)\n- `self.table_ten()` / `self.view_ten()` - Creates table/view with values 0-9\n- `self.unique_values()` - SQL expression generating N unique values via cartesian product\n- `self.join()` - SQL FROM clause for cartesian product joins\n- `self.keyschema()` / `self.schema()` - Kafka schema helpers\n\n### Tips\n\n- **Aim for ~1 second** query time. Too fast = noise; too slow = Unbearably slow CI.\n- **Test td fragments locally** with standalone testdrive before running the full benchmark.\n- **Use `SCALE`** to control data volume: `SCALE = 5` → 100K rows.\n- **Use `FIXED_SCALE = True`** if `--scale` should not affect your scenario. This is usually done when a scenario doesn't scale well, add an explanation.\n- **Use `RELATIVE_THRESHOLD`** to customize regression sensitivity per scenario.\n\n### Running\n\n```bash\nbin/mzcompose --find feature-benchmark run default --root-scenario=MyCategory\nbin/mzcompose --find feature-benchmark run default --root-scenario=MySpecificBenchmark\n```\n\n---\n\n## 2. Scalability Test (SQL Throughput Under Concurrency)\n\nMeasures TPS (transactions per second) at increasing concurrency levels (1, 2, 4, 8, ..., 256 clients). Compares Materialize against Postgres.\n\nThe load is closed-loop: `concurrency` threads each issue the next operation\nwhen the previous one completes (`benchmark_executor.py`). TPS is therefore\ncapped at concurrency divided by mean latency, and a flattening curve can be\nthe client cap rather than a server knee. Use Parallel Benchmark's `OpenLoop`\nto measure behavior at a fixed offered load, and report p99 and max with it.\n\n### How to Add a Workload\n\n1. Create a new file or add to an existing file in `workload/workloads/`.\n2. Extend a marker class (`DmlDqlWorkload`, `DdlWorkload`, or `ConnectionWorkload`).\n3. Implement `operations()` returning a list of `Operation` objects.\n\n### Workload Template (Simple DML/DQL)\n\n```python\nfrom materialize.scalability.operation.operations.operations import (\n    SelectOne,\n    InsertDefaultValues,\n    SelectCountInMv,\n)\nfrom materialize.scalability.workload.workload_markers import DmlDqlWorkload\n\nclass MyWorkload(DmlDqlWorkload):\n    def operations(self) -> list[\"Operation\"]:\n        return [InsertDefaultValues(), SelectCountInMv()]\n```\n\n### Workload Template (DDL with Data Exchange)\n\n```python\nfrom materialize.scalability.operation.operation_data import OperationData\nfrom materialize.scalability.operation.operations.operations import (\n    CreateTableX, PopulateTableX, SelectStarFromTableX, DropTableX,\n)\nfrom materialize.scalability.operation.scalability_operation import OperationChainWithDataExchange\nfrom materialize.scalability.workload.workload import WorkloadWithContext\nfrom materialize.scalability.workload.workload_markers import DdlWorkload\n\nclass MyDdlWorkload(WorkloadWithContext, DdlWorkload):\n    def amend_data_before_execution(self, data: OperationData) -> None:\n        data.push(\"table_seed\", data.get(\"worker_id\"))\n\n    def operations(self) -> list[\"Operation\"]:\n        return [\n            OperationChainWithDataExchange([\n                CreateTableX(), PopulateTableX(), SelectStarFromTableX(), DropTableX(),\n            ])\n        ]\n```\n\n### Custom Operations\n\n```python\nfrom materialize.scalability.operation.scalability_operation import SimpleSqlOperation\n\nclass MyCustomSelect(SimpleSqlOperation):\n    def sql_statement(self) -> str:\n        return \"SELECT COUNT(*) FROM t1 WHERE f1 > 500;\"\n```\n\n### Built-in Operations\n\n`SelectOne`, `SelectStar`, `SelectLimit`, `SelectCount`, `SelectCountInMv`, `SelectUnionAll`, `Update`, `InsertDefaultValues`, `CreateTableX`, `PopulateTableX`, `DropTableX`, `CreateMvOnTableX`, `DropMvOfTableX`, `Connect`, `Disconnect`.\n\n### Database Schema (auto-created)\n\nFor each workload, the framework creates: `CREATE TABLE t1 (f1 INTEGER DEFAULT 1)`, inserts a row, and creates `CREATE MATERIALIZED VIEW mv1 AS SELECT count(*) AS count FROM t1`.\n\n### Running\n\n```bash\nbin/mzcompose --find scalability run default --workload MyWorkload\nbin/mzcompose --find scalability run default --workload-group-marker DmlDqlWorkload\nbin/mzcompose --find scalability run default --min-concurrency 1 --max-concurrency 128\n```\n\n---\n\n## 3. Parallel Benchmark (Sustained Performance)\n\nMeasures latency distributions (p50/p95/p99) and QPS over a sustained load period (default 120s). Supports mixed workloads with both open-loop and closed-loop concurrency. Detects latency degradation over time via slope analysis.\n\n### How to Add a Scenario\n\n1. Add a class in `scenarios.py` extending `Scenario`.\n2. In `__init__`, call `self.init(phases=[...])` with `TdPhase` (setup) and `LoadPhase` (benchmark).\n\n### Scenario Template\n\n```python\nfrom materialize.parallel_benchmark.framework import (\n    ClosedLoop,\n    LoadPhase,\n    OpenLoop,\n    Periodic,\n    ReuseConnQuery,\n    Scenario,\n    StandaloneQuery,\n    TdPhase,\n)\n\nclass MyScenario(Scenario):\n    def __init__(self, c, conn_infos):\n        self.init(\n            phases=[\n                # Setup phase (testdrive)\n                TdPhase(\"\"\"\n                    > CREATE TABLE bench_t (f1 INTEGER);\n                    > INSERT INTO bench_t SELECT generate_series(1, 10000);\n                    > CREATE MATERIALIZED VIEW bench_mv AS\n                      SELECT COUNT(*) AS c FROM bench_t;\n                    > SELECT c FROM bench_mv;\n                    10000\n                \"\"\"),\n                # Benchmark phase\n                LoadPhase(\n                    duration=120,  # seconds\n                    actions=[\n                        OpenLoop(\n                            action=StandaloneQuery(\n                                \"SELECT c FROM bench_mv\",\n                                conn_infos[\"materialized\"],\n                            ),\n                            dist=Periodic(per_second=100),\n                            report_regressions=True,\n                        ),\n                        ClosedLoop(\n                            action=ReuseConnQuery(\n                                \"INSERT INTO bench_t VALUES (1)\",\n                                conn_infos[\"materialized\"],\n                            ),\n                            report_regressions=True,\n                        ),\n                    ],\n                ),\n            ],\n            guarantees={\n                \"SELECT c FROM bench_mv (standalone)\": {\n                    \"qps\": 10,\n                    \"p99\": 500,\n                },\n            },\n        )\n```\n\n### Action Types\n\n| Class | Behavior | Use When |\n|-------|----------|----------|\n| `StandaloneQuery` | New connection per operation | Testing connection overhead + query |\n| `ReuseConnQuery` | Persistent single connection | Testing pure query latency |\n| `PooledQuery` | Draws from connection pool | Testing pooled throughput (set `conn_pool_size > 0`) |\n\n### Load Patterns\n\n| Class | Behavior |\n|-------|----------|\n| `OpenLoop(dist=Periodic(per_second=N))` | Fixed-rate: N operations/second regardless of latency |\n| `OpenLoop(dist=Gaussian(mean, stddev))` | Variable-rate: sleep drawn from gaussian distribution |\n| `ClosedLoop(...)` | Sequential: next operation starts when previous completes |\n\n### Guarantees and Regression Thresholds\n\n```python\n# Absolute guarantees (fail if not met)\nguarantees={\n    \"SELECT 1 (standalone)\": {\"qps\": 15, \"p99\": 400, \"max\": 500},\n}\n\n# Relative thresholds (fail if regression vs baseline)\nregression_thresholds={\n    \"SELECT 1 (pooled)\": {\"qps\": 0.8, \"p99\": 1.3},  # QPS must be >=80% of baseline\n}\n```\n\nAction names are auto-generated as `\"{query} ({connection_type})\"`.\n\n### Available Metrics\n\n`queries`, `qps`, `min`, `max`, `avg`, `p50`, `p95`, `p99`, `p99.9`, ..., `p99.999999`, `std`, `slope`.\n\n### Disabling a Scenario\n\n```python\nfrom materialize.parallel_benchmark.framework import disabled\n\n@disabled(\"Reason for disabling\")\nclass MyScenario(Scenario):\n    ...\n```\n\n### Running\n\n```bash\nbin/mzcompose --find parallel-benchmark run default --scenario MyScenario\nbin/mzcompose --find parallel-benchmark run default --scenario MyScenario --load-phase-duration 60\n```\n\n<!-- File: .agents/skills/mz-commit/SKILL.md (Tokens: ~2462 | Category: anthropic-skill) -->\n---\nname: mz-commit\ndescription: >\n  Trigger: \"commit\", \"prepare commit\", \"create PR\", \"push\", \"open pull request\",\n  or mentions committing, pre-commit checks, pull requests in Materialize. Also\n  \"ship it\", \"ready to merge\". For code review use mz-pr-review.\n---\n\n# Committing in Materialize\n\nRead `doc/developer/guide-changes.md` for the full conventions on submitting and reviewing changes.\n\n## Pre-commit checklist\n\nBefore committing, run these and fix any warnings:\n\n1. `bin/fmt` (formats all `.rs`, `.py`, and `.proto` files; takes no file arguments)\n2. `bin/lint` (can error if tools are missing; use `bin/ci-builder run stable bin/lint` as an alternative)\n3. `cargo clippy --all-targets -- -D warnings`\n\n`bin/lint` and clippy do not run rustdoc. When the diff adds imports or edits\ndoc comments, run the Doctests check on the changed crates (command in the\nmz-debug-ci skill). When a `metric!` definition changed, `bin/lint`\nregenerates `doc/user/data/metrics.yml`. Commit the result, or the \"Lint and\nrustfmt\" job fails on the diff.\n\nAfter rebasing onto upstream/main, run `cargo check --workspace --all-targets`\neven when the rebase had no conflicts. Upstream changes can still break the\nbranch, for example a dependency your branch needs that upstream removed as\nunused (Nightly's `bin/unused-deps` reports those), a duplicate item, or a new\nenum variant your match does not cover. CI checks this in the\n\"Merge skew cargo check\" job.\n\nDo not manually update `*.snap` files.\nUse `cargo test` followed by `cargo insta accept` to update snapshot files.\nRewrite datadriven test expectations with `REWRITE=1 cargo test ...`.\n\n### LIR plan changes: regenerate the schema registry\n\nThe stable LIR format lets optimized plans be stored durably and reused, so\ntwo snapshot tests in `mz-compute-types` pin what a stored plan looks like\nand what it means. Both key off `LIR_VERSION` in\n`src/compute-types/src/plan.rs`.\n\n`lir_schema` traces the serde schema of everything reachable from\n`LirRelationExpr` into `tests/snapshots/lir_v{LIR_VERSION}.json`. Run it\nwhenever the diff changes the serde-visible shape of a type the trace\nreaches: anything in `src/compute-types/src/plan.rs` or\n`src/compute-types/src/plan/`, a `*Func` payload, or a plan-reachable type\nin `mz-expr` or `mz-repr` such as `AggregateFunc`, `TableFunc`,\n`SqlScalarType` or `DateTimeUnits`:\n\n```\ncargo test -p mz-compute-types --test lir_schema\n```\n\nRegenerate with `REWRITE=1 cargo test -p mz-compute-types --test lir_schema`\nand review the diff. It is the reviewable record of the format change. If\nthe current `LIR_VERSION` has shipped (see below), bump it first so the old\n`lir_v{N}.json` stays in place for migration tooling and the rewrite targets\na fresh file.\n\n### Scalar function changes: regenerate the function registry\n\nThe schema registry sees a plan's shape, not what its functions compute.\n`func_registry` covers that gap for `UnaryFunc`, `BinaryFunc`, and\n`VariadicFunc`. Run it whenever the diff touches\n`src/expr/src/scalar/func.rs` or `src/expr/src/scalar/func/` (including\n`impls/` and `#[sqlfunc]` bodies):\n\n```\ncargo test -p mz-compute-types --test func_registry\n```\n\n* It fails when a function is added, removed, or has a changed property\n  (nullability, error behavior, monotonicity, output type, sqlfunc\n  signature) or a changed `#[sqlfunc]` body. If the failure names removed or\n  changed functions, or a body change that alters results, settle the\n  `LIR_VERSION` question below **before** regenerating. A rewrite at the old\n  version overwrites that version's entry in `func_registry_digests.json`\n  instead of adding a new one, no later check catches it, and that entry is\n  the only key back into git history for what the shipped version's\n  functions meant. Once the version is settled, regenerate the checked-in\n  snapshots and review the diff under `src/compute-types/tests/snapshots/`:\n\n  ```\n  REWRITE=1 cargo test -p mz-compute-types --test func_registry\n  ```\n\n  Never hand-edit `func_registry*.json`. The test rebuilds the registry and\n  compares it against all three files, so an edit fails it.\n* **New function with a payload** (any variant that carries data, e.g. a\n  target type, a regex, a scale): the registry build panics naming the\n  variant. Add a representative `Sample` to `unary_samples()`,\n  `binary_samples()`, or `variadic_samples()` in\n  `src/expr/src/scalar/func/registry.rs`. If a property depends on the\n  payload (e.g. a cast only errors when it has a scale), add a second sample\n  with `.labeled(\"...\")` covering the other branch. Payload-free variants and\n  plain `#[sqlfunc]` functions need no sample.\n* **Removed or changed function, or a body change that alters results**: if\n  the current `LIR_VERSION` has shipped in a release, bump it so stored plans\n  are replanned, and only then regenerate. An unshipped version is\n  regenerated in place. Treat the version as shipped when the latest release tag carries the\n  same `LIR_VERSION`:\n\n  ```\n  git fetch --tags\n  git show $(git tag --list 'v*' --sort=-v:refname | grep -v -- '-rc' | head -1):src/compute-types/src/plan.rs | grep 'pub const LIR_VERSION'\n  ```\n\n  A bump is a judgment call, so confirm it with the user before making it.\n  Additions never need a bump. When unsure whether a body change alters\n  results, say so in the PR description rather than deciding silently.\n\nCommit the regenerated snapshots together with the function change, and call\nout the registry diff (added, removed, changed functions) in the PR\ndescription.\n\n## PR titles and commit messages\n\nMaterialize uses squash merging, so the PR title becomes the commit subject on `main`.\n\n* Use imperative mood: \"Fix X\" not \"Fixed X\" or \"Fixes X\".\n* Be specific: \"Fix panic in catalog sync when controller restarts\" not \"Fix bug\".\n* Prefix with area if helpful: `adapter: `, `storage: `, `compute: `, `sql: `.\n\nWrite a thorough PR description explaining the rationale for the change.\nMention which tests were added or modified in the pull request description, but do not list which tests were run.\nAdd release notes for user-visible changes (should complete \"This release will...\").\n\n## Issue tracking: Linear only for new issues\n\nNew issues are filed in Linear, never in the `database-issues` GitHub repo. `database-issues` is legacy. Its open issues are still valid to read, link, and close, so an existing `database-issues#NNNN` reference in code or a comment stays as it is.\n\nReference the Linear issue by its its key, e.g. `Closes: SQL-450`. Don't include the full URL containing the issue title.\n\nWhen a change closes a legacy GitHub issue, `Fixes database-issues#NNNN` works for auto-closing.\n\n## Cargo.lock discipline\n\nNever regenerate the entire Cargo.lock — bare `cargo update` bumps every semver-compatible dep and introduces unrelated breakage (e.g., `os_info` pulling in `objc2` on macOS, `chrono-tz` changing timezone data, `serde_path_to_error` changing error formats).\n\n* **Adding a dep or changing features**: just `cargo check`. It updates only what's needed.\n* **Updating one crate**: `cargo update -p <crate>` (add `--precise <ver>` to pin).\n* **After any Cargo.lock change**, review the diff:\n  ```\n  git diff Cargo.lock | grep '^[+-]version' | head -40\n  ```\n  Pin back anything that moved unexpectedly: `cargo update -p <crate> --precise <old-version>`.\n* **After rebase conflicts in Cargo.lock**: resolve by taking HEAD's version then running `cargo check` (not `cargo update`). This preserves existing pins while adding only what the new commits require.\n\n## Git conventions\n\n* Work against the `main` branch of `MaterializeInc/materialize`.\n* Push branches to your fork.\n* Pull requests target `main` on `MaterializeInc/materialize`.\n* Each PR should contain one semantic change.\n* Put non-trivial user docs for a new feature in a separate PR. See \"Adding documentation for new features\" in `doc/user/README.md`.\n\n## Splitting large branches\n\nThe repo is squash-merge-only: every PR lands as exactly one commit on `main`, however many commits it had internally. Keep each commit/PR under ~500 changed lines, split by concern, not by the chronology of how the code was written.\n\n* Prefer GitHub's native stacked-PR support to land a chain of dependent PRs, where it's set up for the branch. Where it isn't, fall back to sequential landing, one PR at a time: cut a PR from the front, merge, `git fetch upstream && git rebase upstream/main` the remaining tail, repeat. Squash-only means merging an earlier PR always replaces its commits with one new commit, so any later branch still built on the old commits needs a rebase either way; sequential landing keeps that to one clean rebase per merge instead of compounding across an unmanaged chain.\n* Land foundational or low-conflict commits (design docs, scaffolding, new deps) first. The longer a tail branch lives, the more likely a concurrent unrelated PR touches the same files and turns a clean rebase into a real conflict.\n* To split one large diff into smaller commits, or squash a long messy history into fewer reviewable units, as an alternative to interactive rebase: `git reset --soft <base>` stages the full diff, then stage per group with `git add <files>` for a file-level split, or use `git checkout <sha> -- .` to reproduce an exact historical checkpoint's tree for a chronological squash. (Claude Code's Bash tool has no interactive terminal, so agents need this non-interactive form; use whichever you prefer by hand.) `git checkout <sha> -- .` only adds or updates paths present in `<sha>`, it never removes files that shouldn't exist yet at that checkpoint: compute the removal set first with `comm -23 <(git ls-files|sort) <(git ls-tree -r --name-only <sha>|sort)` and `git rm -f` those, or the checkpoint commit silently carries files from later history. Verify every checkpoint with `git diff HEAD <sha>` (must be empty) before moving to the next. Reattach already-clean commits on top with `git rebase --onto <new-branch> <old-base>` (also non-interactive).\n\n<!-- File: .agents/skills/mz-console-review/cost-and-structure.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Mz-console-review Skill\n\nPath: `.agents/skills/mz-console-review/cost-and-structure.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-console-review/cost-and-structure.md)\n\n<!-- File: .agents/skills/mz-console-review/SKILL.md (Tokens: ~7358 | Category: anthropic-skill) -->\n---\nname: mz-console-review\ndescription: >\n  Review a Console (`console/`) PR or diff the way Console reviewers do: run the\n  change to verify claims, check it against the patterns and helpers the app\n  already has, and post findings as one GitHub review. Trigger: \"review this\n  console PR\", \"review my console changes\", \"does this console change look ok\",\n  a github.com/MaterializeInc/materialize/pull/<N> link whose files are under\n  `console/`, or a request to look over a React/TypeScript diff in this repo.\n  For Rust changes use mz-pr-review instead.\nargument-hint: [pr-number]\nallowed-tools: [Bash, Read, Grep, Glob]\n---\n\n# Console code review\n\n`mz-pr-review` covers the Rust checklists, none of which apply under `console/`.\nThe canonical Console rules live in `console/CLAUDE.md` (component conventions,\nimport restrictions, TypeScript and React patterns, testing philosophy) and in\n`console/doc/` (`guide-testing.md`, `guide-tanstack-table.md`,\n`mz-backwards-compatibility.md`). Read the sections the diff touches. Do not\nrestate them in comments, cite them.\n\nThis skill is what Console reviewers actually flag. Section 4 is distilled from\nroughly 570 reviewer comments on about 150 Console PRs from 2025 and 2026, and is\nordered by how often each kind of feedback came up. Sections 5 and 6 are not from\nthat tally. They cover cost and structure that reviewers rarely catch by\nreading, measured on `mz_catalog_server` and in render-count probes.\n\n## 1. Get the diff, and make it runnable\n\nRemotes are usually `upstream` = `MaterializeInc/materialize` and `origin` = a\npersonal fork, so a PR branch will not resolve against `origin`:\n\n```bash\ngit fetch upstream pull/<N>/head:pr-<N>-review\n```\n\nTo run anything you need the files inside `console/`, because `node_modules`\nlives at `console/node_modules` and a fresh worktree does not have it. Check the\nfiles into a tree with no local edits under `<path>`, run, then clean up. The\nrestore removes files the PR added and reverts the ones it changed, and it also\ndiscards any local edits under `<path>`:\n\n```bash\ngit checkout pr-<N>-review -- console/src/<path>\n(cd console && npx vitest run src/<path>)\ngit restore --source=HEAD --staged --worktree -- console/src/<path>\ngit branch -D pr-<N>-review\n```\n\nConfirm `git status --short` is empty before you finish.\n\n## 2. Verify every behavioral claim before writing it\n\nRun the suite and state the result (\"30/30 pass\"). Probe anything you intend to\nassert. A claim about draw order, nullability, or event handling that you reasoned\nout but did not execute is a claim you will get wrong.\n\nVitest hides `console.log` in passing tests. Assert against a sentinel to see a\nvalue: `expect(actual).toEqual(\"MARKER\")`.\n\njsdom makes graph and pointer assertions silently vacuous:\n\n| Symptom | Cause | Fix in the probe |\n|---|---|---|\n| Geometric assertions pass trivially | `ParentSize` measures nothing, width is 0 | `vi.mock(\"@visx/responsive/lib/components/ParentSize\", ...)` with a fixed size |\n| Pointer handler commits `NaN` | no `createSVGPoint` / `getScreenCTM`, so visx `localPoint` fails | `vi.mock(\"@visx/event\", () => ({ localPoint: vi.fn(() => ({ x, y })) }))` |\n\nMeasure colors instead of judging them: `d3.lab(color).l` gives perceived\nlightness. `d3` is already a dependency.\n\nFor SQL claims, compile the builder and read the SQL. Kysely's `.select` appends,\nit does not replace, so a \"fallback\" select can emit a duplicate column.\n\n## 3. Read the PR before the code\n\nReviewers raise these before any line comment:\n\n* **Motivation is traceable.** A linked issue (a Linear key such as `Closes: CNS-192`, or `Fixes database-issues#N` for a legacy GitHub issue), a Slack thread, or a design doc. If a reviewer cannot tell why the change exists, ask for context before reviewing.\n* **UI changes link the Figma design** in the description.\n* **Stacked PRs are linked** in each description, or the work is one PR with logical commits. Review semantic commits one at a time when the author asks.\n* **Commit messages are imperative and descriptive** per `doc/developer/guide-changes.md`, not \"addressed feedback\".\n* **A removed compatibility shim states its floor.** If the PR deletes a version map or gate, the description names the minimum supported environment version.\n* **WIP code is marked** with `TODO` (spelled `TODO`, not `TO-DO`), including leftover `console.log`.\n* **New pages and routes sit behind a LaunchDarkly flag.** The point is developer experience: it lets the work merge incrementally. Self-managed flags default to `true` in `src/config/flexibleDeploymentFlags.ts`, so a flag meant to stay off there must be set to `false` explicitly.\n\n## 4. What reviewers flag, most frequent first\n\n### 4.1 Remove and simplify\n\nThe single most common request. Look for:\n\n* Unused variables, atoms, exports, props, and query keys. Variables that are always `true`.\n* Redundant fallbacks (`?? 0` behind a value that cannot be null) and conditionals that render nothing.\n* Wrapper hooks that re-expose library options as custom `enable*` props. Keep wrappers thin, since the library's own options are better documented.\n* Features with no requirement behind them (column pinning, LRU layers). Ask \"is there a requirement for this?\"\n* A mini rules engine where two `if` statements would do.\n* Extra state flags (`destroyed`, `initialized`) that every method must check, and piles of refs. Stale refs are a bug source.\n* A copied error state when the mutation hook already exposes `error`.\n* `!!x` inside an expression that is already boolean, and several early returns that can merge into one.\n\n### 4.2 SQL and catalog correctness\n\n* **Nullable columns.** `disk_bytes` and friends are nullable. Coalesce, and fix the cast. When dividing, check the *denominator* for zero, not each operand.\n* **`ORDER BY ... DESC` puts nulls first.** Add `nulls last` where the column is nullable.\n* **User objects are `id LIKE 'u%'`**, via `isSystemId` in `~/api/materialize`. Not `name NOT LIKE 'mz_%'`, and not a bare `startsWith(\"u\")`.\n* **Statistics tables keep rows for dropped replicas** for up to a day. Aggregates like `bool_and(snapshot_committed)` must filter to extant replicas.\n* **Sources:** filter to top-level sources the way `sourceList.ts` does. Subsources and progress collections re-enter through joins on `mz_hydration_statuses`. Newer sources expose subsources as tables.\n* **Latest vs max.** Is the query returning the most recent sample or the largest one, and is that what the UI claims?\n* **A windowed flag must not drive a point-in-time answer.** \"Unreadable at some point in the window\" is not \"unreadable now\". Check which columns and predicates a window-wide flag reaches.\n* **Multi-process skew.** Summing usage over processes hides a single process about to OOM. Prefer `MAX`, and prefer existing catalog views over recomputing their math client-side.\n* **Two surfaces, two answers.** When a PR touches one of several queries for the same metric (list vs detail page), check the staleness bounds and column semantics match. A column can mean different things in the old and new relation.\n* Factor a repeated join or subquery into `src/api/materialize/expressionBuilders.ts` and left-join it, rather than `GROUP BY` just to pull one column.\n* Aggregate with `jsonb_agg` / `list_agg` and format on the client, instead of `STRING_AGG` or `pg_size_pretty` in SQL.\n* Raw SQL goes through the `sql` tagged template. Qualified names are structured (`{ databaseName, schemaName, name }` quoted with `sql.id()` or `quoteIdentifier`), never a dotted string, which breaks on names containing periods.\n* Comment catalog behavior the docs do not cover (for example `object_sub_id IS NULL` meaning the relation itself), and rewrite clever predicates for intent (`BOOL_AND(x IS NOT NULL)` over count arithmetic).\n\n### 4.3 Reuse before re-rolling\n\nBefore accepting any new helper, grep for what it replaces. Reviewers name these most:\n\n| Re-rolled | Already exists |\n|---|---|\n| clamp, pluralize, capitalize, null-guard | `clamp`, `pluralize`, `capitalizeSentence`, `notNullOrUndefined` in `~/util` |\n| Percent and byte text | `formatPercentage(fraction, decimals)` (takes a fraction. Call it directly and handle null inline, since a local `formatPercent` wrapper reads as a re-roll), `formatBytesShort`, `humanizeBytes`, `formatMemoryUsage` in `~/utils/format` |\n| Sorting | d3 `sort(iterable, ...accessors)` (multi-key ties, returns a copy), `handleSortOrder` in `~/utils/sort`, `sortingFunctions` (`nullsLast`, `numericNullsLast`) in `components/Table/tableColumnBuilders.ts`. The same comparator written twice becomes one shared helper |\n| `60_000`-style time constants | `millisecondsInMinute`, `millisecondsInHour` from `date-fns/constants` (not the `date-fns` root in v4) |\n| Object type text | `kebabToTitleCase`, `kebabToSentenceCase`, `kebabToScreamingSpaceCase` (SQL keywords such as `MATERIALIZED VIEW`) in `~/util`, `formatObjectType` in `platform/clusters/format.ts` |\n| Route strings built by hand | path helpers in `~/platform/routeHelpers` (`regionPath`, `useBuildWorkflowGraphPath`, ...). Add one there when it is missing |\n| Logic copied from a neighboring page | extract one helper and switch both call sites to it, so the copies cannot drift |\n| System-object check | `isSystemId` in `~/api/materialize` |\n| Duration formatting, lag caps | `formatInterval`, `formatDurationForAxis` in `~/utils/format` (fractional units are reserved for seconds). `formatDurationForAxis` is for axes: it rounds to whole minutes and caps at 24h, so 90s reads \"2m\" in text. The 24h cap `MAX_LAG_MS` is module-private there and `FreshnessGraph.tsx` declares its own copy, so export one instead of adding a third |\n| An anchor with custom hover/underline | `TextLink` from `~/components/TextLink` |\n| Status colors copied from a pill | export `colorSchemeToStyles` from `StatusPill.tsx` |\n| Layout props threaded one by one | the `containerProps` pattern, see `Gauge.tsx` |\n| A new cluster-sizes query | `useAvailableClusterSizes` / `fetchAvailableClusterSizes` |\n| A new \"all objects\" fetch | `useAllObjects` and the global subscribes (`useGlobalUpsertSubscribe`) |\n| A new filter dropdown | mirror `SchemaFilter.tsx` / `DatabaseFilter.tsx` (reserved \"all\" id, caller sets width) |\n| A new DAG layout | `dagreGraphHelpers.ts` and the workflow graph components |\n| A `useEffect` + timeout debounce | `lodash.debounce` (see `ObjectExplorer.tsx`) |\n| Previous-value or latest-value refs, or a callback read inside a debounce or timer | `usePrevious`, `useLatestRef` from `@chakra-ui/react` (see `useAutomaticallyConnectSocket.ts`) |\n| Tab active state from `location.pathname` | `NavLink`'s `isActive`, see `PageTabStrip` |\n| Set diffing by hand | `Set.prototype.difference` |\n| Secret masking | export `formatAppPassword` from `~/queries/frontegg` |\n| A per-tenant localStorage key | export `buildCacheKey` from `platform/shell/commandCache.ts` |\n| Chart axes, grid, tooltip, legend, cursor | `buildXYGraphLayoutProps` and friends in `~/utils/graph`, `~/components/graphComponents`, `useChartLegend`, `useCursorState`, `colors.lineGraph` |\n| visx plumbing | `Line` from `@visx/shape` for straight lines (as `GraphLineCursor` does), `Group` from `@visx/group` instead of repeating offset arithmetic on each child, `ParentSizeProvidedProps` to type a `ParentSize` child's props, `useDrag` from `~/hooks/useDrag` for draggable handles |\n| Several small components for one feature | one `xComponents.tsx` file, like `formComponents.tsx` |\n\nWhen a new component duplicates an existing one, say which pages it lands on and\nwhat those pages have today.\n\n### 4.4 Tests\n\nValue over volume. Look for:\n\n* **Tests that assert on query-builder string output, SQL snapshots, CSS attributes, or class names.** Replace with SQL tests (`*.test.sql.ts`) that create real objects and assert results, or with user-visible assertions.\n* **Missing corner cases.** Ask for the harder fixture: a deeper role hierarchy, overlapping buckets, multiple inherited roles, users that can log in. For graph fixtures, an ASCII diagram in a comment helps.\n* **Assert the outcome, not the call.** After a `GRANT`, query `mz_internal.mz_show_all_privileges`.\n* **Fixtures that do not look like real data.** Mock only the relation under test, and make values realistic.\n* **`vi.mock` / `vi.spyOn` on app modules.** Reviewers dislike them (unstable, break on circular imports). Prefer MSW via `buildSqlQueryHandlerV2`, with per-call query keys when one mutation issues several statements, or dependency injection.\n* **Redundant happy-path slices.** One happy-path test plus separate corner cases beats ten tests that all break together. Multiple assertions per integration test are fine.\n* Unnecessary setup (`GRANT CREATEROLE` when the test already logs in as a superuser).\n* Regex over `toContain` for generated code, `!` over `?.` inside tests so they fail early, no `if` branches around expects, constants hoisted out of `describe` in `SCREAMING_SNAKE_CASE`.\n* Every bug found in review gets a regression test. New helpers and classes get unit tests.\n* A test that asserts a function's arity or a mock's call shape breaks when someone adds an optional parameter. Assert what the user sees instead.\n\n### 4.5 Types and nullability\n\n* No `any`, no casts that the types do not need (`as unknown as T`).\n* Ask why each field is nullable when every caller defines it.\n* Fields that are always null together become one nested object (`cluster: { id, name } | null`).\n* Two booleans that can conflict (`usersOnly`, `rolesOnly`) become a discriminated union.\n* Boolean props are optional (`canIssue?: boolean`) so `<X canIssue />` works. An error prop is `error?: boolean`, not `unknown`.\n* Type a required key as required instead of throwing when it is missing.\n* `===` over `==`. Prefer `undefined` over `null` for optional values.\n\n### 4.6 Naming\n\n* Units are suffixes: `durationMs`, not `ms`.\n* Booleans are present tense (`hasActiveSubscription`). Components are nouns, not verbs (`LicenseKeyCtaContent`, not `GetLicenseKey`).\n* `Inner` suffix only for a suspense boundary split, `Content` for a block of content.\n* `build...Query` is reserved for SQL builders. Transforms are `transform...`, fetchers `fetch...`, DOM listener setup `register...`.\n* Do not alias a name as an id (`parent.name as parentId`). SQL aliases are camelCase, named for the general concept (`relationComment`, not `tableComment`).\n* Callback props keep parity with DOM events (`onPointMove`, `onClick`), not new verbs like `onPointSelect`.\n* A variant that changes styling is a `variant` prop, not inferred from which callbacks were passed.\n* Icons end in `Icon`. Version gates are named constants (`MCP_OAUTH_MIN_VERSION`), not inline strings.\n* Loop variables say what they index (`bucketIndex`, not `j`).\n\n### 4.7 Code comments\n\nThe rules are in the root `CLAUDE.md` \"Code comments\" section. What reviewers\nflag in practice: comments with fluff, details that make no sense out of context\n(\"avoids alphanumeric sorting issues\"), and comments that no longer match the\ncode. They ask *for* comments on undocumented catalog behavior, on unconventional\nuses (react-query used only to get loading/error state for an initializer), on\nnon-obvious algorithms, and on props whose meaning is subtle. It is not necessary\nto describe the evolution of the code by referring to code that has been removed\n(\"use quicksort because bubble sort is inefficient\").\n\n### 4.8 Loading, error, and empty states\n\n* Every `useQuery` renders distinct loading, error (`isError`), and empty UI. A mutation failure is visible to the user, not only `console.error`.\n* No state falls through to an endless spinner (a disabled region is an error, not \"loading\").\n* Loading is a spinner inside the card, not \"Loading...\" text, which shifts layout on every refetch. Empty states render inside the card, not as a blank page.\n* Prefer `useQuery` with explicit states over new `useSuspenseQuery`.\n* **The global subscribes start empty.** `useAllClusters` and `useAllObjects` return `data: []` until `snapshotComplete`, so a page that reads empty as \"none\" shows \"No clusters\" or a false success on a cold load. Gate on `snapshotComplete`, and render the subscribe's error.\n* An entity with nothing to report gets one empty state, not a green \"0 of 0 within target\" summary over an empty chart.\n\n### 4.9 Data layer\n\n* Query keys start with `buildRegionQueryKey(...)` (`~/api/buildQueryKeySchema`), so switching region or organization never serves another region's cache.\n* Queries live under `src/api/materialize/...`, not in `platform/`.\n* `staleTime` / `gcTime` are deliberate. If data never goes stale, use `Infinity`, and invalidate on logout or mutation.\n* Refresh derived data by putting its query key in the namespace that mutations already invalidate, rather than polling (a 60s poll means 60s-stale ownership).\n* No new `useSql*` hook calls. `console/CLAUDE.md` (\"React Query\") has the read and write pattern.\n* Global subscribes are hoisted to the route that owns the section, or navigating remounts them and the view jitters.\n* Validate user-supplied parameters at the `build...Query` layer (see the start-time check in `queryHistoryList.ts`), not only in the UI.\n* Anything persisted to localStorage is scoped by organization and region, and cleared on region switch. An unscoped key leaks one tenant's catalog into another session.\n\n### 4.10 React hooks and state\n\n* No `useEffect` to sync props into state or to react to a user action. Do it in the event handler, or initialize state in the modal's `onOpen`.\n* Effect dependencies hold the primitive that matters (the JWT string, not the whole query result), so work reruns only when it should.\n* TanStack table `columns` are static. Dynamic data flows through `data`, or cells remount and tooltips vanish.\n* One owner per lifecycle: a connection manager owns connect, disconnect, and retry, and the hook is a thin shell. Shell output changes go through the `websocketFsm.ts` state machine.\n* For Jotai, a derived write-only atom with an early return, not returning the previous object and hoping it skips a render.\n* A helper that mutates variables outside its scope is harder to read than the same code inline with a comment.\n\n### 4.11 Routing and URL state\n\n* View modes and modal flows (graph vs table, edit role) are subroutes, so a reload keeps the user's place.\n* Filter and lookback state belongs in the URL, not localStorage, when it should survive reload and share.\n* Do not branch on `location.pathname` when `<Route>` already branches.\n* A URL-selected entity that does not exist (a dropped cluster in an old link) must not silently show a different one. Write the fallback into the URL, or say it was not found.\n* Imports use `~/` unless the file shares a parent directory.\n\n### 4.12 Versions, flags, and deployment modes\n\n* A query that reads a new column or relation breaks older environments. Gate it with `useEnvironmentGate` and a named version constant, per `console/doc/mz-backwards-compatibility.md`. Self-managed can run a new Console against much older environments.\n* Test across versions with emulator images on another port (`misc/docker/README.md` in the Console tree) or a local kind cluster.\n* Cloud-only initialization goes in `CloudAppInitializer`, not `AppInitializer` (flexible deployments and impersonation).\n* `forceShow` on a nav item breaks when the environment is disabled.\n* Superuser checks through `usePrivileges` hit `has_table_privilege` and load environmentd. Use `useIsSuperUser`.\n* An `AppConfigSwitch` whose branches render the same thing is noise.\n\n### 4.13 Visual and UX QA\n\nReviewers run the branch and attach screenshots or recordings. If you can run it\n(`console/CLAUDE.md` has the dev commands), do. Otherwise list these for the human\nreviewer:\n\n* Overflow, especially in modals, filters, and form footers.\n* Dark mode. Any literal color (`colors.green[50]`, hex, `hsl(`) is suspect, since it does not flip.\n* Spacing on the 4px grid in relative units (`3`, not `\"12px\"`). Valid `textStyle` values come from `src/theme/typography.ts`. Valid Chakra tokens for `zIndex`. Chakra style props over `style`.\n* **Types do not catch invalid style values.** Chakra passes `alignItems=\"left\"` straight through (Chrome computes it as `normal`, which `CSS.supports(\"align-items\", \"left\")` confirms), and an undefined `textStyle` such as `text-ui-sm` falls back to the default without an error.\n* Consistency with existing patterns: the app's purple color scheme, existing button hover behavior, filter chips like query history, an Apply button for unit inputs. \"We don't do this anywhere else\" is a common and valid objection.\n* Layout shift on refetch, jitter on navigation, flicker on load.\n* Thresholds that agree with each other (a value turns orange at 5s but the filter starts at a minute).\n* Copy: title case in titles, correct terminology (\"role with LOGIN attribute\", not \"privilege\"), where to save a generated config, a link to the docs page.\n* Semantic elements (`ul`/`li` for lists).\n* **Mouse-only click targets.** A clickable `Tr`, `HStack` or legend pill with `onClick` cannot be reached from the keyboard. Make it a button (`as=\"button\"`, `type=\"button\"`) with `aria-pressed` for toggles (see `connectComponents.tsx`), and prefer `:focus-visible` over an always-on focus ring.\n* One placeholder for a missing value across a page (`-` or `—`, not both).\n\n### 4.14 Security and session traps\n\nRare but high severity, and most often caught by automated QA review after merge:\n\n* Tenant or region state that survives a switch: caches, module-level singletons, persisted rows seeded with `snapshotComplete: true`.\n* Auth flows where the client clock or cached token decides a redirect. Redirects should be server-authoritative, or users loop.\n* Two concurrent sessions (password and OIDC) where queries silently run as the wrong account.\n* State-changing `GET`s, and protections that rely on another system's cookie config (`SameSite`) instead of checking `Origin` / `Sec-Fetch-Site`.\n* `btoa` on non-ASCII input. Service accounts where the code assumes a user email.\n\n### 4.15 Charts and graphs\n\nGraph PRs collect the same gotchas. Check each:\n\n* **Reuse first.** The 4.3 rows for graph helpers and visx plumbing. A block near identical to `FreshnessGraph`, `UtilizationGraph` or `ThresholdLineGraph` should come from the same pieces.\n* **Hover redraws only the cursor and tooltip.** Mark layers (lines, bars, rows) are `React.memo` with stable props: scales and layouts memoized on width and domain, series memoized. A memo only helps if callers hoist their arrays. One built inline reruns every render.\n* **Volatile inputs get their own layer.** Anything derived from `useAllObjects` or `useAllClusters` gets a new identity on any change anywhere in the environment. Draw it as its own memoized layer, or key its memo by content, so an unrelated DDL does not redraw the chart.\n* **Which point is hovered.** `findNearestDatum` returns the nearest bucket start, which near a bar's right edge is the next bar. For bucketed bars, find the bucket that contains the time.\n* **Decorative layers keep `pointerEvents=\"none\"`** and sit under the event overlay, or a full-width strip swallows hover.\n* **Labels state only what the data says.** A max-per-bucket series is a peak, not an average. Name the unit precisely (\"% of replica CPU allocation\"), and do not label an inferred state as recorded.\n* **Wide windows stay readable.** Bars narrower than a few pixels alias into stripes. Merge buckets client-side, keeping each metric's peak.\n* **Colors.** Series take `colors.lineGraph`. A generated palette varies perceived lightness (`d3.hcl` / `d3.lab`), not HSL lightness.\n* **Axis formatters are for axes.** `formatDurationForAxis` and tick formatters round. Headline and table values need the unrounded formatter.\n* **Refresh cost.** A chart that polls re-reads its whole window on every refetch. See 5.1 in `cost-and-structure.md`.\n\n### 4.16 Design docs\n\nFor `doc/design/` PRs, reviewers ask for concrete ranges and limits, error states\n(an unresponsive cluster), reuse of existing global subscribes and components,\nconsistency when two polled queries resolve at different times (a subscribe\navoids it), client-side formatting, and a scoped first version (superuser only)\nbefore the general case.\n\n## 5. Check what the change costs\n\nWhen the diff changes a query builder, a subscribe, a `refetchInterval` or\n`staleTime`, or anything that runs at pointer or keystroke rate, read section 5\nof `cost-and-structure.md` in this skill's directory and work through it. The\n`mz_catalog_server` cost and the render cost it checks do not show up in a read\nof the diff.\n\n## 6. Audit hooks and component structure\n\nWhen the diff adds or moves a `useEffect`, `useMemo`, `useCallback`, or\n`React.memo`, or splits a component, read section 6 of `cost-and-structure.md`.\n\n## 7. Write the comment the way a Console reviewer writes it\n\nFindings are triaged with the person you are reviewing for first. Present the\nlist, let them cut and rephrase, then post. Do not post a review off your own draft.\n\nComments are short, hedged, and collaborative. State the claim, give at most one\nclause of why, name the alternative, stop. Typical openers: `Nit:`, `I think we\nshould`, `Maybe we can`, `Could we reuse`, `Let's just`, `I'd recommend`,\n`I wonder if`, `Kinda feels like`, `Why is X nullable here?`, `How does this look\nin dark mode?`.\n\nReal comments at the right length:\n\n> nit: We usually like to use `<>` for react fragments\n\n> Can most likely reuse the TextLink component\n\n> Why is objectId nullable here? Usage of this hook shows me that it's always defined\n\n> Kinda feels like these should be grouped into a `cluster` object, especially if both of them can be null\n\n> Let's remove this comment.\n\nKeep measurements and probe output out of the comment. They belong in your reply\nto the person you are reviewing for. A bug claim is the exception: attach the\nrepro (a screenshot, a recording, or a short diff that triggers it), because that\nis what makes it actionable.\n\nPoint at the existing pattern with a permalink rather than describing it. Offer\npseudocode or a suggestion block when the fix is not obvious. Say when something\nis not blocking: \"Doesn't need to be blocking but I suggest in a followup PR,\"\n\"We can do this as a followup.\"\n\n## 8. Post as a single review\n\nOne review, not N comments, so the author gets one notification:\n\n```bash\ngh api repos/MaterializeInc/materialize/pulls/<N>/reviews \\\n  --method POST --input review.json\n```\n\n`review.json` is `{commit_id, body, event: \"COMMENT\", comments: [...]}`. Use\n`event: \"COMMENT\"` unless the person asked to approve or request changes. Each\ninline comment is `{path, line, side: \"RIGHT\", body}`, and a multi-line anchor adds\n`start_line` and `start_side`. General findings go in `body`.\n\nPermalink every code reference and pin the SHA:\n\n* Code already on main gets the PR's **base** SHA (`gh pr view <N> --json baseRefOid`).\n* Code inside the PR gets the **head** SHA.\n* Verify each cited line resolves before posting: `git show \"<sha>:<path>\" | sed -n '<line>p'`. When the SHA is in a shell variable, brace it (`\"${sha}:${path}\"`). zsh reads `$sha:c` as a modifier and drops the colon, even inside quotes.\n\nAfterwards, confirm what landed:\n\n```bash\ngh api \"repos/MaterializeInc/materialize/pulls/<N>/comments?per_page=100\" \\\n  --jq '[.[] | select(.pull_request_review_id == <id>)] | length'\n```\n\nBefore posting, check for a pending review. GitHub allows one pending review per\nperson per PR, so a stray one blocks a new review, and one pinned to an older\ncommit cannot take new comments because their lines resolve against that commit:\n\n```bash\ngh api \"repos/MaterializeInc/materialize/pulls/<N>/reviews?per_page=100\" \\\n  --jq '[.[] | select(.state == \"PENDING\")]'\n```\n\n* **Inline comments must fall inside a diff hunk.** Read the ranges with `gh api \"repos/MaterializeInc/materialize/pulls/<N>/files\" --jq '.[].patch' | grep '^@@'`. Anything outside goes on the file (`subjectType: FILE`) or in the body.\n* **Replies and new threads in one review** need GraphQL: `addPullRequestReview` with `commitOID`, then `addPullRequestReviewThread` and `addPullRequestReviewThreadReply` with that `pullRequestReviewId`, then `submitPullRequestReview` with `event: COMMENT`.\n* **Pending comments are invisible to REST**, where `pulls/comments/<id>` returns 404. Read and edit them through GraphQL (`updatePullRequestReviewComment`).\n* **Link the priorities.** When a review mixes bugs and nits, list the bugs in the body with `#discussion_r<id>` links so the author fixes those first.\n* **Pace the batches.** When the author has just received a large review, post the blocking items and hold structural suggestions until they respond.\n\n## 9. Re-review after the author pushes\n\n* **Diff from the last SHA you reviewed, not from the base:** `git fetch upstream pull/<N>/head:pr-<N>-r2 && git diff <last-reviewed-sha> pr-<N>-r2`.\n* **Sort each earlier comment into fixed, partly fixed, or open by reading the code at the new head.** Replies do not count, and a fix without a reply is fine. Thread state comes from GraphQL `reviewThreads { isResolved isOutdated }`.\n* **Probe every fix for the bug it might introduce.** Removing an effect left a debounce that could strand a drag handle, and scoring unreadable objects as breaching reached a \"right now\" column. Both passed the existing tests.\n* **When the author pushes back with numbers, check they measured what you suggested,** and time a plan label rather than trusting it. `lookup` against `differential join` said nothing about which was faster. Reply in the thread with your own measurement, since numbers belong in that reply.\n* **Re-run the suite on the new head,** and say so in the body.\n\n## Output\n\nGroup findings as **Blocking**, **Strong suggestions**, **Nits**, and mark\nfollow-up candidates. Open the summary with one specific sentence on what the PR\ndoes well (\"Loving the tests!\", \"Great work making this page dynamic, and it uses\na lot of our existing patterns\"), then the verdict (\"Small nits but otherwise\nlooking really good\", \"Once my comments are addressed, LGTM\"). Name the test\nresult. Flag anything you could not verify rather than asserting it.\n\n<!-- File: .agents/skills/mz-dbt-release/SKILL.md (Tokens: ~1312 | Category: anthropic-skill) -->\n---\nname: mz-dbt-release\ndescription: >\n  Cut a dbt-materialize PyPI release: bump the version in `__version__.py`\n  and `setup.py`, date the `Unreleased` CHANGELOG entry, and open the\n  release PR with a `Ship: <url>` body. Trigger: \"cut a dbt release\",\n  \"release dbt-materialize\", \"release the dbt adapter\", \"ship\n  dbt-materialize vX.Y.Z\", \"publish dbt-materialize to PyPI\", \"bump\n  dbt-materialize version\", \"new dbt adapter version\". Use this skill\n  even if the user just says \"ship the dbt adapter\" or pastes a feature\n  PR and asks for \"the next dbt release\" without naming version mechanics.\n---\n\n# Cutting a dbt-materialize release\n\nA release PR bumps the published version of the `dbt-materialize` PyPI\npackage. It is mechanical: three files change, one commit, one PR.\n\nThe adapter lives in `misc/dbt-materialize/`. PyPI publication is handled by\nCI after the PR merges.\n\n## Files to touch\n\nExactly three files:\n\n| File | Change |\n|---|---|\n| `misc/dbt-materialize/dbt/adapters/materialize/__version__.py` | Bump `version = \"X.Y.Z\"` |\n| `misc/dbt-materialize/setup.py` | Bump `version=\"X.Y.Z\"` (kwarg in `setup(...)`) |\n| `misc/dbt-materialize/CHANGELOG.md` | Insert `## X.Y.Z - YYYY-MM-DD` between `## Unreleased` and the existing bullets |\n\nBoth version strings must stay in sync — the comments in each file say so.\n\n## Picking the next version\n\n* Default: bump the **patch** component (`1.9.9 → 1.9.10`).\n* The **minor** component tracks the required `dbt-postgres` minor. Only bump\n  minor when upgrading to a new `dbt-postgres` minor (e.g. `1.9.x → 1.10.0`).\n* Read the current version from `__version__.py`.\n\n## Confirming what's being released\n\nThe `## Unreleased` section of `CHANGELOG.md` is the source of truth for what\nships. If it's empty, there is nothing to release. Cross-check with:\n\n```\ngit log <prev-release-sha>..HEAD -- misc/dbt-materialize/\n```\n\nIf a commit since the previous release touched `misc/dbt-materialize/` but is\nnot represented under `## Unreleased`, stop and ask whether it should be\nincluded — don't silently invent a changelog entry.\n\n## CHANGELOG edit\n\nThe release does **not** rewrite the unreleased bullets; it just dates them.\nInsert a new dated heading immediately after `## Unreleased`, leaving\n`## Unreleased` itself empty:\n\n```\n## Unreleased\n\n## 1.9.10 - 2026-05-20\n\n* Support unmanaged clusters in `deploy_init`. ...\n```\n\nUse today's date in `YYYY-MM-DD`.\n\n## Branch, commit, PR\n\n* Branch name: `dbt-release-X.Y.Z`, branched from current `main`.\n* Commit subject: `dbt-materialize: release vX.Y.Z`\n* Commit body / PR body: a single `Ship: <url>` line pointing at the PR that\n  triggered this release (the feature/fix PR being shipped). If multiple PRs\n  are being shipped, pick the headline one — the changelog already enumerates\n  them.\n* PR title: same as the commit subject.\n* PR targets `MaterializeInc/materialize:main`.\n\nExample:\n\n```\ngh pr create --repo MaterializeInc/materialize --base main \\\n  --head <your-fork>:dbt-release-1.9.10 \\\n  --title \"dbt-materialize: release v1.9.10\" \\\n  --body \"Ship: https://github.com/MaterializeInc/materialize/pull/36625\"\n```\n\n## What you do NOT run\n\nThis PR touches only Python version strings and Markdown. Skip:\n\n* `cargo check`, `cargo clippy`, Cargo.lock work — no Rust changes.\n* Test suites — there are no tests to add for a version bump.\n\n`bin/fmt` and `bin/lint` cover `misc/dbt-materialize/*.py`, but a\none-character version bump almost never triggers them. Run them if\nyou've made an accidental wider change; otherwise they're optional.\n\n## Verifying before pushing\n\n```\ngit diff --stat\n```\n\nShould show exactly three files, predominantly a `+4 / -2` shape (one\nline each in the two version files, two added lines in `CHANGELOG.md`).\nA few extra lines in `CHANGELOG.md` are fine if a bullet was reformatted\nwhile finalising it; an unexpected fourth file means you've gone\noff-script.\n\n## Quick reference\n\n```\n# 1. Branch\ngit checkout -b dbt-release-X.Y.Z\n\n# 2. Edit the three files (see table above)\n\n# 3. Verify\ngit diff --stat   # expect 3 files, +4/-2\n\n# 4. Commit\ngit commit -am \"dbt-materialize: release vX.Y.Z\n\nShip: <feature-pr-url>\"\n\n# 5. Push and open PR\ngit push -u origin dbt-release-X.Y.Z\ngh pr create --repo MaterializeInc/materialize --base main \\\n  --head <your-fork>:dbt-release-X.Y.Z \\\n  --title \"dbt-materialize: release vX.Y.Z\" \\\n  --body \"Ship: <feature-pr-url>\"\n```\n\n## Historical reference\n\nRecent release commits (find via `git log --oneline -- misc/dbt-materialize/dbt/adapters/materialize/__version__.py`):\n\n* v1.9.10 — PR #36636\n* v1.9.9 — PR #36609\n* v1.9.8 — PR #36529\n\nInspect any of them with `git show <sha>` to see the exact diff shape.\n\n<!-- File: .agents/skills/mz-debug-ci/bk-fallback.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Mz-debug-ci Skill\n\nPath: `.agents/skills/mz-debug-ci/bk-fallback.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/bk-fallback.md)\n\n<!-- File: .agents/skills/mz-debug-ci/rare-failures.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Mz-debug-ci Skill\n\nPath: `.agents/skills/mz-debug-ci/rare-failures.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-debug-ci/rare-failures.md)\n\n<!-- File: .agents/skills/mz-debug-ci/SKILL.md (Tokens: ~4114 | Category: anthropic-skill) -->\n---\nname: mz-debug-ci\ndescription: >\n  Investigate CI failures on PR via gh + Buildkite MCP or bk CLI. Trigger: failing checks,\n  Buildkite failures, CI issues — \"why is CI red\", \"build broken\",\n  \"checks failing\", \"what went wrong in CI\", \"nightly broke\",\n  \"tests failing on this PR\", or pasted Buildkite URL. Also PR number +\n  why failing.\nargument-hint: <PR number, GitHub PR URL, or Buildkite build URL>\n---\n\nInvestigate CI failures for a Materialize PR or Buildkite build.\n\nIf the input is a Buildkite build URL rather than a PR (a scheduled nightly on\nmain, a release-qualification build), start directly at \"Listing a\nbuild's failed jobs directly\", taking `<PIPELINE>` and `<BUILD_NUMBER>`\nstraight from the URL:\n`https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>`,\nthen continue from Step 2.\n\n## Prerequisites\n\nThis skill requires `gh` (GitHub CLI), authenticated, plus one of two ways\nto reach Buildkite: the Buildkite MCP server's tools (preferred, next\nsection) or `bk` (Buildkite CLI), authenticated. If any of these turns out\nto be missing or unauthenticated, **stop immediately** and tell the user\nwhat to fix (`bk configure` or `bk auth login` for the Buildkite CLI, or the\nMCP setup below). Do not attempt to use the REST API directly or any other\nworkaround.\n\n## Buildkite MCP: preferred when available\n\nWhen the Buildkite MCP server's tools are available in the session (tool\nnames like `get_build_failure_summary`, `search_logs`, ...), prefer them\nover `bk` everywhere below: structured JSON with the\njob-state semantics already applied, and no log cleanup needed. The server documents its\nown tools; this section adds only what it does not know:\n\n- `org_slug` is `materialize`. `get_build_failure_summary` replaces \"Listing\n  a build's failed jobs directly\", Step 2, and often Step 3;\n  `search_logs`/`read_logs`/`tail_logs` replace the `bk job log` pipelines;\n  the artifact and annotation tools replace the corresponding `bk` commands.\n  Everything Materialize-specific (annotation contents, `bin/ci-failures`,\n  `bin/ci-shards`, Steps 4-5) applies unchanged.\n- The summary's job list is bounded by the response's `job_limit` (10 on the\n  hosted server, where a larger `max_jobs` is silently clamped), with terminal\n  problem jobs sorted before downstream broken ones. On `jobs_truncated:\n  true`, get the definitive failed-job list from `list_jobs` with\n  `state: \"failed,timed_out\"` and `include_retried_jobs: false`. That last\n  parameter is what preserves triage semantics: the server defaults it to\n  true, which counts every failed attempt of a retried job separately,\n  including jobs that passed on retry. The summary bounds annotation content\n  too: on `content_truncated: true`, fetch the full set with\n  `list_annotations`.\n\nIf the MCP is absent, read `bk-fallback.md` in this skill's directory for\nthe `bk` command recipes and continue with those. At the end, mention to\nthe user that they can set the server up (hosted read-only endpoint:\n`claude mcp add --scope user --transport http buildkite https://mcp.buildkite.com/mcp/readonly`;\nit saves tool calls but costs somewhat more tokens).\n\n## Search existing failures by pattern\n\nSearch for already recorded failures in CI with a short, stable error\nsubstring. The output is JSON whose `content` blobs are huge, so project it\ndown, and read the total from `.meta.totalRowCount`, which ignores the row\ncap and is the \"how chronic is this?\" number:\n\n```bash\nbin/ci-failures 'foobar' 2>/dev/null | jq -r '.meta.totalRowCount,\n  (.data[] | [.build_date[0:10], .build_identifier, .test_suite, .issue] | @tsv)'\n```\n\nThe positional pattern matches failure content. `--search <text>` replaces\nthe positional pattern and matches across build, test, issue, content, and\nbranch at once. Narrow results with `--branch main`, `--version`, `--issue`,\n`--test` (the Buildkite job name), `--build`, `--start-date`, or\n`--end-date`; `--size` raises the row cap (default 31, max 100).\n\n## Step 1: Find the build\n\nParse `$ARGUMENTS` to get the PR number (a plain number or a\n`https://github.com/MaterializeInc/materialize/pull/<PR_NUMBER>` URL), then\nlist the failing checks:\n\n```bash\ngh pr checks <PR_NUMBER> 2>&1\n```\n\nFailing rows link to\n`https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>#<JOB_ID>`,\nthe identifiers used throughout this skill.\n\n### Listing a build's failed jobs directly\n\nBuildkite mirrors each job's result into GitHub as a\n`buildkite/<pipeline>/<job>` commit status (never-dispatched jobs post\nnothing), but `gh pr checks` reads the PR's current head commit only. A build\ntherefore shows up there only if it ran on exactly that commit. For everything\nelse (scheduled nightlies on main, release-qualification builds, a PR nightly\nfrom before the latest push), list the genuinely failed jobs via the API:\n`list_jobs` as described in the MCP section, or with `bk` the listing recipe\nin `bk-fallback.md`. Either way, also grab the build's commit and branch:\nStep 5's known-vs-new checks need the commit, and neighboring-build lookups\nneed the branch.\n\nJob-state semantics matter here:\n- The failure states are `failed` and `timed_out`, the same set as\n  `BUILDKITE_RELEVANT_FAILED_BUILD_STEP_STATES` in\n  `misc/python/materialize/buildkite_insights/buildkite_api/buildkite_constants.py`.\n- `broken` is not a failure. Buildkite marks a job `broken` when its\n  configuration prevents it from running, for example a branch filter or an\n  `if:` condition that evaluates false, so the job never dispatches. Every\n  build contains broken jobs (a routine main nightly has dozens), so counting\n  them massively over-counts failures.\n- Jobs with `soft_failed: true` are allowed to fail without making the build\n  red; no current CI step sets it, so filtering it is defensive.\n- `waiting_failed` is not a failure either: the job's dependency (usually a\n  build step) failed, so it never started. Its presence is informative,\n  though. A PR build full of `waiting_failed` jobs ran no tests at all, so\n  report that fixing the visible failures is necessary but no proof the\n  build then goes green.\n- Retried jobs (automatic or manual) appear only as their latest attempt, so\n  a job that failed and then passed on retry counts as passed. That is the\n  right verdict for build triage; fetch the earlier attempts only when an\n  in-depth investigation needs them. A manual retry that failed again is\n  itself a signal: someone already tried to shake the failure off and it\n  reproduced.\n\nTo find a PR's builds that are not on its current head in the first place (an\nolder nightly, a pre-push run), get the branch with\n`gh pr view <PR_NUMBER> --json headRefName` and filter Buildkite builds by\nbranch; the MCP's `list_builds` takes a `branch:` filter, and fork branches\nare named `<owner>:<branch>` on Buildkite.\n\n## Step 2: Check annotations first\n\n**Before diving into logs**, fetch the build annotations. They contain pre-extracted error messages, stack traces, and links to known flaky test issues — this saves significant time compared to grepping through raw logs. With the MCP they arrive in `get_build_failure_summary` (or `list_annotations`); with `bk`, use the annotations recipe in `bk-fallback.md`.\n\nBesides the error itself, an error annotation (`style` `\"error\"`) carries\nknown-issue links (Linear keys like `CPU-170`, or legacy\n`database-issues/#NNNN`) and the job's main-branch history, a flaky-test\nindicator.\n\nThings to know when reconciling annotations against jobs:\n\n- Error annotations persist from failed attempts even when a retry later\n  passed, so a build can have more error annotations than failed jobs. A\n  `bin/ci-annotate-errors` annotation's `context` field is `<JOB_ID>-error`,\n  and that job id can belong to a retried attempt that the default job\n  listing does not contain. (Other producers use other contexts, e.g.\n  `images-not-public` from the build step.)\n- Only jobs running through the mzcompose/cloudtest plugins get annotations\n  and `bin/ci-failures` rows (both come from `bin/ci-annotate-errors`). Plain\n  shell steps (lint, Security advisories, and similar checks) produce neither,\n  so a real failure there has no annotation at all. Go straight to its job\n  log.\n- Info-style annotations (\"<job> succeeded with known error logs\") mean the\n  job passed and every logged error matched an open known issue; they are\n  not failures. When a job went red via \"Test succeeded, but unknown errors\n  found in logs\" (see Step 3), the unknown and potential-regression entries\n  in its annotation are what turned it red. The reverse also exists: a\n  failed job turns green (\"<job> would have failed with known error logs\")\n  when all its logged errors match issues marked `ci-ignore-failure: true`.\n\n## Step 3: Fetch logs when needed\n\nOnly fetch full logs when annotations don't provide enough detail. On PR test\nbuilds, read compile and lint job logs first (clippy, lint-and-rustfmt): they\noften explain every downstream failure.\n\nFetch and search logs with the MCP's `search_logs`/`read_logs`/`tail_logs`,\nor with `bk` use the job-log recipes in `bk-fallback.md`. The log tail can be a\nred herring: some jobs print long non-error output after the actual error\n(cargo deny's dependency tree runs hundreds of lines past the advisory), so\nwhen the tail shows no error, search or grep for the error instead of\ntailing more.\n\nNOTE: for mzcompose-based jobs (testdrive, SQLsmith/SQLancer, platform checks,\n...) the job console log holds only the harness's output. The services' own\noutput (environmentd/clusterd panics and errors) goes to `services.log` and\nsimilar files inside the job's log artifacts. A panic that is absent from\nthe job's console log can still be the failure, so never conclude \"this\nerror did not happen in this run\" from it alone. Check the annotations or\n`bin/ci-failures` instead, both are fed by `bin/ci-annotate-errors`, which\nscans the uploaded log artifacts (`services.log`, `run.log`, junit XML, ...)\nat the end of each job. The same scan can fail a job whose own workflow\npassed: the console log then reads `Test succeeded, but unknown errors found\nin logs, marking as failed`, and the cause is whatever the scan found in the\nartifacts, not a test failure. Alternatively, download the log artifacts and\ngrep them directly.\n\n### Artifacts\n\nJobs upload artifacts (junit XML, service logs, coredumps, ...). Use the\nMCP's `list_artifacts_for_build`/`list_artifacts_for_job`/`get_artifact`,\nor with `bk` use the artifacts recipes in `bk-fallback.md`.\n\n### Shard contents\n\nSharded jobs (SLT, testdrive, platform checks, feature benchmark, ...) record\nthe workflows/files/scenarios they ran in build meta-data. `bin/ci-shards`\nshows it:\n\n```bash\n# Mapping of every sharded job to what it ran, with a link per job whose\n# `#` fragment is the `<JOB_ID>` for fetching job logs (omit links with --no-url)\nbin/ci-shards https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER>\n\n# Which job(s) ran a specific item (test file, scenario, or workflow,\n# `workflow_foo_bar` function names are matched as `foo-bar`)\nbin/ci-shards https://buildkite.com/materialize/<PIPELINE>/builds/<BUILD_NUMBER> numeric.td\n```\n\n## Step 4: Categorize failures\n\nUse these Materialize-specific patterns to diagnose:\n\n### Clippy errors\nCode lint issues in changed files. Fix the code, not the lint config.\n\n### `check-test-flags` lint failure\nA new dyncfg (`Config::new(\"name\", ...)`) was introduced but not registered in the required places. Reproduce with `bin/lint-test-flags`.\n- `misc/python/materialize/parallel_workload/action.py` (`FlipFlagsAction.flags_with_values` or `uninteresting_flags`)\n- `misc/python/materialize/mzcompose/__init__.py` (get_variable_system_parameters / get_minimal_system_parameters / UNINTERESTING_SYSTEM_PARAMETERS)\n\n### Doctests failure\nThe Doctests job runs `bin/doc --document-private-items` (`ci/test/lint-doc.sh`), which sets `RUSTDOCFLAGS=-D warnings` and uses the pinned nightly. Reproduce on one crate with `bin/doc --document-private-items -p <crate>`. A common cause is a public doc comment linking a private item (`rustdoc::private_intra_doc_links`), which fails even with `--document-private-items`. `cargo doc` stops at the first failing crate, so a fix can reveal errors in the next crate.\n\n### Merge skew cargo check failure\nThis PR-only job (`ci/test/check-merge-with-target.sh`) merges the PR with its target branch and runs `cargo check --all-targets`. When it is red while the PR's own build and clippy jobs pass, the branch compiles alone but not merged with current main. Rebase and fix as the mz-commit skill describes.\n\n### Testdrive cascades\nAfter one test crashes environmentd, all subsequent tests in that shard fail with `Name or service not known` or `connection closed`. **Only the first failure in a shard matters** — everything after it is a cascade. Look for the first `error:` or `FAIL` in the log.\n\nTestdrive shards with the same number (e.g., `testdrive-10` and `testdrive-with-alloydb-10`) run the same tests — if both fail, it's likely to be the same root cause.\n\n### SLT failures\nCheck whether it's wrong output (behavioral change) vs. connection error (crash/timeout). Wrong output means the query semantics changed. An `InconsistentViewOutcome` failure comes from CI's `--auto-index-selects` mode; the mz-test skill explains the mechanism and the exemption lists.\n\n### Timeouts\nA `timed_out` job hit its step budget (`timeout_in_minutes` in\n`ci/<pipeline>/pipeline.template.yml`). The annotation records only the fact\n(\"test timed out\"), never the cause. Find the cause in the console log: the\nlast completed unit of work, plus anything marked as still running when the\njob was canceled (nextest prints `SLOW [>1200.000s]` lines naming the stuck\ntests). If no such marker appears, compare wall-clock against the job's last\npassing run to tell a gradually outgrown budget from a hung test.\n\n### Infrastructure flakes\nObserved: failures in unrelated jobs while downloading from `github.com/.../releases/download/` were GitHub CDN outages. Retry the job instead of debugging it.\n\n### Rarer classes\n\nIf the failure matches one of these, read `rare-failures.md` in this\nskill's directory for its triage pattern: `New regression against\n<version>` (feature benchmark), a cargo-fuzz crash, a Miri failure, a\nlimits-test failure, or `ImagesNotPublicError`.\n\n## Step 5: Summarize\n\nGroup failures by **root cause**, not by job name — typically many failing\njobs share just 1-2 root causes. Per root cause, report a description, the\naffected jobs, and what to fix.\n\nDistinguish between issues that are clearly caused by the change under test vs. pre-existing flaky tests. The annotations often link to known flaky test issues — use these to identify pre-existing flakes vs. new regressions.\n\nMore ways to establish known vs new:\n\n- Check whether main already fixed it. Any build can run code that is behind\n  main: a nightly investigated the morning after, but also a fresh PR based\n  on an older main. From an up-to-date main checkout run\n  `git log <BUILD_COMMIT>..HEAD -- <suspect-file>` or\n  `git log -S '<error token>'`. A fix can also sit in a not-yet-merged PR,\n  invisible to git log: search open PRs for the step name or error token\n  (`gh pr list --state open --search '<...>'`); a PR's file list shows\n  which of the build's root causes it covers. Do this for every failure,\n  and name any hit in the summary so nobody authors a duplicate. When\n  citing a later build as evidence\n  of a fix, confirm the specific job's state there is `passed`: a build can\n  be green because the job was `broken` and never ran.\n- Known-issue tracking lives in Linear. Annotations and `bin/ci-failures`\n  cite issue keys like `CPU-170` or `SS-361`, which resolve to\n  `https://linear.app/materializeinc/issue/<KEY>`. Legacy references point at\n  GitHub `MaterializeInc/database-issues` instead.\n- Annotations and `bin/ci-failures` classify a failure three ways; for the\n  latter two, check the signature's history with `bin/ci-failures` before\n  treating the failure as new. A known-issue link means it matched an open\n  tracked issue. `\"UNKNOWN ERROR\"` means it matched no tracked issue, not\n  that the failure is new: a long streak under that label is a chronic\n  untracked flake worth filing. An issue reference tagged\n  `(POTENTIAL REGRESSION)` means it matched a tracked issue that is now\n  closed: a known signature with stale tracking, not by itself evidence of\n  a new regression; suggest to the user that the closed issue may need\n  reopening. The issue matching is a content heuristic and often wrong, so\n  verify that the matched issue actually describes the problem: search the\n  tracker for the error text, since the right issue may already exist open,\n  or the problem may need a totally new one.\n\n<!-- File: .agents/skills/mz-dependencies/SKILL.md (Tokens: ~389 | Category: anthropic-skill) -->\n---\nname: mz-dependencies\ndescription: >\n  Trigger: adding, removing, or updating a Rust dependency, changing crate\n  features, touching Cargo.toml or Cargo.lock, \"cargo update\", \"add a crate\",\n  \"bump a dependency\", or a new dependency license in Materialize.\n---\n\n# Dependency management\n\n## Workspace dependencies\n\nAll third-party versions declared in `[workspace.dependencies]` in root\n`Cargo.toml`. Members use `dep.workspace = true` or\n`dep = { workspace = true, optional = true }`.\n\n* **Add new dep**: add to `[workspace.dependencies]` in root `Cargo.toml` first, then `dep.workspace = true` in member crate.\n* **Never inline version** in member `Cargo.toml` — `bin/lint-cargo` enforces.\n* **Update version**: change once in root `Cargo.toml`.\n* Cargo unifies features workspace-wide, so workspace declaration is union of all features across crates. Members just use `dep.workspace = true`.\n\n## Cargo.lock\n\nNever regenerate full Cargo.lock. When changing deps:\n\n* **Add dep or change features**: run `cargo check` — updates only what changed.\n* **Update specific crate**: `cargo update -p <crate>` (optional `--precise <version>`).\n* **Never bare `cargo update`** — bumps every semver-compatible dep, causes unrelated breakage from transitive changes.\n* **If lock regenerated**, diff before commit (`git diff Cargo.lock | grep '^[+-]version'`) and pin back unintended bumps with `cargo update -p <crate> --precise <old-version>`.\n\n## Licensing\n\nTwo files control license policy, **keep in sync**: `deny.toml` (`[licenses].allow`) and `about.toml` (`accepted`). New dep with new license not already allowed: add SPDX identifier to both.\n\n<!-- File: .agents/skills/mz-feature-flags/SKILL.md (Tokens: ~433 | Category: anthropic-skill) -->\n---\nname: mz-feature-flags\ndescription: >\n  Trigger: adding or changing a feature flag, dyncfg, or system parameter\n  default, reviewing a PR that adds one, LaunchDarkly flag setup, or\n  bin/lint-test-flags failures in Materialize.\n---\n\n# Feature flags\n\nChoosing whether a new feature flag defaults on or off in production is a\nproduct decision that needs context an agent rarely has. When adding a flag,\nask a human which default to use. When reviewing a PR, do not question the\ndefault it chose. However, in tests, a new flag should usually be on, so that\nsqllogictest and testdrive exercise the new code path. If\nits production default is off, turn it on for CI in\n`misc/python/materialize/mzcompose/__init__.py`\n(`get_minimal_system_parameters` if tests fail with it off, otherwise\n`get_variable_system_parameters`), which `bin/sqllogictest` and mzcompose's\n`Materialized` service pass to environmentd as system parameter defaults.\nRust integration tests use `TestHarness::with_system_parameter_default`.\nA value listed there overrides the compiled-in default in CI, so changing a\ndefault means changing both. `bin/lint-test-flags` fails unless every new\ndyncfg is registered for parallel workload and mzcompose (see the mz-debug-ci\nskill). A compiled-in default is not evidence of the production value: check\nwhether `test/launchdarkly-flag-consistency/mzcompose.py` lists the flag in\n`INTENTIONAL_LD_OVERRIDES` or `KNOWN_CROSS_ENV_DIVERGENCES`. A session\nvariable takes a system default only if `SESSION_SYSTEM_VARS` in\n`src/sql/src/session/vars.rs` lists it. Otherwise `--system-parameter-default`\nfor it only logs a warning at startup. Observed: the LaunchDarkly MCP\n`create-flag` tool creates boolean flags that serve true in every environment,\nincluding production. A human must set each environment's off variation and\nfallthrough to false.\n\n<!-- File: .agents/skills/mz-limits-test/SKILL.md (Tokens: ~812 | Category: anthropic-skill) -->\n---\nname: mz-limits-test\ndescription: >\n  Add/modify/debug limits test. Trigger: \"limits test\", \"Generator subclass\",\n  \"many objects\", \"scaling test\", or stress-test Materialize with many objects\n  (tables, views, sources, indexes). Also edits in test/limits/mzcompose.py.\n---\n\n# Limits Test Framework\n\nThe limits test stresses Materialize with large numbers of objects, complex queries, and large ingestions. It catches regressions via fixpoint panics, stack overflows, CI timeouts (O(N^M) with N~1000), and OOM panics (2GB memory limit).\n\n## Where It Lives\n\n- **Main file**: `test/limits/mzcompose.py`\n- All Generator subclasses live in that single file\n- **Auto-discovery**: Subclasses of `Generator` are found automatically via `all_subclasses(Generator)` from `materialize.util` - no registration needed\n\n## Generator Base Class\n\n```python\nclass Generator:\n    COUNT: int = 1000      # Number of objects to create (override per test)\n    VERSION: str = \"1.0.0\" # Bump when test logic changes\n    MAX_COUNT: int | None = None  # Upper limit for --find-limit mode\n\n    @classmethod\n    def body(cls) -> None:\n        \"\"\"Override this. Print testdrive commands to stdout.\"\"\"\n        raise NotImplementedError\n\n    # Convenience iterators\n    @classmethod\n    def all(cls) -> range:       # range(1, COUNT+1)\n    @classmethod\n    def no_first(cls) -> range:  # range(2, COUNT+1) - skip first\n    @classmethod\n    def no_last(cls) -> range:   # range(1, COUNT) - skip last\n\n    @classmethod\n    def store_explain_and_run(cls, query: str) -> str | None:\n        \"\"\"Records EXPLAIN query for timing and prints `> {query}`.\"\"\"\n```\n\n`header()` is inherited - it resets the `public` schema and grants privileges. `footer()` prints a blank line. `generate()` calls `header()` → `body()` → `footer()`. `mz_system` is a built-in testdrive connection, so `$ postgres-execute connection=mz_system` works in `body()` without any registration.\n\n## How to Add a New Limits Test\n\n1. Add a new class in `test/limits/mzcompose.py` that extends `Generator`.\n2. Implement the `body()` classmethod. It must `print()` testdrive commands.\n3. Optionally override `COUNT` and `MAX_COUNT`, only if it's causing issues in CI.\n\n### Minimal Template\n\n```python\nclass MyNewFeature(Generator):\n    COUNT = min(Generator.COUNT, 100)  # Lower if operation is slow\n    MAX_COUNT = 800  # Optional: cap for --find-limit mode\n\n    @classmethod\n    def body(cls) -> None:\n        # Bump system limits if needed\n        print(\"$ postgres-execute connection=mz_system\")\n        print(f\"ALTER SYSTEM SET max_tables = {cls.COUNT * 10};\")\n        print(\"$ postgres-execute connection=mz_system\")\n        print(f\"ALTER SYSTEM SET max_objects_per_schema = {cls.COUNT * 10};\")\n\n        # Create objects\n        for i in cls.all():\n            print(f\"> CREATE TABLE t{i} (f1 INTEGER);\")\n            print(f\"> INSERT INTO t{i} VALUES ({i});\")\n\n        # Query and verify\n        for i in cls.all():\n            cls.store_explain_and_run(f\"SELECT * FROM t{i}\")\n            print(f\"{i}\")\n```\n\nYou can run it locally with `bin/mzcompose --find limits down && bin/mzcompose --find limits run main --scenario=MyNewFeature`, add `--find-limit` for the Release Qualification mode.\n\n<!-- File: .agents/skills/mz-parallel-workload/SKILL.md (Tokens: ~1829 | Category: anthropic-skill) -->\n---\nname: mz-parallel-workload\ndescription: >\n  Extend parallel-workload stress framework: random SQL concurrently to catch\n  panics + unexpected errors (not perf — see mz-benchmark). Trigger:\n  \"parallel workload\", \"parallel-workload\", \"action.py\" re parallel workload,\n  or testing panics/unexpected errors under concurrency. Also \"add this to\n  parallel workload\" or bug that panics under concurrent DDL/DML.\n---\n\n# Extending Parallel Workload\n\nThe parallel-workload framework stress-tests Materialize by running random SQL actions concurrently across multiple threads. It catches **panics and unexpected query errors** - it does not verify result correctness.\n\n## When to Use This Framework\n\nAdd coverage here when a bug manifests as:\n- A panic under concurrent operations\n- An unexpected error from a query that should succeed (or fail gracefully)\n- A crash or connection loss triggered by specific DDL/DML combinations\n\n## How It Works\n\n1. The orchestrator spawns N worker threads, each assigned an **action list** (read, fetch, write, dml_nontrans, or ddl) based on the configured `Complexity`.\n2. Each worker randomly selects actions from its list using **weighted random choice** and executes them in a loop until the runtime expires.\n3. If an action raises a `QueryError`, the worker checks `action.errors_to_ignore(exe)`. If the error matches, it's logged and ignored. If not, the **test fails**.\n4. All threads share a `Database` object that tracks created objects (tables, views, clusters, etc.) with thread-safe locking.\n\n## Adding a New Action\n\n### Step 1: Define the Action Class\n\nAdd your class in `action.py`. Subclass `Action` and implement `run()`:\n\n```python\nclass MyNewAction(Action):\n    def run(self, exe: Executor) -> bool:\n        # Return False to skip (e.g., precondition not met)\n        # Return True after successful execution\n        exe.execute(\"SELECT my_new_feature()\", http=Http.RANDOM)\n        return True\n```\n\n### Step 2: Override `errors_to_ignore()` If Needed\n\nIf your action can produce expected errors (e.g., object was concurrently dropped), extend the base list:\n\n```python\n    def errors_to_ignore(self, exe: Executor) -> list[str]:\n        result = super().errors_to_ignore(exe)\n        result.extend([\n            \"my expected error message\",\n        ])\n        # Conditionally ignore errors based on complexity or scenario\n        if exe.db.complexity == Complexity.DDL:\n            result.extend([\"does not exist\"])\n        return result\n```\n\n### Step 3: Register in an Action List\n\nAdd your action class and weight to the appropriate list at the bottom of `action.py`:\n\n```python\n# In ddl_action_list, read_action_list, write_action_list, etc.\nddl_action_list = ActionList(\n    [\n        ...\n        (MyNewAction, 5),  # weight controls selection frequency\n        ...\n    ],\n    autocommit=True,  # True for DDL, False for transactional actions\n)\n```\n\n**Which list to use:**\n- `read_action_list` - SELECT, SUBSCRIBE, COPY TO (autocommit=False)\n- `fetch_action_list` - DECLARE CURSOR / FETCH (autocommit=False)\n- `write_action_list` - INSERT, COPY FROM (autocommit=False)\n- `dml_nontrans_action_list` - DELETE, UPDATE, INSERT RETURNING (autocommit=True)\n- `ddl_action_list` - CREATE/DROP/ALTER/RENAME (autocommit=True)\n\n## Common Patterns\n\n### Picking a Random Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    with exe.db.lock:\n        if not exe.db.tables:\n            return False\n        table = self.rng.choice(exe.db.tables)\n    # Use table outside db.lock but inside table.lock for mutations\n    with table.lock:\n        if table not in exe.db.tables:\n            return False  # Was dropped while we waited\n        exe.execute(f\"ALTER TABLE {table} ...\", http=Http.RANDOM)\n    return True\n```\n\n### Respecting Object Limits\n\n```python\ndef run(self, exe: Executor) -> bool:\n    if len(exe.db.tables) >= MAX_TABLES:\n        return False  # Skip if at capacity\n    ...\n```\n\nLimits are defined in `database.py`: `MAX_TABLES=5`, `MAX_VIEWS=15`, `MAX_CLUSTERS`, `MAX_SCHEMAS`, etc.\n\n### Creating a New Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    if len(exe.db.views) >= MAX_VIEWS:\n        return False\n    view_id = exe.db.view_id\n    exe.db.view_id += 1\n    try:\n        schema = self.rng.choice(exe.db.schemas)\n    except IndexError:\n        return False\n    with schema.lock:\n        if schema not in exe.db.schemas:\n            return False\n        view = View(self.rng, view_id, base_object, schema, ...)\n        view.create(exe)\n    exe.db.views.append(view)\n    return True\n```\n\n### Dropping a Database Object\n\n```python\ndef run(self, exe: Executor) -> bool:\n    with exe.db.lock:\n        if len(exe.db.views) <= 2:\n            return False  # Keep a minimum\n        view = self.rng.choice(exe.db.views)\n    with view.lock:\n        if view not in exe.db.views:\n            return False\n        exe.execute(f\"DROP VIEW {view}\", http=Http.RANDOM)\n        exe.db.views.remove(view)\n    return True\n```\n\n### Scenario-Specific Actions\n\nSome actions only run in specific scenarios. Override `applicable()` for\nthis, do NOT gate on the scenario inside `run()`: the worker skips\ninapplicable actions without counting an attempt, which keeps the\nend-of-run action coverage check (an action that never succeeds over a\nwhole run fails it) free of false positives.\n\n```python\ndef applicable(self, exe: Executor) -> bool:\n    return exe.db.scenario == Scenario.Rename\n\ndef run(self, exe: Executor) -> bool:\n    ...\n```\n\n### Using Prepared Statements\n\n```python\ndef run(self, exe: Executor) -> bool:\n    query = \"SELECT ...\"\n    if self.rng.choice([True, False]):\n        self.stmt_id += 1\n        self.exe_prepared(query, f\"mystmt{self.stmt_id}\", exe)\n    else:\n        exe.execute(query, http=Http.RANDOM)\n    return True\n```\n\n## Extending an Existing Action\n\nOften the simplest approach is to add new SQL variants to an existing action's `run()` method. For example, to test a new expression type, add it to `expression.py`. To test a new system flag, add it to `FlipFlagsAction.flags_with_values`.\n\n### Adding a System Flag to FlipFlagsAction\n\nIn `FlipFlagsAction.__init__`, add an entry to the `flags_with_values` dictionary:\n\n```python\nself.flags_with_values[\"my_new_flag\"] = [\"true\", \"false\"]\n```\n\n## Running\n\n```bash\n# Default: DDL complexity, regression scenario, 600s runtime\nbin/mzcompose --find parallel-workload run default\n\n# Custom options\nbin/mzcompose --find parallel-workload run default \\\n    --complexity=ddl \\\n    --scenario=regression \\\n    --runtime=300 \\\n    --seed=42\n\n# Available complexities: read, dml, ddl, ddl-only, random\n# Available scenarios: regression, cancel, kill, rename, backup-restore, 0dt-deploy, random\n```\n\n## Important Design Rules\n\n1. **Minimize locking** - Use `exe.db.lock` briefly to pick objects, then use per-object locks. Excessive locking reduces the chance of finding real race conditions. You can instead add an expected error in `errors_to_ignore`\n2. **Return False, don't raise** - If a precondition isn't met (empty list, at capacity), return `False` to skip the action.\n3. **Handle IndexError from `rng.choice`** - Objects can be concurrently removed. Either check length first under lock, or catch `IndexError` and return `False`.\n4. **Track state changes** - If you create an object, append it to the appropriate `exe.db` list. If you drop one, remove it.\n5. **Use `self.rng`** - Always use the action's seeded `random.Random` instance, never `random.choice()` directly. This makes failures reproducible via `--seed`.\n\n<!-- File: .agents/skills/mz-platform-checks/SKILL.md (Tokens: ~1489 | Category: anthropic-skill) -->\n---\nname: mz-platform-checks\ndescription: >\n  Create/modify/debug platform check. Trigger: \"platform check\",\n  \"platform-checks\", \"upgrade check\", \"restart check\", or writing Check class\n  testing feature survival across restarts/upgrades. Also edits in\n  misc/python/materialize/checks/all_checks/.\n---\n\n# Platform Checks\n\nThe Platform Checks framework is \"write once, run everywhere\" - a single test validates a feature across multiple scenarios (restarts, upgrades, etc.).\n\n## Key Concepts\n\n- **Check**: A Python class that tests one feature via testdrive fragments.\n- **Scenario**: The context (e.g., restart, upgrade) in which checks run.\n- **Action**: An individual step within a scenario (e.g., stop a container).\n\n## Where Checks Live\n\nAll checks are in `misc/python/materialize/checks/all_checks/`. The `__init__.py` auto-discovers every `.py` file in that directory, so adding a new file is all that's needed for registration.\n\n## Anatomy of a Check\n\nA check is a class that extends `Check` and implements three methods. Each returns a `Testdrive` object wrapping a testdrive fragment string.\n\n```python\nfrom textwrap import dedent\nfrom materialize.checks.actions import Testdrive\nfrom materialize.checks.checks import Check\n\nclass MyFeature(Check):\n    def initialize(self) -> Testdrive:\n        \"\"\"Run once at the start. Create tables, insert seed data, set up objects.\"\"\"\n        return Testdrive(dedent(\"\"\"\n            > CREATE TABLE my_feature_table (f1 INTEGER);\n            > INSERT INTO my_feature_table VALUES (1), (2), (3);\n        \"\"\"))\n\n    def manipulate(self) -> list[Testdrive]:\n        \"\"\"MUST return a list of exactly 2 Testdrive fragments.\n        Each runs after a restart/upgrade phase. Insert more data, ALTER objects,\n        or create derived objects (e.g., materialized views).\"\"\"\n        return [\n            Testdrive(dedent(\"\"\"\n                > INSERT INTO my_feature_table VALUES (4), (5);\n                > CREATE MATERIALIZED VIEW my_feature_view1 AS\n                  SELECT COUNT(*) AS c FROM my_feature_table;\n            \"\"\")),\n            Testdrive(dedent(\"\"\"\n                > INSERT INTO my_feature_table VALUES (6), (7);\n                > CREATE MATERIALIZED VIEW my_feature_view2 AS\n                  SELECT SUM(f1) AS s FROM my_feature_table;\n            \"\"\")),\n        ]\n\n    def validate(self) -> Testdrive:\n        \"\"\"Run after all initialize + manipulate steps. Verify everything survived.\n        MAY BE CALLED MORE THAN ONCE - must be idempotent. Any objects created\n        here must be TEMPORARY or explicitly dropped.\"\"\"\n        return Testdrive(dedent(\"\"\"\n            > SELECT * FROM my_feature_view1;\n            7\n            > SELECT * FROM my_feature_view2;\n            28\n        \"\"\"))\n```\n\n## Execution Order in a Scenario\n\n```\nStartMz → initialize() → [restart/upgrade] → manipulate(phase=0)\n  → [restart/upgrade] → manipulate(phase=1) → [restart/upgrade] → validate()\n```\n\nBoth manipulate phases always run. validate() may run multiple times.\n\n## Decorators and Version Gating\n\n### Disable a check\n```python\nfrom materialize.checks.checks import disabled\n\n@disabled(ignore_reason=\"due to SQL-450\")\nclass MyBrokenCheck(Check):\n    ...\n```\n\n### Non-idempotent external interactions\nIf a check ingests non-UPSERT data into Kafka/Postgres (i.e., replaying manipulate would duplicate data), annotate it:\n```python\nfrom materialize.checks.checks import externally_idempotent\n\n@externally_idempotent(False)\nclass MyKafkaCheck(Check):\n    ...\n```\n\n### Version-gated checks\nFor features that only exist from a certain version, override `_can_run`:\n```python\nfrom materialize.checks.executors import Executor\nfrom materialize.mz_version import MzVersion\n\nclass MyNewFeature(Check):\n    def _can_run(self, e: Executor) -> bool:\n        return self.base_version >= MzVersion.parse_mz(\"v0.80.0-dev\")\n```\n\n### Version-conditional testdrive lines\nUse `[version>=VVMMPP]` or `[version<VVMMPP]` prefixes for syntax that changed between versions (e.g., v0.92.0 → `9200`):\n```\n>[version<9200] CREATE SOURCE s FROM LOAD GENERATOR COUNTER (SCALE FACTOR 0.01)\n>[version>=9200] CREATE SOURCE s FROM LOAD GENERATOR COUNTER\n```\n\n## Tips for Good Checks\n\n1. **Naming**: Prefix ALL object names with a unique string (e.g., `my_feature_`) to avoid collisions with other checks running in the same Mz instance.\n2. **manipulate() returns exactly 2 elements**: The framework asserts this.\n3. **validate() is idempotent**: It may run multiple times. Use TEMPORARY objects or DROP at end.\n4. **Create objects in manipulate, not just initialize**: This tests that DDL works after restarts/upgrades.\n5. **Create derived objects on both old and new base objects**: e.g., a materialized view on a table from initialize AND on a table from manipulate.\n6. **Exercise full SQL syntax breadth**: Test every WITH option, every variant of the feature.\n7. **Ingest data in every phase**: Ensure data flows during restarts/upgrades.\n8. **Use `dedent()` on all testdrive strings**: Required for proper testdrive parsing.\n\n## Helper Methods on Check\n\n- `self.base_version`: The MzVersion the test started from (useful for upgrade scenarios).\n- `self.current_version`: The MzVersion currently running.\n- `self._kafka_broker()`: Returns the broker connection string for testdrive.\n- `self._default_cluster()`: Returns `\"quickstart\"`.\n- `self.rng`: A seeded `Random` instance for deterministic randomness.\n\n## Running\n\n```bash\n# Run a specific check against a specific scenario\nbin/mzcompose --find platform-checks run default --scenario=RestartEntireMz --check=MyFeature\n\n# Run with no restarts (useful for debugging the check itself)\nbin/mzcompose --find platform-checks run default --scenario=NoRestartNoUpgrade --check=MyFeature\n\n# Run all checks (default)\nbin/mzcompose --find platform-checks run default --scenario=RestartEntireMz\n```\n\n## Debugging CI Failures\n\n1. Find the scenario name near the top of the log: `Running ... --scenario=RestartEnvironmentdClusterdStorage`\n2. Find the failing check just before the error: `Running validate() from <materialize.checks.threshold.Threshold ...>`\n3. Reproduce locally: `bin/mzcompose --find platform-checks run default --scenario=RestartEnvironmentdClusterdStorage --check=Threshold`\n4. Try `--scenario=NoRestartNoUpgrade` to isolate whether the failure is restart/upgrade-related.\n\n<!-- File: .agents/skills/mz-pr-review/SKILL.md (Tokens: ~1645 | Category: anthropic-skill) -->\n---\nname: mz-pr-review\ndescription: >\n  Local code review of current branch vs Materialize standards. Trigger:\n  \"review my code\", \"review my changes\", \"check my diff\", \"does this look ok\",\n  \"what do you think of this PR\", \"code review\", or look over changes before\n  merging. Also PR number + wants feedback on quality, style, correctness.\n  For changes under `console/`, use mz-console-review instead.\nargument-hint: [base-branch]\nallowed-tools: [Bash, Read, Grep, Glob, Task]\n---\n\nPerform a local code review of the current branch's changes against Materialize project standards.\n\n## Steps\n\n1. Parse arguments from: $ARGUMENTS — a PR number, base branch, or nothing.\n2. Get the diff using the first method that works:\n   - **PR number given** (e.g. `123`): `gh pr diff 123`\n   - **git available**: `git diff <base>...HEAD` (default base: `main`)\n   - **jj available**: `jj diff -r <revset>` (default: diff from trunk)\n3. Get the file list from the same diff (add `--stat` for git, `--stat` for jj, or `gh pr diff 123 --stat` for PR).\n4. Review the diff against the checklists below.\n5. Present findings organized as: **Blocking**, **Strong suggestions**, **Nits**.\n\n## Review checklist\n\nThe overall developer guide for reviewing changes is defined in `doc/developer/guide-changes.md`, always read and follow its guidance.\n\n### Tests\n- Every behavior change has at least one new or modified test.\n- SQL/query behavior → look for `.slt` in `test/sqllogictest/`.\n- Wire/protocol behavior → look for `.pt` in `test/pgtest/`.\n- Rust logic/types/APIs → add or extend a Rust unit test in the crate (e.g. `#[cfg(test)]` or `tests/`); run with `cargo test -p mz-<crate>`.\n- Prefer testing observable behavior (SQL results, wire protocol) over implementation details.\n- Red flag: behavior change with no test changes.\n- For more testing guidelines, read `doc/developer/guide-testing.md`\n\n### LIR schema registry\nApplies when the diff changes the serde-visible shape of a type the LIR\nschema reaches: `src/compute-types/src/plan.rs`,\n`src/compute-types/src/plan/**`, a `*Func` payload, or a plan-reachable type\nin `mz-expr` or `mz-repr`.\n- `src/compute-types/tests/snapshots/lir_v{N}.json` is regenerated in the\n  same PR, and the diff matches the intended format change. Red flag: a\n  serde-visible change to a plan type with no snapshot diff.\n- If `LIR_VERSION` in `src/compute-types/src/plan.rs` had shipped, it is\n  bumped and the old `lir_v{N}.json` is left untouched rather than rewritten.\n- `Row` and `EvalError` never appear directly in plan types, only as\n  `StableRow` / `StableEvalError`, enforced by\n  `lir_schema_contains_only_stable_types`.\n\n### Scalar function registry\nApplies when the diff touches `UnaryFunc`, `BinaryFunc`, or `VariadicFunc`\n(`src/expr/src/scalar/func.rs`, `src/expr/src/scalar/func/**`, `#[sqlfunc]`\nbodies).\n- The registry snapshots under `src/compute-types/tests/snapshots/` are\n  regenerated in the same PR. A property change moves `func_registry.json`\n  and the current version's entry in `func_registry_digests.json`, a\n  `#[sqlfunc]` declaration or body change moves `func_registry_source.json`\n  alone, and a change to a hand-written variant's body moves neither.\n- Every new variant with a payload has a `Sample` in\n  `src/expr/src/scalar/func/registry.rs`, with a labeled second sample when a\n  property depends on the payload.\n- A removed function, a changed property, or a `body_fingerprint` change that\n  alters results for some input bumps `LIR_VERSION` in\n  `src/compute-types/src/plan.rs` if that version has shipped, and leaves the\n  old digest in `func_registry_digests.json` untouched. Additions need no\n  bump.\n- The PR description names the added, removed, and changed functions and,\n  for body changes, states whether results change.\n\n### Code style (Rust)\n- **Imports:** `std` → external crates → `crate::`; one `use` per module; prefer `crate::` over `super::` in non-test code.\n- **Errors:** Structured with `thiserror`; no bare `anyhow!(\"...\")`. `Display` should not print full error chain.\n- **Async:** Use `ore::task::spawn` / `spawn_blocking`, not raw `tokio::spawn`.\n- **Tests:** `#[mz_ore::test]`; panic in tests rather than returning `Result`.\n\n### Code style (SQL)\n- Keywords capitalized (`SELECT`, `FROM`); identifiers lowercase.\n- No space between function name and `(`.\n\n### Error messages\n- Primary: short, factual, lowercase first letter, no trailing punctuation.\n- Detail/hint: complete sentences, capitalized, period.\n- No \"unable\", \"bad\", \"illegal\", \"unknown\"; say what kind of object.\n\n### Sensitive data handling\n- Types holding passwords, keys, tokens, or credentials should use `mz_ore::secure::{SecureString, SecureVec}` or `zeroize::Zeroizing<T>` (from `mz_ore::secure`).\n- Sensitive types should **not** derive `Clone` or `Debug` (use custom `Debug` that redacts).\n- Stack-local buffers holding derived keys, nonces, or HMAC outputs should be wrapped in `Zeroizing<T>`.\n- See `doc/developer/generated/ore/secure.md` for full guidance and `src/ssh-util/src/keys.rs` for a reference implementation.\n\n### Architecture\n- **Simplicity:** No incidental complexity; simplify redundant logic.\n- **No special casing:** Prefer composable design over extra booleans/branches.\n- **Encapsulation:** sql-parser = grammar only (no semantic validation); sql = planning + semantics.\n- **Dependencies:** New crates must be justified.\n- For more design guidelines read: `doc/developer/best-practices.md`\n\n### Polish\n- No leftover `// XXX`, `// FIXME`, `dbg!`, `println!`, or commented-out code.\n- No unrelated formatting changes in untouched code.\n- New public items should have doc comments.\n\n### Release notes\n\nRelease notes are auto-generated per release from PR descriptions (the [`/mz-release-notes` skill](https://github.com/MaterializeInc/mz-skills/blob/main/.agents/skills/mz-release-notes/SKILL.md));\nauthors don't write them. So for a user-visible change, check that the PR *description* states the\nuser-observable effect in user-facing terms — not only implementation detail — so the classifier includes it.\n\n## One semantic change rule\n\nThe PR should do one thing. If it spans multiple CODEOWNERS areas (e.g. sql-parser + sql planner), consider suggesting a split.\n\n## Rules\n\n- Review the code, not the author. Explain the *why* behind suggestions.\n- Use **nit:** for preferences where reasonable people could disagree.\n- If the PR improves overall codebase health and blocking items are addressed, say so.\n- Do NOT make any changes — this is read-only review.\n\n<!-- File: .agents/skills/mz-profile/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# mz-profile\n\nPath: `.agents/skills/mz-profile/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-profile/SKILL.md)\n\n<!-- File: .agents/skills/mz-query-perf/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# mz-query-perf\n\nPath: `.agents/skills/mz-query-perf/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-perf/SKILL.md)\n\n<!-- File: .agents/skills/mz-query-tracing/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# mz-query-tracing\n\nPath: `.agents/skills/mz-query-tracing/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-query-tracing/SKILL.md)\n\n<!-- File: .agents/skills/mz-release-signoff/references/adapter.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/adapter.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/adapter.md)\n\n<!-- File: .agents/skills/mz-release-signoff/references/compute.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/compute.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/compute.md)\n\n<!-- File: .agents/skills/mz-release-signoff/references/persist.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/persist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/persist.md)\n\n<!-- File: .agents/skills/mz-release-signoff/references/reference-dashboards.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/reference-dashboards.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/reference-dashboards.md)\n\n<!-- File: .agents/skills/mz-release-signoff/references/sources-and-sinks.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.agents/skills/mz-release-signoff/references/sources-and-sinks.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/references/sources-and-sinks.md)\n\n<!-- File: .agents/skills/mz-release-signoff/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# mz-release-signoff\n\nPath: `.agents/skills/mz-release-signoff/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-release-signoff/SKILL.md)\n\n<!-- File: .agents/skills/mz-run/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# mz-run\n\nPath: `.agents/skills/mz-run/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-run/SKILL.md)\n\n<!-- File: .agents/skills/mz-test/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# mz-test\n\nPath: `.agents/skills/mz-test/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/mz-test/SKILL.md)\n\n<!-- File: .agents/skills/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Skills Skill\n\nPath: `.agents/skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/.agents/skills/README.md)\n\n<!-- File: doc/developer/generated/compute/extensions/_module.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Extensions Skill\n\nPath: `doc/developer/generated/compute/extensions/_module.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/MaterializeInc/materialize/HEAD/doc/developer/generated/compute/extensions/_module.md)\n\n<!-- ... +42 more modular skills available via /api/skills/MaterializeInc/materialize?path={path} -->\n</yakaai_skills>"}