# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this repo is `opencode-build` is a Docker image build for the [`opencode-ai`](https://github.com/sst/opencode) CLI. The image: - Installs Node.js 22, `uv`-managed Python 3.11, and `ripgrep`. - Installs Python data-stack deps from `requirements.txt` (PySpark, pandas, polars, clickhouse-connect, pymongo, redis, hdfs, ...). - Installs `opencode-ai` globally via npm. - Copies `skills/` into `/root/.config/opencode/skills/` so they are auto-discovered by Claude Code at runtime. The `skills/` directory contains a deterministic Chinese-language business-data → reviewed PySpark SQL pipeline. See [Pipeline architecture](#pipeline-architecture) for that. Most edits touch the skills, not the image plumbing. ``` requirements-analysis → metadata-validator → logic-planner → sql-context-builder → (SQL gen) → pyspark-sql-guardrails → sql-review ``` `pyspark-sql-pipeline` is the orchestrator that drives the full flow with explicit user-confirmation gates between stages. Every stage has a fixed **input/output contract** with one of two statuses: a *green* status that lets the pipeline advance, or `NEED_USER_CONFIRMATION` which **must stop** the pipeline and surface `pending_questions` to the user. Stage hand-offs are durable artifacts (`requirements_output`, `validation_result`, `field_mapping`, `logic_plan`, `sql_context`) that must be carried forward — never re-derived downstream. Status names are fixed strings (`READY_FOR_VALIDATION`, `VALIDATED`, `PLANNED`, `READY`, `PASS`, `FAIL`); re-check the actual status text, do not pattern-match on memory. ## Commands ### Build the Docker image ```bash docker build -t opencode-custom:latest . ``` The `Dockerfile` is two-stage: a builder that fetches Node.js 22 and a Python 3.11 tarball via `uv`, and a final image that copies the artifacts, installs `opencode-ai` via npm, and copies `skills/` to `/root/.config/opencode/skills/`. The `ripgrep-*.tar.gz` file in the repo root is unpacked into `/opt/ripgrep-15.1.0-x86_64-unknown-linux-musl/` and symlinked to `/usr/local/bin/rg`. ### Run the image ```bash docker run --rm -it opencode-custom:latest ``` The image's `ENTRYPOINT` is `opencode`. ### Validate a SQL string (the gate) The validator lives at `skills/pyspark-sql-guardrails/scripts/validate_sql.py` in this repo. **In the built image, it is at `/root/.config/opencode/skills/pyspark-sql-guardrails/scripts/validate_sql.py`** — the Dockerfile copies `skills/` to that path. ```bash # From the repo root — pipe multi-line SQL via stdin (preferred) cat <<'EOF' | python3 skills/pyspark-sql-guardrails/scripts/validate_sql.py SELECT 1 FROM dual EOF # Or single-line as first argument python3 skills/pyspark-sql-guardrails/scripts/validate_sql.py "SELECT 1 FROM dual" ``` Exit codes: - `0` → `PASS - first keyword: , body length: ` — you may now show the SQL - `1` → `FAIL - ` — stop, fix the SQL, re-run - `2` → usage error (no SQL provided) > ⚠️ The bundled `SKILL.md` and `validate_sql.py` / `test_sql_guard.py` docstrings reference `.claude/skills/...` paths from an earlier layout. Those paths are stale for this repo — use the `skills/` (host) or `/root/.config/opencode/skills/` (image) paths above. ### Run the validator's standard test set ```bash # From project root python3 skills/pyspark-sql-guardrails/scripts/test_sql_guard.py # Or from the scripts directory cd skills/pyspark-sql-guardrails/scripts && python3 test_sql_guard.py ``` Expected output: 6 `[OK] EXPECT_OK` lines, 12 `[OK] EXPECT_RAISE: raised as expected` lines, terminated with `ALL TESTS PASSED`. Run this on **any** change to `sql_guard.py` / `validate_sql.py`. The canonical validator lives in `skills/pyspark-sql-guardrails/scripts/sql_guard.py` and exports `assert_select_or_insert(sql)` and `safe_spark_sql(spark, sql)`. Do not redefine the regex inline at call sites; import from this module. ### Validating metadata files `metadata-validator` recursively scans the current working directory for metadata files (JSON / YAML / Markdown / CSV / Excel / DDL / data dictionaries). There is no CLI — the skill is invoked conversationally. Filenames hint at table identity but **never trust the filename**; open the file and confirm the table identifier inside. ### Evals `skills/requirements-analysis/evals/evals.json` contains 3 prompt/expected-output pairs for the requirements-analysis skill. There is no test runner — the file documents expected behavior for manual or harness-driven eval. ## Skill file format Every skill is a single `SKILL.md` with YAML frontmatter (`name`, `description`) describing when to invoke the skill. New skills must follow this format; do not introduce other conventions. The first paragraph of `description` is the trigger surface — keep it concrete (input contract + trigger phrases). ## Hard rule: the validation gate `skills/pyspark-sql-guardrails/SKILL.md` enforces a non-negotiable gate: **every generated SQL string must pass through `assert_select_or_insert()` before it is shown to the user**. The pipeline ordering and the gate are independent — even if `sql-review` already passed, the SQL still has to pass through this guardrail. The forbidden-behaviors table in `skills/pyspark-sql-guardrails/SKILL.md` enumerates every rationalization for skipping the validator; treat the list as closed. ## Pipeline architecture Each skill in the pipeline has a single responsibility and refuses to do work that belongs to its neighbors: - **requirements-analysis** — clarify ambiguous business terms (time window, user definition, conversion semantics) into a DRD. Never writes SQL. Outputs `requirements_output` with status `READY_FOR_VALIDATION` only when all 7 completion items are checked. - **metadata-validator** — confirm candidate tables/fields/joins actually exist in workspace metadata. Outputs `validation_result` with `field_mapping` as the single source of truth for downstream stages. Never guesses column names — surface as `pending_question` instead. - **logic-planner** — decompose into a fixed step taxonomy (`source` / `filter` / `join` / `transform` / `dedupe` / `window` / `aggregate` / `output`). No SQL syntax, business terms only. Required reasoning: grain, metric formula, time grain, join cardinality, dedup position, null strategy. - **sql-context-builder** — freeze aliases, join keys, field sources, types, and role tags exactly once. Anything not in the SQL Context does not exist for downstream skills. - **(SQL generation)** — no dedicated skill. The pipeline composes SQL from `logic_plan.steps` in taxonomy order, using only `sql_context.fields`. Must be runnable through `safe_spark_sql` without edits. - **pyspark-sql-guardrails** — allowlist validator. Allowed: `SELECT`, `WITH ... SELECT`, `INSERT [INTO|OVERWRITE] ... SELECT`. Forbidden: `DELETE`, `TRUNCATE`, `DROP`, `ALTER`, `CREATE`, `REPLACE`, `MERGE`, `UPDATE`, `MSCK`, `REFRESH`, `ANALYZE`, `CACHE`, `UNCACHE`, `VACUUM`, `OPTIMIZE`, stacked statements, arbitrary SQL from config/YAML/env passed directly to `spark.sql`. `INSERT OVERWRITE` requires explicit user approval. - **sql-review** — static review against the SQL Context (not vibes). 10 checks; severity `HIGH`/`MEDIUM`/`LOW`; verdict `FAIL` if any `HIGH`. Reports findings — does not modify SQL. The two non-pipeline skills (`logic-planner` taxonomy details, `sql-review` check list) are deliberately stricter than typical code review because they are the only line of defense against hallucinated column names, 1:N row inflation, and silent full scans on partitioned fact tables. ## Working with this repo - **Adding a new skill** — create `skills//SKILL.md` with the YAML frontmatter and follow the same input/output/status contract used by existing skills. If the skill emits SQL, it must be downstream of `pyspark-sql-guardrails` (or use it as a library). - **Modifying `sql_guard.py`** — run `test_sql_guard.py` after every change. The 18-case test set is the contract; do not weaken it to make a SQL string pass. - **Pipeline status changes** — never collapse gates without user confirmation. The pipeline's value comes from each stage being a separate, re-runnable hand-off; inlining them couples them. - **Triggers in `description:` frontmatter** — these are what Claude Code matches against user prompts. Be specific (input contract + concrete trigger phrases). Vague triggers cause skill over-invocation. - **Stale path references** — the bundled SKILL.md / scripts use `.claude/skills/...` paths from an earlier layout. When updating validator instructions, prefer the actual paths (`skills/...` on host, `/root/.config/opencode/skills/...` in image).