From dfa47b7161358a5d6bbe7e5f62d55e5a251703e1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:49:32 +0000 Subject: [PATCH 1/2] Match overlay fixtures after stripping a leading ramp-kit/ prefix Rules scoped to examples/**, src/diffusers/**, and tests/** missed the fork overlay clone at ramp-kit/examples/..., so the catch-the-bad demo only showed 4 of 8 findings. Strip only that overlay-clone prefix. Co-authored-by: ale93.moro --- tools/convention_check.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/tools/convention_check.py b/tools/convention_check.py index 7e6a8ed..4432410 100644 --- a/tools/convention_check.py +++ b/tools/convention_check.py @@ -101,24 +101,44 @@ def load_rules() -> list[Rule]: return rules +def _paths_for_apply(rel_path: str) -> tuple[str, ...]: + """Repo-relative paths to match against applies_to globs. + + On the library fork the overlay clone lives at ``ramp-kit/``, so a fixture + such as ``ramp-kit/examples/candidate_scheduler/scheduling_my_sde.py`` must + still match ``examples/**``. Strip only that overlay-clone prefix. Do not + rewrite any other leading segment (real library files stay exact). + """ + rel = rel_path.replace("\\", "/") + out = [rel] + prefix = "ramp-kit/" + if rel.startswith(prefix): + stripped = rel[len(prefix) :] + if stripped: + out.append(stripped) + return tuple(out) + + def rule_applies(rule: Rule, rel_path: str) -> bool: """A rule applies if any of its globs match the file's repo-relative path. Matching is deliberately precise so a rule never leaks onto files it wasn't scoped to (e.g. the kit's own tooling/tests). Two ways a glob can match: - 1. full-path fnmatch against the repo-relative path, and + 1. full-path fnmatch against the repo-relative path (and, if present, the + same path with a leading ``ramp-kit/`` overlay-clone prefix stripped), 2. a *filename-convention* glob (like `.../scheduling_*.py`) matched against just the basename — but only when that basename glob is a real pattern, never a bare `**` catch-all. """ from fnmatch import fnmatch - for pat in rule.applies_to: - if fnmatch(rel_path, pat): - return True - base = pat.split("/")[-1] - if base not in ("**", "*") and "*" in base and fnmatch(Path(rel_path).name, base): - return True + for candidate in _paths_for_apply(rel_path): + for pat in rule.applies_to: + if fnmatch(candidate, pat): + return True + base = pat.split("/")[-1] + if base not in ("**", "*") and "*" in base and fnmatch(Path(candidate).name, base): + return True return False From a4c791bd284989a1612a7094e4338d0545612547 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:49:32 +0000 Subject: [PATCH 2/2] Make scheduler scaffolds pass the library's own CI gates Templates are ruff-clean with Google-style docstrings and no leftover TEMPLATE/CHANGE_ME tokens after copy. /scaffold now registers the class in inits/dummies and invokes make style, make quality, and check_copies/ check_dummies/check_repo until they exit 0. Co-authored-by: ale93.moro --- .cursor/commands/scaffold.md | 51 ++++++++++---- docs/CURSOR_PROMPTS.md | 51 ++++++++------ overlay/OVERLAY.md | 8 ++- overlay/scaffold.md | 60 ++++++++++++---- templates/scheduler/scheduling_TEMPLATE.py | 50 ++++++++----- tests/_templates/scheduler_test.py | 21 +++--- tests/test_tooling.py | 82 +++++++++++++++++++++- tools/demo_contribute.py | 25 ++++++- 8 files changed, 268 insertions(+), 80 deletions(-) diff --git a/.cursor/commands/scaffold.md b/.cursor/commands/scaffold.md index efd0b7c..349bde2 100644 --- a/.cursor/commands/scaffold.md +++ b/.cursor/commands/scaffold.md @@ -3,10 +3,9 @@ Usage: `/scaffold ` - `$1` = component. Today the registry ships `scheduler`. Adding `model` or `pipeline` is a data change (see `templates/README.md`), not a new command. -- `$2` = PascalCase name **without** the type suffix. Example: `EulerLite` → class `EulerLiteScheduler`, file `scheduling_euler_lite.py`. +- `$2` = PascalCase name **without** the type suffix. Example: `EulerLite` → class `EulerLiteScheduler`, file `scheduling_euler_lite.py`. If those files already exist, pick a new unused `$2`. -Done is only what `conventions/rules.yaml` checks. Copy the templates. Do not -start from a blank file, from library scheduler source, or from a docs search. +Done is what `conventions/rules.yaml` checks **and**, on a real library checkout, what the library's own CI checks. Copy the templates. Do not start from a blank file, from library scheduler source, or from a docs search. ## 0. Ground in the registry @@ -24,32 +23,60 @@ Convert `$2` to snake_case (`EulerLite` → `euler_lite`). The class is `$2Sched - Test: `tests/schedulers/test_scheduling_.py` This stand-in repo uses a thin `src/diffusers/schedulers/` tree so the path -matches the real library. It is not a full diffusers checkout. +matches the real library. On the fork, that path is the real library. -## 2. Copy the template, then rename +## 2. Copy the template, then rename every placeholder - Start from `templates/scheduler/scheduling_TEMPLATE.py`. Copy it to the implementation path. - Rename `TemplateScheduler` to `$2Scheduler`. -- Keep `TODO(engineer)` in `step()`. Do not replace the placeholder. +- Keep `TODO(engineer)` in `step()`. Do not replace that placeholder. +- After the copy, the new files must not contain `TEMPLATE —`, `CHANGE_ME`, + `ChangeMeScheduler`, or `TemplateScheduler`. Add a `# Copied from` marker only + when a block is actually copied (malformed markers fail `check_copies`). ## 3. Copy the contract test (TEST001 / TEST002) Copy `tests/_templates/scheduler_test.py` to the test path. Set `TARGET` and -`CLASS` only. +`CLASS`. Replace every other template token. The test docstring names +`$2Scheduler`, not ChangeMe. -## 4. File-scoped gate +## 4. File-scoped convention gate (never `--all` on the library) ```bash python3 tools/convention_check.py src/diffusers/schedulers/scheduling_.py python3 -m unittest tests.schedulers.test_scheduling_ -v ``` -Fix every **blocking** finding. Stop at 0 blocking. Behavioral tests skip -without torch — do not edit `step()` to make them pass. +On the fork the checker lives at `ramp-kit/tools/convention_check.py`. Fix every +**blocking** finding. Stop at 0 blocking. Do not edit `step()` math to make +behavioral tests pass. -## 5. Report +## 5. On a real library checkout: register, then invoke library CI + +If this workspace has the library `Makefile` (`style` / `quality` targets): + +1. Register `$2Scheduler` alphabetically in + `src/diffusers/schedulers/__init__.py` (`_import_structure` + TYPE_CHECKING + import), `src/diffusers/__init__.py` (schedulers lazy list + TYPE_CHECKING + import), then run `python utils/check_dummies.py --fix_and_overwrite`. +2. **Invoke** the library's own tooling until each exits 0. Do not reimplement + them: + +```bash +make style +make quality +python utils/check_copies.py && python utils/check_dummies.py && python utils/check_repo.py +python3 -m unittest tests.schedulers.test_scheduling_ -v +``` + +Only open a PR when those commands pass. Do not disable inherited workflows. +Do not overwrite `AGENTS.md` or `.ai/`. + +## 6. Report - Blocking rule ids the gate checked - 0 blocking - `TODO(engineer)` still in `step()` -- Confirmation you added only those two files +- No leftover `TEMPLATE —` / `CHANGE_ME` / `ChangeMeScheduler` / `TemplateScheduler` +- On the fork: `make style` / `make quality` / check_copies / check_dummies / + check_repo exit codes diff --git a/docs/CURSOR_PROMPTS.md b/docs/CURSOR_PROMPTS.md index 7e13889..1e0f9b1 100644 --- a/docs/CURSOR_PROMPTS.md +++ b/docs/CURSOR_PROMPTS.md @@ -22,42 +22,48 @@ Do **not** enable Hub HTTP MCP. The draft PR **base must be `main`** so overlay CI (`ramp-kit-overlay` / `overlay-gate`) actually runs. Do not stack onto a `cursor/…` topic branch. -`EulerLiteScheduler` is already on fork `main`. This prompt scaffolds **HeunLite**. +`EulerLiteScheduler` and `HeunLiteScheduler` files are already on fork `main`. +This prompt scaffolds a **new unused** name (example: **PNDMLite**) and runs the +library's own CI tools until they pass. Do not overwrite existing lite files. ``` You just joined. First contribution on this huggingface/diffusers FORK (alex-16moro/diffusers). Overlay: ramp-kit/ (from alex-16moro/diffuser_agent). -Done is only what ramp-kit/conventions/rules.yaml checks. The templates already pass those checks. Copy them. Do not design a scheduler. +Copy the overlay templates. Do not design a scheduler. Keep TODO(engineer) in step(). Do not disable inherited workflows. Do not overwrite .ai/ or root AGENTS.md. If ramp-kit/ is missing: git clone --depth 1 https://github.com/alex-16moro/diffuser_agent.git ramp-kit -Scope — create exactly these two files, then stop: -- src/diffusers/schedulers/scheduling_heun_lite.py -- tests/schedulers/test_scheduling_heun_lite.py +Pick an unused PascalCase name. If scheduling_euler_lite.py / scheduling_heun_lite.py / scheduling_pndm_lite.py already exist, pick a different unused $2. Example when those three are free: PNDMLite → PNDMLiteScheduler. -Do this: +Follow ramp-kit overlay /scaffold (.cursor/commands/scaffold.md): -1. Read ramp-kit/conventions/rules.yaml. Blocking ids for this change: SCHED001, SCHED002, SCHED003, REPRO001, DEVICE001, DEPR001, MUT001, TEST001, TEST002. +1. Read ramp-kit/conventions/rules.yaml. Blocking ids: SCHED001, SCHED002, SCHED003, REPRO001, DEVICE001, DEPR001, MUT001, TEST001, TEST002. -2. Copy ramp-kit/templates/scheduler/scheduling_TEMPLATE.py → src/diffusers/schedulers/scheduling_heun_lite.py - Rename TemplateScheduler → HeunLiteScheduler. Keep TODO(engineer) in step(). +2. Copy ramp-kit/templates/scheduler/scheduling_TEMPLATE.py → src/diffusers/schedulers/scheduling_.py + Rename TemplateScheduler → $2Scheduler. Keep TODO(engineer) in step(). + After the copy, no TEMPLATE —, CHANGE_ME, ChangeMeScheduler, or TemplateScheduler may remain. -3. Copy ramp-kit/tests/_templates/scheduler_test.py → tests/schedulers/test_scheduling_heun_lite.py - Set TARGET and CLASS only. +3. Copy ramp-kit/tests/_templates/scheduler_test.py → tests/schedulers/test_scheduling_.py + Set TARGET and CLASS. Replace every other template token. -4. python3 ramp-kit/tools/convention_check.py src/diffusers/schedulers/scheduling_heun_lite.py +4. Register $2Scheduler alphabetically in src/diffusers/schedulers/__init__.py, src/diffusers/__init__.py, then: python utils/check_dummies.py --fix_and_overwrite + +5. python3 ramp-kit/tools/convention_check.py src/diffusers/schedulers/scheduling_.py Fix blocking findings only. Stop at 0 blocking. Never convention_check.py --all. -5. python3 -m unittest tests.schedulers.test_scheduling_heun_lite -v - Structural/signature must pass. Skips without torch are success — do not edit step() to make behavioral tests pass. +6. Invoke the library's own tooling until each exits 0 (do not reimplement them): + make style + make quality + python utils/check_copies.py && python utils/check_dummies.py && python utils/check_repo.py + python3 -m unittest tests.schedulers.test_scheduling_ -v + Do not edit step() math. Behavioral tests with torch must pass. -6. Draft PR on this fork, base main, those two files only. - Title: [fork demo — not for upstream] Scaffold HeunLiteScheduler contract - Never PR huggingface/diffusers. Do not overwrite .ai/ or root AGENTS.md. Do not delete inherited workflows. - Do not copy ramp-kit/examples/candidate_scheduler/. Do not touch scheduling_euler_lite.py. +7. Draft PR on this fork, base main, only after those commands pass. + Title: [fork demo — not for upstream] Add $2Scheduler scaffold + Never PR huggingface/diffusers. Do not copy ramp-kit/examples/candidate_scheduler/. -Then stop. Report the two paths, blocking rule ids, 0 blocking, leftover TODO(engineer), draft PR URL. Do not merge. +Then stop. Report paths, blocking rule ids, 0 blocking, leftover TODO(engineer), library-gate exit codes, draft PR URL. Do not merge. ``` --- @@ -153,13 +159,14 @@ the tooling. Keep it minimal; do not implement a real model. the overlay templates. Euler/DDPM source is how *we* verified the YAML, not the first-PR recipe. MCP is opt-in, not the demo. - **Catch-early:** you run this on the **kit**, not inside Prompt A. -- **Correct scaffold:** two files in `src/diffusers/schedulers/` + matching - test; file-scoped gate 0 blocking; `TODO(engineer)` still in `step()`. +- **Correct scaffold:** scheduler + matching test with no leftover placeholders; + registered in inits/dummies; file-scoped gate 0 blocking; `TODO(engineer)` + still in `step()`; library `make style` / `make quality` / check_* green. - **File-scoped gate:** never `convention_check.py --all` on the fork. - **Multi-audience:** open `projections/pm|qa|devops/` and `.github/` — same rules, different surface. Note the upstream-vs-customer split. One registry, many projections — not a capability per SDLC step. - **Fork PR hygiene:** draft, **base `main`**, `[fork demo — not for upstream]`. Overlay-gate is the customer check-run. Overlay green does not mean Hugging - Face CI is green; do not delete their workflows. Two files only; math stays TODO. + Face CI is green; do not delete their workflows. Math stays TODO. - **Maintainability:** Prompt B — one edit propagates everywhere. diff --git a/overlay/OVERLAY.md b/overlay/OVERLAY.md index 150386a..64e2a17 100644 --- a/overlay/OVERLAY.md +++ b/overlay/OVERLAY.md @@ -36,10 +36,14 @@ Hub HTTP and extra servers stay in `.cursor/mcp.optional.json`. `make demo-maintain` if you show maintainability. 2. Then launch on **this fork** (`alex-16moro/diffusers`), branch `main`. 3. Scaffold writes `src/diffusers/schedulers/scheduling_.py` here - (HeunLite if EulerLite already exists). Gate: + (a new unused name if EulerLite/HeunLite already exist). Gate: `python3 ramp-kit/tools/convention_check.py ` (never `--all`). + Register the class in `schedulers/__init__.py`, `diffusers/__init__.py`, and + dummy objects. Invoke `make style`, `make quality`, and + `python utils/check_copies.py && python utils/check_dummies.py && python utils/check_repo.py` + until they exit 0. 4. Open the PR **on this fork**, not on huggingface/diffusers, **draft, base - `main`**, those two files only. + `main`**, only after those library gates pass. Catch-early fixture: `ramp-kit/examples/candidate_scheduler` — do not copy it. diff --git a/overlay/scaffold.md b/overlay/scaffold.md index ebf3507..1e0360f 100644 --- a/overlay/scaffold.md +++ b/overlay/scaffold.md @@ -4,12 +4,14 @@ Usage: `/scaffold ` This workspace is `huggingface/diffusers` (fork). Overlay kit is `ramp-kit/` (cloned from alex-16moro/diffuser_agent). Do not overwrite `.ai/` or root `AGENTS.md`. +Do not disable or guard inherited GitHub workflows. -`$1` = component (`scheduler`). `$2` = PascalCase name without suffix (`HeunLite` -→ `HeunLiteScheduler`, `scheduling_heun_lite.py`). +`$1` = component (`scheduler`). `$2` = PascalCase name without suffix (`PNDMLite` +→ `PNDMLiteScheduler`, `scheduling_pndm_lite.py`). If `scheduling_.py` +already exists, pick a new unused `$2`. -Done is only what `ramp-kit/conventions/rules.yaml` checks. Copy the templates. -Do not start from library scheduler source and do not search docs for this PR. +Copy the overlay templates. Do not start from library scheduler source and do +not search docs for this PR. Keep `TODO(engineer)` in `step()`. ## 0. Ground in the registry @@ -22,33 +24,61 @@ read or copy `ramp-kit/examples/candidate_scheduler/`. ## 1. Paths (library layout, not the kit stand-in) -Convert `$2` to snake_case (`HeunLite` → `heun_lite`). The class is `$2Scheduler`. +Convert `$2` to snake_case (`PNDMLite` → `pndm_lite`). The class is `$2Scheduler`. - Implementation: `src/diffusers/schedulers/scheduling_.py` - Test: `tests/schedulers/test_scheduling_.py` -If `scheduling_euler_lite.py` already exists, do not overwrite it — use a new `$2`. +Do not overwrite `scheduling_euler_lite.py` or `scheduling_heun_lite.py`. -## 2. Copy from the overlay templates +## 2. Copy from the overlay templates — replace every placeholder - `ramp-kit/templates/scheduler/scheduling_TEMPLATE.py` → implementation - Rename `TemplateScheduler` → `$2Scheduler` -- Keep `TODO(engineer)` in `step()`. Do not replace the placeholder. +- Keep `TODO(engineer)` in `step()`. Do not replace that placeholder. +- After the copy, the new files must not contain `TEMPLATE —`, `CHANGE_ME`, + `ChangeMeScheduler`, or `TemplateScheduler`. +- Add a `# Copied from` marker only when a block is actually copied. ## 3. Copy the contract test (TEST001 / TEST002) -Copy `ramp-kit/tests/_templates/scheduler_test.py`. Set `TARGET` and `CLASS` -only. Do not add files or tests the template does not already contain. +Copy `ramp-kit/tests/_templates/scheduler_test.py`. Set `TARGET` and `CLASS`. +Replace every other template token so the test names `$2Scheduler`. -## 4. Gate (file-scoped — do not `--all` this library) +## 4. Register the scheduler (library public API) + +Alphabetically insert `$2Scheduler` in: + +- `src/diffusers/schedulers/__init__.py` — `_import_structure["scheduling_"]` + and the matching TYPE_CHECKING import +- `src/diffusers/__init__.py` — the torch schedulers lazy-import list and the + TYPE_CHECKING import from `.schedulers` + +Then invoke (do not hand-write dummy classes): + +```bash +python utils/check_dummies.py --fix_and_overwrite +``` + +## 5. File-scoped convention gate (never `--all`) ```bash python3 ramp-kit/tools/convention_check.py src/diffusers/schedulers/scheduling_.py python3 -m unittest tests.schedulers.test_scheduling_ -v ``` -Fix every **blocking** finding. Stop at 0 blocking. Behavioral skips without -torch are success — do not edit `step()` to make them pass. +Fix every **blocking** finding. Stop at 0 blocking. Do not edit `step()` math. + +## 6. Library CI — invoke until green, then open the PR + +Iterate until all of these exit 0. Do not reimplement them: + +```bash +make style +make quality +python utils/check_copies.py && python utils/check_dummies.py && python utils/check_repo.py +python3 -m unittest tests.schedulers.test_scheduling_ -v +``` -Do not open a PR against huggingface/diffusers — PR this fork, **draft, base -`main`**, those two files only. +Only then open a **draft** PR on this fork, **base `main`**. Never PR +huggingface/diffusers. Title prefix: `[fork demo — not for upstream]`. diff --git a/templates/scheduler/scheduling_TEMPLATE.py b/templates/scheduler/scheduling_TEMPLATE.py index e52665a..f76b654 100644 --- a/templates/scheduler/scheduling_TEMPLATE.py +++ b/templates/scheduler/scheduling_TEMPLATE.py @@ -1,7 +1,17 @@ -# TEMPLATE — the /scaffold command copies this to -# src/diffusers/schedulers/scheduling_.py and renames the class. -# It is convention-correct (passes the gate); the numerical method is left as a -# TODO because that is the engineer's actual work, not something to fabricate. +# Copyright 2026 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Optional, Tuple, Union import torch @@ -11,7 +21,7 @@ class TemplateScheduler(SchedulerMixin, ConfigMixin): - """One-line summary of the scheduler (replace TemplateScheduler with Scheduler).""" + """Discrete scheduler scaffold. Sampler math stays in `step()` as TODO(engineer).""" @register_to_config def __init__( @@ -31,13 +41,16 @@ def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.devic """Set the discrete timesteps used for the denoising loop. Args: - num_inference_steps: Number of diffusion steps used at inference. - device: Device the timesteps should be moved to. + num_inference_steps (`int`): + Number of diffusion steps used at inference. + device (`str` or `torch.device`, *optional*): + Device the timesteps tensor should be moved to. """ self.num_inference_steps = num_inference_steps step = self.config.num_train_timesteps // num_inference_steps timesteps = (torch.arange(0, num_inference_steps) * step).round().long() - timesteps = torch.flip(timesteps, dims=[0]) # torch has no [::-1] + # torch.Tensor has no [::-1]; reverse with flip (see DDPM-style spacing). + timesteps = torch.flip(timesteps, dims=[0]) self.timesteps = timesteps.to(device) if device is not None else timesteps def step( @@ -51,18 +64,21 @@ def step( """Predict the sample at the previous timestep. Args: - model_output: Direct output from the learned diffusion model. - timestep: The current discrete timestep in the diffusion chain. - sample: A current instance of a sample created by the diffusion process. - generator: A torch.Generator for reproducible sampling. - return_dict: Whether to return a SchedulerOutput or a plain tuple. + model_output (`torch.Tensor`): + Direct output from the learned diffusion model. + timestep (`int`): + Current discrete timestep in the diffusion chain. + sample (`torch.Tensor`): + Current sample. + generator (`torch.Generator`, *optional*): + PRNG for reproducible noise. Never use a global RNG. + return_dict (`bool`, *optional*, defaults to `True`): + Whether to return a [`SchedulerOutput`] or a plain tuple. Returns: - SchedulerOutput or tuple with the predicted previous sample. + [`SchedulerOutput`] or `tuple`: Predicted previous sample. """ - noise = torch.randn( - sample.shape, generator=generator, device=sample.device, dtype=sample.dtype - ) + noise = torch.randn(sample.shape, generator=generator, device=sample.device, dtype=sample.dtype) # TODO(engineer): replace this placeholder with the real update rule. prev_sample = sample - model_output + 0.0 * noise diff --git a/tests/_templates/scheduler_test.py b/tests/_templates/scheduler_test.py index 399e90f..6f7dfee 100644 --- a/tests/_templates/scheduler_test.py +++ b/tests/_templates/scheduler_test.py @@ -1,19 +1,17 @@ -"""TEMPLATE — copy to tests/schedulers/test_scheduling_.py and set the -two constants below. Runs with zero installs (structural + signature contract) -and adds behavioral checks when torch + diffusers are available. +"""Contract tests for ChangeMeScheduler. -See examples/scaffolded_scheduler + tests/schedulers/test_scheduling_ddpm_lite.py -for a filled-in example. +Structural and signature tests run with no extra installs. Behavioral checks +run when torch and diffusers are importable. """ + import ast import importlib.util import unittest from pathlib import Path -# ---- edit these two for your scheduler ------------------------------------- + TARGET = Path(__file__).resolve().parents[2] / "src" / "diffusers" / "schedulers" / "scheduling_CHANGE_ME.py" CLASS = "ChangeMeScheduler" -# ---------------------------------------------------------------------------- def _class_node(): @@ -53,8 +51,11 @@ def test_set_timesteps_signature(self): m = _method(self.node, "set_timesteps") self.assertIsNotNone(m, "set_timesteps missing") args = [a.arg for a in m.args.args] - self.assertIn("num_inference_steps", args, - "set_timesteps must accept num_inference_steps (verified against upstream source)") + self.assertIn( + "num_inference_steps", + args, + "set_timesteps must accept num_inference_steps (verified against upstream source)", + ) self.assertIn("device", args, "set_timesteps must accept device") def test_step_signature(self): @@ -90,6 +91,7 @@ def test_timesteps_count(self): def test_output_type(self): import torch + s = self.Scheduler() s.set_timesteps(10) sample = torch.zeros(1, 3, 8, 8) @@ -100,6 +102,7 @@ def test_output_type(self): def test_same_seed_same_output(self): import torch + s = self.Scheduler() s.set_timesteps(10) sample = torch.zeros(1, 3, 8, 8) diff --git a/tests/test_tooling.py b/tests/test_tooling.py index 58a0b19..270f4c4 100644 --- a/tests/test_tooling.py +++ b/tests/test_tooling.py @@ -16,6 +16,62 @@ from convention_check import check_file, load_rules # noqa: E402 +class TestRuleAppliesOverlayPrefix(unittest.TestCase): + """Path-prefixed applies_to globs must match overlay clones at ramp-kit/.""" + + REQUIRED_BLOCKING = { + "TEST001", + "SCHED001", + "SCHED002", + "SCHED003", + "DEPR001", + "MUT001", + "REPRO001", + "DEVICE001", + } + + def test_paths_for_apply_strips_only_leading_ramp_kit(self): + from convention_check import _paths_for_apply + + overlay = "ramp-kit/examples/candidate_scheduler/scheduling_my_sde.py" + self.assertEqual( + _paths_for_apply(overlay), + (overlay, "examples/candidate_scheduler/scheduling_my_sde.py"), + ) + real = "src/diffusers/schedulers/scheduling_pndm.py" + self.assertEqual(_paths_for_apply(real), (real,)) + nested = "examples/ramp-kit/scheduling_x.py" + self.assertEqual(_paths_for_apply(nested), (nested,)) + + def test_prefixed_rules_match_overlay_fixture(self): + from convention_check import load_rules, rule_applies + + rel = "ramp-kit/examples/candidate_scheduler/scheduling_my_sde.py" + ids = {r.id for r in load_rules() if rule_applies(r, rel)} + missing = self.REQUIRED_BLOCKING - ids + self.assertFalse(missing, f"overlay fixture missed {missing}") + + def test_does_not_loosen_real_repo_files(self): + from convention_check import Rule, rule_applies + + examples_only = Rule( + id="X", + title="t", + severity="block", + check="regex", + applies_to=["examples/**"], + params={}, + agent_hint="", + ) + self.assertTrue( + rule_applies(examples_only, "ramp-kit/examples/candidate_scheduler/x.py") + ) + self.assertFalse( + rule_applies(examples_only, "src/diffusers/schedulers/scheduling_pndm.py") + ) + self.assertFalse(rule_applies(examples_only, "tools/convention_check.py")) + + class TestLibraryPaths(unittest.TestCase): def test_kit_standin_or_adjacent_fork(self): from library_paths import KIT_ROOT, resolve_docs_root, resolve_library_root @@ -679,24 +735,31 @@ def _fork_prompt_a_paste(self) -> str: def test_prompt_a_stays_inside_registry_checked_files(self): paste = self._fork_prompt_a_paste() self.assertIn("conventions/rules.yaml", paste) - self.assertIn("scheduling_heun_lite.py", paste) - self.assertIn("test_scheduling_heun_lite.py", paste) self.assertIn("TODO(engineer)", paste) self.assertIn("scheduling_TEMPLATE.py", paste) + self.assertIn("check_dummies.py", paste) + self.assertIn("make style", paste) + self.assertIn("make quality", paste) + self.assertIn("check_copies.py", paste) + self.assertIn("check_repo.py", paste) self.assertNotIn("scheduling_euler_discrete.py", paste) self.assertNotIn("scheduling_ddpm.py", paste) self.assertNotIn("docs_mcp_server.py", paste) self.assertNotIn("docs/source", paste) self.assertNotIn("search_docs", paste) self.assertNotIn("grokbot_sim", paste) + self.assertIn("Never convention_check.py --all", paste) def test_overlay_scaffold_copies_templates_not_library_source(self): text = (ROOT / "overlay" / "scaffold.md").read_text() self.assertIn("rules.yaml", text) self.assertIn("scheduling_TEMPLATE.py", text) + self.assertIn("make style", text) + self.assertIn("check_dummies.py", text) self.assertNotIn("Those files are the contract", text) self.assertNotIn("docs_mcp_server.py", text) self.assertNotIn("scheduling_euler_discrete.py", text) + self.assertNotIn("convention_check.py --all", text) class TestOverlayPrGate(unittest.TestCase): @@ -780,6 +843,21 @@ def test_create_gate_clean_then_remove(self): self.assertFalse(impl.exists(), "default run must not leave contribution files") self.assertFalse(test.exists()) + def test_keep_run_has_no_template_placeholders(self): + from demo_contribute import _LEFTOVER_PLACEHOLDERS, class_name, paths_for, scaffold + + name = "PlaceholderProbe" + cls, impl, test = paths_for(name) + self.addCleanup(lambda: impl.exists() and impl.unlink()) + self.addCleanup(lambda: test.exists() and test.unlink()) + scaffold(cls, impl, test) + combined = impl.read_text() + "\n" + test.read_text() + for tok in _LEFTOVER_PLACEHOLDERS: + self.assertNotIn(tok, combined) + self.assertIn("TODO(engineer)", impl.read_text()) + self.assertIn(class_name(name), impl.read_text()) + self.assertIn(class_name(name), test.read_text()) + if __name__ == "__main__": unittest.main() diff --git a/tools/demo_contribute.py b/tools/demo_contribute.py index 44f3187..bd9ff40 100644 --- a/tools/demo_contribute.py +++ b/tools/demo_contribute.py @@ -64,12 +64,34 @@ def ground() -> None: ) +# Tokens that must not survive a scaffolded copy. TODO(engineer) stays. +_LEFTOVER_PLACEHOLDERS = ( + "TEMPLATE —", + "TEMPLATE -", + "CHANGE_ME", + "ChangeMeScheduler", + "TemplateScheduler", +) + + +def _assert_no_placeholders(path: Path) -> None: + text = path.read_text() + leftover = [tok for tok in _LEFTOVER_PLACEHOLDERS if tok in text] + if leftover: + raise SystemExit( + f"scaffold left placeholder(s) {leftover} in {path.relative_to(ROOT)}" + ) + if "TODO(engineer)" not in text and path.name.startswith("scheduling_"): + raise SystemExit(f"scaffold dropped TODO(engineer) in {path.relative_to(ROOT)}") + + def scaffold(cls: str, impl: Path, test: Path) -> None: tmpl = (ROOT / "templates" / "scheduler" / "scheduling_TEMPLATE.py").read_text() if "TemplateScheduler" not in tmpl: raise SystemExit("scheduler template missing TemplateScheduler") impl.parent.mkdir(parents=True, exist_ok=True) impl.write_text(tmpl.replace("TemplateScheduler", cls)) + _assert_no_placeholders(impl) print(f"wrote {impl.relative_to(ROOT)}") print(f" class {cls} — numerical update left as TODO(engineer)") @@ -79,8 +101,9 @@ def scaffold(cls: str, impl: Path, test: Path) -> None: ttmpl = ttmpl.replace("ChangeMeScheduler", cls) test.parent.mkdir(parents=True, exist_ok=True) test.write_text(ttmpl) + _assert_no_placeholders(test) print(f"wrote {test.relative_to(ROOT)}") - print(" TARGET + CLASS set; signature tests keep set_timesteps / step (TEST001)") + print(" TARGET + CLASS set; every template token replaced (TEST001)") def remove(impl: Path, test: Path) -> None: