diff --git a/.github/workflows/skills-ci.yml b/.github/workflows/skills-ci.yml new file mode 100644 index 0000000..54cd57b --- /dev/null +++ b/.github/workflows/skills-ci.yml @@ -0,0 +1,42 @@ +name: skills-ci + +on: + push: + paths: + - 'skills/**' + - 'scripts/**' + - 'tests/**' + - 'evals/**' + - '.release-policy.yml' + - '.github/workflows/skills-*.yml' + pull_request: + paths: + - 'skills/**' + - 'scripts/**' + - 'tests/**' + - 'evals/**' + - '.release-policy.yml' + - '.github/workflows/skills-*.yml' + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Validate skills + run: python scripts/validate_skills.py + - name: Run repository tests + run: python -m unittest discover -s tests -v + - name: Smoke discovery + run: npx --yes skills@1.5.19 add . --list diff --git a/.github/workflows/skills-release.yml b/.github/workflows/skills-release.yml new file mode 100644 index 0000000..8c4c08d --- /dev/null +++ b/.github/workflows/skills-release.yml @@ -0,0 +1,191 @@ +name: skills-release + +on: + push: + branches: [main] + paths: + - 'skills/**' + - 'scripts/**' + - '.release-policy.yml' + - '.github/workflows/skills-release.yml' + workflow_dispatch: + inputs: + version_override: + description: 'Optional explicit version tag (vX.Y.Z)' + required: false + type: string + bump_type: + description: 'Semver bump type when no version_override is set' + required: false + default: 'patch' + type: choice + options: + - patch + - minor + - major + - auto + +permissions: + contents: read + +concurrency: + group: skills-release + cancel-in-progress: false + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Validate skills + run: python scripts/validate_skills.py + + - name: Run repository tests + run: python -m unittest discover -s tests -v + + - name: Smoke discovery + run: npx --yes skills@1.5.19 add . --list + + release: + needs: verify + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Read release policy + id: policy + run: | + python - << 'PY' + import re + from pathlib import Path + + txt = Path('.release-policy.yml').read_text(encoding='utf-8') + m = re.search(r"(?im)^\s*auto_release\s*:\s*(true|false)\s*(?:#.*)?$", txt) + auto = bool(m and m.group(1).lower() == 'true') + with open(Path.cwd() / '.policy_out', 'w', encoding='utf-8') as f: + f.write(f"auto_release={'true' if auto else 'false'}\n") + PY + cat .policy_out >> "$GITHUB_OUTPUT" + + - name: Decide whether release is allowed + id: gate + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "should_release=true" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.policy.outputs.auto_release }}" = "true" ]; then + echo "should_release=true" >> "$GITHUB_OUTPUT" + else + echo "should_release=false" >> "$GITHUB_OUTPUT" + fi + + - name: Stop (policy gate) + if: steps.gate.outputs.should_release != 'true' + run: echo "Release policy disabled for push events; exiting successfully." + + - name: Determine bump from PR labels + if: steps.gate.outputs.should_release == 'true' && github.event_name == 'push' + id: prlabels + uses: actions/github-script@v7 + with: + script: | + const {owner, repo} = context.repo; + let bump = 'patch'; + try { + const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner, + repo, + commit_sha: context.sha, + }); + if (prs.data && prs.data.length > 0) { + const labels = (prs.data[0].labels || []).map(l => l.name); + if (labels.includes('release:major')) bump = 'major'; + else if (labels.includes('release:minor')) bump = 'minor'; + } + } catch (e) { + core.warning(`Could not infer PR labels: ${e.message}`); + } + core.setOutput('bump', bump); + + - name: Compute version + if: steps.gate.outputs.should_release == 'true' + id: version + env: + INPUT_BUMP: ${{ github.event.inputs.bump_type }} + INPUT_VERSION: ${{ github.event.inputs.version_override }} + PUSH_BUMP: ${{ steps.prlabels.outputs.bump }} + run: | + if [ -n "${INPUT_VERSION:-}" ]; then + VERSION=$(python scripts/compute_next_version.py --validate "$INPUT_VERSION") + else + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + BUMP="${INPUT_BUMP:-patch}" + else + BUMP="${PUSH_BUMP:-patch}" + fi + VERSION=$(python scripts/compute_next_version.py --bump "$BUMP") + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Check tag does not already exist + if: steps.gate.outputs.should_release == 'true' + run: | + if git show-ref --verify --quiet "refs/tags/${{ steps.version.outputs.version }}"; then + echo "Tag ${{ steps.version.outputs.version }} already exists" >&2 + exit 1 + fi + + - name: Determine release range + if: steps.gate.outputs.should_release == 'true' + id: range + run: | + PREV=$(git tag --list 'v*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true) + if [ -n "$PREV" ]; then + FROM="$PREV" + else + FROM=$(git hash-object -t tree /dev/null) + fi + echo "from_ref=$FROM" >> "$GITHUB_OUTPUT" + echo "to_ref=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + + - name: Build release notes + if: steps.gate.outputs.should_release == 'true' + run: | + python scripts/build_release_notes.py \ + --from-ref "${{ steps.range.outputs.from_ref }}" \ + --to-ref "${{ steps.range.outputs.to_ref }}" \ + --version "${{ steps.version.outputs.version }}" \ + --repo "${{ github.repository }}" \ + --output RELEASE_NOTES.md + + - name: Create and push tag + if: steps.gate.outputs.should_release == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag "${{ steps.version.outputs.version }}" "${GITHUB_SHA}" + git push origin "${{ steps.version.outputs.version }}" + + - name: Create GitHub release + if: steps.gate.outputs.should_release == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.version }} + name: ${{ steps.version.outputs.version }} + body_path: RELEASE_NOTES.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6cddb49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Local skill-installer outputs +.agents/ +.claude/ +.junie/ +.kiro/ +.windsurf/ +skills-lock.json + +# Python test/cache outputs +__pycache__/ +*.py[cod] +.pytest_cache/ diff --git a/.release-policy.yml b/.release-policy.yml new file mode 100644 index 0000000..574cc56 --- /dev/null +++ b/.release-policy.yml @@ -0,0 +1 @@ +auto_release: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..95ee507 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gecode + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 8b13789..4bedc61 100644 --- a/README.md +++ b/README.md @@ -1 +1,105 @@ +# Gecode Skills +Canonical skill repository for the umbrella Gecode AI agent skill. + +[Gecode](https://www.gecode.org/) 6.4.0 is the current knowledge and compatibility baseline. Repository releases use an independent semantic version because the skill can evolve between Gecode releases. + +Install with: + +```bash +npx skills add Gecode/gecode-skills +``` + +List available skills: + +```bash +npx skills add Gecode/gecode-skills --list +``` + +Install a single skill: + +```bash +npx skills add Gecode/gecode-skills --skill gecode +``` + +## Available Skill + +- `gecode` + +The skill routes internally to focused reference documents for: +- Gecode architecture and runtime semantics +- modeling and search setup +- custom propagators +- custom branchers +- memory management +- built-in search engines +- custom search engine implementation +- downstream CMake consumption + +## Versioning and distribution + +- GitHub releases are immutable snapshots of this repository, starting with `v1.0.0`. +- The standard `npx skills add Gecode/gecode-skills` command installs from the public repository's default branch. +- A new Gecode release normally causes a minor skill release when it adds or changes substantial guidance; corrections and refinements are patch releases. +- Major releases are reserved for incompatible skill structure or behavior changes. +- skills.sh discovers the public skill automatically after an installation through the `skills` CLI; there is no separate package upload. + +## Contributing + +### Verification + +Run the structural validator and repository regression tests locally: + +```bash +python scripts/validate_skills.py +python -m unittest discover -s tests -v +``` + +The CI smoke test also checks that the skill is discoverable by the skills CLI: + +```bash +npx --yes skills add . --list +``` + +### Skill structure + +The skill must be under: + +- `skills//SKILL.md` + +Optional metadata for UIs can be added at: + +- `skills//agents/openai.yaml` + +### Required frontmatter + +`SKILL.md` must include YAML frontmatter with: + +- `name` +- `description` + +The `name` must match the directory name (``). + +### Release bump labels + +Auto-release determines semver bump from PR labels: + +- `release:major` -> major bump +- `release:minor` -> minor bump +- no label -> patch bump + +## Release policy + +Releases are controlled by `.release-policy.yml`. + +- Initially: `auto_release: false` +- This means pushes to `main` do **not** auto-release. +- Manual release via workflow dispatch is enabled. + +To enable auto-release later, set: + +```yaml +auto_release: true +``` + +and merge that change with maintainer review. diff --git a/evals/gecode-trigger-evals.json b/evals/gecode-trigger-evals.json new file mode 100644 index 0000000..0fa02da --- /dev/null +++ b/evals/gecode-trigger-evals.json @@ -0,0 +1,74 @@ +[ + { + "query": "I'm implementing a cumulative-style constraint in Gecode and the decomposition is too weak. I think I need a custom propagator with better domain filtering. Can you sketch the post/propagate structure and what propagation conditions I should subscribe to?", + "should_trigger": true + }, + { + "query": "I need a custom Gecode brancher for a packing model where the second alternative should encode a symmetry-breaking exclusion. How should I structure the choice payload and commit logic so recomputation stays safe?", + "should_trigger": true + }, + { + "query": "My Gecode model solves, but BAB is crawling. Help me decide whether to stay on BAB, switch to restart-based search, or diversify with PBS. I also want to understand the completeness tradeoffs.", + "should_trigger": true + }, + { + "query": "Can you help debug a Gecode issue where a stored choice seems invalid after recomputation? The model uses custom branchers and I suspect I'm breaking clone or commit invariants.", + "should_trigger": true + }, + { + "query": "I am packaging a C++ solver that targets current Gecode main (6.4+) and I want the downstream CMake project to use find_package(Gecode CONFIG) with a FetchContent fallback when it's not installed.", + "should_trigger": true + }, + { + "query": "Please model this scheduling problem in Gecode with strong global constraints, sensible symmetry breaking, and a branching strategy that focuses on the cost-driving variables first.", + "should_trigger": true + }, + { + "query": "My Gecode search tree exploded after I added a few side constraints. I need a debugging workflow for deciding whether the problem is weak propagation, bad branching, or missing symmetry breaking.", + "should_trigger": true + }, + { + "query": "I want a cookbook-style answer for modeling a Gecode optimization problem with channeling, a couple of implied constraints, and a clean brancher order. Think recipe, not theory.", + "should_trigger": true + }, + { + "query": "How should I model this warehouse assignment problem in Gecode if each customer can be assigned to a set of depots and I care about subset and cardinality structure? I think set vars might fit better than raw ints.", + "should_trigger": true + }, + { + "query": "I'm using Gecode with a mix of discrete choices and continuous tolerances. What should I watch out for when float vars are part of the model, especially compared with ordinary integer branching intuition?", + "should_trigger": true + }, + { + "query": "I need to clean up a generic CMakeLists.txt for a small SDL app. There is no Gecode involved, I just want better target_link_libraries usage and install rules.", + "should_trigger": false + }, + { + "query": "How would you model a nurse rostering problem in constraint programming? I'm open to any solver or even OR-Tools, I mainly want high-level CP advice.", + "should_trigger": false + }, + { + "query": "I'm debugging a C++ memory leak around std::shared_ptr and custom allocators. This is ordinary application code, not a solver, and I'm not using Gecode.", + "should_trigger": false + }, + { + "query": "Can you explain branch and bound versus depth-first search in general terms? I don't need implementation details for any particular library.", + "should_trigger": false + }, + { + "query": "I have an old FindFoo.cmake module and want to migrate to package config usage. The library is our in-house SDK, not Gecode.", + "should_trigger": false + }, + { + "query": "What are good heuristics for graph coloring in CP, and how do I think about symmetry? Solver-agnostic advice is fine.", + "should_trigger": false + }, + { + "query": "My game engine is slow and I want advice on profiling frame spikes with Tracy and Instruments. This has nothing to do with constraint programming.", + "should_trigger": false + }, + { + "query": "I need help modeling a production schedule, but I'm doing it in CP-SAT and mostly want generic scheduling ideas, not Gecode-specific guidance.", + "should_trigger": false + } +] diff --git a/scripts/build_release_notes.py b/scripts/build_release_notes.py new file mode 100755 index 0000000..a9be7d4 --- /dev/null +++ b/scripts/build_release_notes.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import subprocess +from pathlib import Path + + +def changed_skills(diff_range: str, cwd: Path | None = None) -> list[str]: + out = subprocess.check_output( + ["git", "diff", "--name-only", diff_range], text=True, cwd=cwd + ) + names: set[str] = set() + for line in out.splitlines(): + parts = line.split("/") + if len(parts) >= 3 and parts[0] == "skills" and parts[1]: + names.add(parts[1]) + return sorted(names) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--from-ref", required=True) + ap.add_argument("--to-ref", required=True) + ap.add_argument("--version", required=True) + ap.add_argument("--repo", required=True, help="owner/repo") + ap.add_argument("--output", required=True) + args = ap.parse_args() + + diff_range = f"{args.from_ref}..{args.to_ref}" + skills = changed_skills(diff_range) + + lines = [] + lines.append(f"# {args.version}") + lines.append("") + if skills: + lines.append("## Changed skills") + lines.append("") + for s in skills: + lines.append(f"- `{s}`") + lines.append("") + else: + lines.append("No skill directory changes detected in this release range.") + lines.append("") + + lines.append("## Install") + lines.append("") + lines.append(f"```bash\nnpx skills add {args.repo}\n```") + lines.append("") + lines.append("List available skills:") + lines.append("") + lines.append(f"```bash\nnpx skills add {args.repo} --list\n```") + + Path(args.output).write_text("\n".join(lines) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/compute_next_version.py b/scripts/compute_next_version.py new file mode 100755 index 0000000..73970d5 --- /dev/null +++ b/scripts/compute_next_version.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +import subprocess +import sys + +TAG_RE = re.compile(r"^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$") + + +def validate_version_tag(value: str) -> str: + value = value.strip() + if not TAG_RE.fullmatch(value): + raise ValueError("version must be in form vX.Y.Z") + return value + + +def latest_tag() -> tuple[int, int, int]: + out = subprocess.check_output( + ["git", "tag", "--list", "v*", "--sort=-v:refname"], text=True + ).strip() + if not out: + return (0, 0, 0) + for tag in out.splitlines(): + m = TAG_RE.match(tag.strip()) + if m: + return tuple(int(m.group(i)) for i in (1, 2, 3)) + return (0, 0, 0) + + +def bump_from_labels(labels: list[str]) -> str: + s = set(labels) + if "release:major" in s: + return "major" + if "release:minor" in s: + return "minor" + return "patch" + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--bump", choices=["major", "minor", "patch", "auto"], default="auto") + ap.add_argument("--labels", default="") + ap.add_argument("--current", default="") + ap.add_argument("--validate", default="", metavar="VERSION") + args = ap.parse_args() + + if args.validate: + try: + print(validate_version_tag(args.validate)) + except ValueError as error: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + return 0 + + if args.current: + try: + current = validate_version_tag(args.current) + except ValueError as error: + print(f"ERROR: --current {error}", file=sys.stderr) + return 1 + m = TAG_RE.fullmatch(current) + assert m is not None + cur = (int(m.group(1)), int(m.group(2)), int(m.group(3))) + else: + cur = latest_tag() + + bump = args.bump + if bump == "auto": + labels = [x.strip() for x in args.labels.split(",") if x.strip()] + bump = bump_from_labels(labels) + + major, minor, patch = cur + if bump == "major": + nxt = (major + 1, 0, 0) + elif bump == "minor": + nxt = (major, minor + 1, 0) + else: + nxt = (major, minor, patch + 1) + + print(f"v{nxt[0]}.{nxt[1]}.{nxt[2]}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_skills.py b/scripts/validate_skills.py new file mode 100755 index 0000000..0abcd2c --- /dev/null +++ b/scripts/validate_skills.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SKILLS_DIR = REPO_ROOT / "skills" +EVALS_DIR = REPO_ROOT / "evals" +EXPECTED_SKILLS = {"gecode"} +REFERENCE_RE = re.compile(r"`references/([^`]+)`") +FRONTMATTER_FIELDS = {"name", "description"} +INTERFACE_FIELDS = {"display_name", "short_description", "default_prompt"} + + +def parse_frontmatter(skill_md: Path) -> dict[str, str]: + text = skill_md.read_text(encoding="utf-8") + lines = text.splitlines() + if not lines or lines[0].strip() != "---": + raise ValueError("missing opening frontmatter delimiter '---'") + + end = None + for i in range(1, len(lines)): + if lines[i].strip() == "---": + end = i + break + if end is None: + raise ValueError("missing closing frontmatter delimiter '---'") + + fm = {} + kv_re = re.compile(r"^([A-Za-z0-9_-]+):\s*(.*)$") + for line in lines[1:end]: + if not line.strip() or line.lstrip().startswith("#"): + continue + m = kv_re.match(line) + if not m: + continue + key, raw_val = m.group(1), m.group(2).strip() + if raw_val.startswith('"') and raw_val.endswith('"') and len(raw_val) >= 2: + raw_val = raw_val[1:-1] + if raw_val.startswith("'") and raw_val.endswith("'") and len(raw_val) >= 2: + raw_val = raw_val[1:-1] + fm[key] = raw_val + return fm + + +def validate_trigger_evals(skill_name: str, errors: list[str]) -> None: + eval_path = EVALS_DIR / f"{skill_name}-trigger-evals.json" + if not eval_path.is_file(): + errors.append(f"{skill_name}: missing trigger evals: {eval_path}") + return + + try: + cases = json.loads(eval_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as error: + errors.append(f"{eval_path}: invalid JSON: {error}") + return + + if not isinstance(cases, list) or not cases: + errors.append(f"{eval_path}: expected a non-empty JSON array") + return + + seen_queries: set[str] = set() + outcomes: set[bool] = set() + for index, case in enumerate(cases): + location = f"{eval_path}[{index}]" + if not isinstance(case, dict): + errors.append(f"{location}: expected an object") + continue + query = case.get("query") + should_trigger = case.get("should_trigger") + if not isinstance(query, str) or not query.strip(): + errors.append(f"{location}: query must be a non-empty string") + elif query in seen_queries: + errors.append(f"{location}: duplicate query") + else: + seen_queries.add(query) + if not isinstance(should_trigger, bool): + errors.append(f"{location}: should_trigger must be a boolean") + else: + outcomes.add(should_trigger) + + if outcomes != {False, True}: + errors.append(f"{eval_path}: include both triggering and non-triggering cases") + + +def main() -> int: + if not SKILLS_DIR.exists(): + print(f"ERROR: skills directory not found: {SKILLS_DIR}") + return 1 + + errors: list[str] = [] + seen_names: dict[str, Path] = {} + + skill_dirs = sorted(p for p in SKILLS_DIR.iterdir() if p.is_dir()) + actual_names = {p.name for p in skill_dirs} + missing = sorted(EXPECTED_SKILLS - actual_names) + unexpected = sorted(actual_names - EXPECTED_SKILLS) + + if missing: + errors.append(f"missing expected skills: {', '.join(missing)}") + if unexpected: + errors.append(f"unexpected skill directories: {', '.join(unexpected)}") + + for skill_dir in skill_dirs: + skill_md = skill_dir / "SKILL.md" + if not skill_md.exists(): + errors.append(f"{skill_dir}: missing SKILL.md") + continue + + try: + fm = parse_frontmatter(skill_md) + except ValueError as e: + errors.append(f"{skill_md}: {e}") + continue + + for req in ("name", "description"): + if req not in fm or not fm[req].strip(): + errors.append(f"{skill_md}: missing required frontmatter field '{req}'") + + unexpected_fields = sorted(set(fm) - FRONTMATTER_FIELDS) + if unexpected_fields: + errors.append( + f"{skill_md}: unsupported frontmatter fields: {', '.join(unexpected_fields)}" + ) + + referenced_files = set( + REFERENCE_RE.findall(skill_md.read_text(encoding="utf-8")) + ) + for reference in sorted(referenced_files): + reference_path = skill_dir / "references" / reference + if not reference_path.is_file(): + errors.append(f"{skill_md}: referenced file does not exist: {reference_path}") + + references_dir = skill_dir / "references" + actual_references = ( + {path.name for path in references_dir.glob("*.md")} + if references_dir.is_dir() + else set() + ) + orphaned_references = sorted(actual_references - referenced_files) + if orphaned_references: + errors.append( + f"{skill_md}: references not routed from SKILL.md: " + f"{', '.join(orphaned_references)}" + ) + + name = fm.get("name", "") + if name and name != skill_dir.name: + errors.append( + f"{skill_md}: frontmatter name '{name}' does not match directory '{skill_dir.name}'" + ) + + if name: + if name in seen_names: + errors.append( + f"duplicate skill name '{name}' in {skill_md} and {seen_names[name]}" + ) + else: + seen_names[name] = skill_md + + agents_dir = skill_dir / "agents" + if agents_dir.exists(): + openai_yaml = agents_dir / "openai.yaml" + if not openai_yaml.is_file(): + errors.append(f"{skill_dir}: agents/ exists but agents/openai.yaml is missing") + else: + metadata = openai_yaml.read_text(encoding="utf-8") + for field in sorted(INTERFACE_FIELDS): + if not re.search(rf"(?m)^\s{{2}}{field}:\s*\S", metadata): + errors.append(f"{openai_yaml}: missing interface field '{field}'") + + validate_trigger_evals(skill_dir.name, errors) + + if errors: + print("Skill validation failed:") + for e in errors: + print(f"- {e}") + return 1 + + print(f"Validated {len(skill_dirs)} skill successfully.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/gecode/SKILL.md b/skills/gecode/SKILL.md new file mode 100644 index 0000000..4b09cde --- /dev/null +++ b/skills/gecode/SKILL.md @@ -0,0 +1,72 @@ +--- +name: gecode +description: "Gecode architecture, modeling, cookbook-style modeling patterns, set/float/scheduling guidance, propagators, branchers, memory management, search engine usage and implementation, recomputation/cloning behavior, debugging/performance diagnosis, and downstream CMake consumption. Use for any materially Gecode-specific task: building or refining Gecode models, implementing custom constraints or branchers, tuning DFS/BAB/RBS/PBS/LDS search, diagnosing weak propagation or search pathologies, reasoning about set/float/resource-style models, or integrating Gecode into CMake projects." +--- + +# Gecode + +Use this skill as the entry point for any Gecode-specific task. Carry the universal Gecode runtime model in mind for every response, then load only the additional reference files needed for the task. + +## Always-On Mental Model +- Space is the home for variables, propagators, branchers, and optimization order. +- Propagation is explicit: call `status()`. +- Search primitives are `status()`, `choice()`, `clone()`, `commit()`, and `constrain()`. +- Space status values are `SS_FAILED`, `SS_SOLVED`, and `SS_BRANCH`. +- Choice is a space-independent descriptor; alternatives are indexed `0..n-1`. +- Choice compatibility is clone-based: a choice is valid for its source space and clones. +- `choice()` invalidates previous choices for later `commit()` on that space. +- Clone only stable, non-failed spaces. +- Branchers run in posting order. +- Recomputation can be nondeterministic with weakly monotonic propagation while remaining sound and complete. +- Model as `class M : public Space`, implement a copy constructor and virtual `copy()`, and update variable arrays with `x.update(*this, s.x)` in the model copy constructor; reserve `home` for actor `copy(Space& home)` APIs. +- After `status()==SS_BRANCH`, compute `choice()` immediately. +- Treat returned solutions as owned `Space` objects and delete seed models, choices, and solution spaces explicitly. +- Do not assume posting performs full propagation. +- Do not call the `Space` copy constructor directly instead of `clone()`. +- Do not reuse stale choices after another `choice()` call. + +## Default Gecode Heuristics +- Tighten variable domains as early as possible. +- Prefer global constraints over weak manual decompositions. +- Keep branching explicit and problem-specific rather than relying on generic defaults. +- Treat symmetry handling as first-class design work. +- Use `DFS` as the baseline complete search engine. +- Use `BAB` for optimization unless there is a concrete reason to move to restart or portfolio search. +- Treat restart, portfolio, and parallel search behavior as intentionally nondeterministic. +- Remember that clone footprint matters when designing actor state and cached data. +- Use explicit disposal discipline for external or heap-backed resources. + +## Routing +- Read `references/modeling.md` for model structure, variables, constraints, branching setup, and built-in search configuration. +- Read `references/modeling-cookbook.md` when the user needs concrete recipe-style guidance for channeling, symmetry, branching, optimization setup, or choosing globals versus decompositions. +- Read `references/debugging-workflow.md` for weak propagation, exploding search trees, stale choices, recomputation bugs, memory growth, or tracing/profiling workflow. +- Read `references/set-and-float-modeling.md` for set-variable modeling, float-specific caveats, or mixed-domain modeling. +- Read `references/scheduling-patterns.md` for cumulative/resource-style models, sequencing/order constraints, and scheduling-oriented branching or symmetry choices. +- Read `references/propagator-implementation.md` for custom propagator design, posting, propagation lifecycle, advisors, and rewriting. +- Read `references/brancher-implementation.md` for custom branchers, choices, commits, archiving, and NGL support. +- Read `references/memory-handling.md` for space/region/heap allocation, handles, clone footprint, and disposal obligations. +- Read `references/search-engines.md` for using and tuning built-in engines such as `DFS`, `BAB`, `LDS`, restart, and portfolio search. +- Read `references/search-engine-implementation.md` for custom engine orchestration, recomputation strategy, LAO, and completeness invariants. +- Read `references/cmake-consumption.md` for `find_package(Gecode CONFIG)`, target usage, version checks, and vendored fallback patterns. +- Read `references/general-knowledge.md` only for broad conceptual explanations, tracing/observability guidance, or staged model-improvement workflow discussion that goes beyond the always-on mental model. + +## Operating Rules +- Read only the reference file or files needed for the current task. +- Combine references only when the task genuinely crosses boundaries, such as a custom propagator with nontrivial memory strategy or a search-engine bug tied to choice compatibility. +- Prefer the narrowest useful reference set first, then expand if the user asks for adjacent concerns. +- Keep answers Gecode-specific. If the request is generic CMake, generic C++ memory, or generic CP theory without a real Gecode angle, do not over-apply this skill. +- Use this `SKILL.md` alone for broad explanations, initial modeling guidance, and many runtime/debugging answers before reaching for extra references. + +## Reference Index +- `references/general-knowledge.md`: advanced observability, staged improvement workflow, and broad conceptual framing beyond the always-on core. +- `references/modeling.md`: variable selection, globals, reification, symmetry, branching, and search setup in ordinary models. +- `references/modeling-cookbook.md`: concrete modeling recipes for globals, channeling, symmetry, branching, optimization, and “propagation versus search” decisions. +- `references/debugging-workflow.md`: symptom-driven diagnosis for weak models, stale choices, recomputation issues, performance pathologies, and observability tooling. +- `references/set-and-float-modeling.md`: set-variable patterns, float-specific caveats, and mixed-domain modeling reminders. +- `references/scheduling-patterns.md`: scheduling/resource modeling patterns, sequencing constraints, and search guidance for schedule-like problems. +- `references/propagator-implementation.md`: actor lifecycle, `ExecStatus`, propagation conditions, iterators, advisors, and rewrite patterns. +- `references/brancher-implementation.md`: `status`, `choice`, `commit`, archive compatibility, NGLs, and heuristic encoding. +- `references/memory-handling.md`: memory areas, lazy vs eager allocation, shared/local handles, and `AP_DISPOSE` discipline. +- `references/search-engines.md`: engine selection, restart/portfolio tradeoffs, no-goods, parallel semantics, and completeness caveats. +- `references/search-engine-implementation.md`: custom engine state, replay/recomputation, ownership, branch-and-bound integration, and invariants. +- `references/cmake-consumption.md`: package-config integration, exported targets, component selection, and fetch fallback. diff --git a/skills/gecode/agents/openai.yaml b/skills/gecode/agents/openai.yaml new file mode 100644 index 0000000..c1973fe --- /dev/null +++ b/skills/gecode/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Gecode" + short_description: "Route any Gecode task to the right internal reference" + default_prompt: "Handle this Gecode-specific task by reading only the relevant internal reference files, then provide concise, technically accurate guidance." diff --git a/skills/gecode/references/brancher-implementation.md b/skills/gecode/references/brancher-implementation.md new file mode 100644 index 0000000..73309f3 --- /dev/null +++ b/skills/gecode/references/brancher-implementation.md @@ -0,0 +1,33 @@ +# Gecode Brancher Implementation + +## Core +- Brancher is the actor that implements branching behavior. +- Implement `status`, `choice(Space&)`, `choice(const Space&, Archive&)`, `commit`, `print`, `copy`, and `dispose`. +- Choice stores only space-independent commit data. +- `commit` must work with recomputed or cloned spaces using only the choice payload. +- Choices must be archive-compatible and deterministic. +- Branchers execute in queue order of posting. +- Optional `ngl()` adds no-good support. +- `status()==false` does not imply immediate disposal; commits for earlier choices must remain valid. + +## Key Patterns +- Track the first candidate index such as `start` to avoid rescanning. +- Keep the choice payload minimal, for example `pos`, `val`, and alternative count, and archive it deterministically. +- Use binary alternatives such as `eq` versus `nq` unless an assignment brancher genuinely needs a single alternative. +- Implement an NGL class with `status`, `prune`, `subscribe`, `cancel`, `reschedule`, and `copy`; add `notice` and disposal handling when the literal owns resources. +- For complementary last alternatives, `ngl()` can return `NULL` when that is semantically valid. +- Reuse branchers through views, notably minus views for max-style variants. +- Encode the problem heuristic explicitly, such as Warnsdorff or best-fit slack. +- Mix assignment-style one-alternative choices with pruning alternatives only when the heuristic justifies it. +- Design second alternatives to embed symmetry breaking when safe. +- Pair the brancher with branch print callbacks for explainability and debugging. + +## Pitfalls +- Storing views or pointers to space state inside choice objects. +- Disposing the brancher too early when `status()` becomes false. +- Depending on mutable brancher state not encoded in the choice for `commit()`. +- Using choices after invalidation by a later `choice()` call on the same space. +- Not skipping assigned views, causing repeated same choice or an infinite tree. +- Violating recomputation invariants and commit-order assumptions. +- Using generic variable-value branching when a structure-aware heuristic is required. +- Forgetting that brancher disposal is not automatic when external resources exist. diff --git a/skills/gecode/references/cmake-consumption.md b/skills/gecode/references/cmake-consumption.md new file mode 100644 index 0000000..4a494e2 --- /dev/null +++ b/skills/gecode/references/cmake-consumption.md @@ -0,0 +1,40 @@ +# Gecode CMake Consumption + +## Core +- Prefer config-package consumption: `find_package(Gecode CONFIG REQUIRED)`. +- Link downstream targets to `Gecode::gecode` unless explicit component granularity is required. +- Current Gecode `main` is building version `6.4.0`; when targeting that line, require `Gecode_VERSION >= 6.4.0` and use package version checks rather than parsing `gecode/support/config.hpp`. If supporting older releases too, document `6.3.0` as the minimum compatibility policy separately. +- Hint package location with `Gecode_ROOT` or `CMAKE_PREFIX_PATH`. +- Keep `cmake_minimum_required(VERSION 3.21)` or newer for parity with Gecode's build. +- Use a C++17-capable compiler; current main exports that requirement through the Gecode targets. + +## Key Patterns +- Minimal integration: + ```cmake + find_package(Gecode CONFIG REQUIRED) + target_link_libraries(app PRIVATE Gecode::gecode) + ``` +- Component-specific integration: + ```cmake + find_package(Gecode CONFIG REQUIRED COMPONENTS driver) + target_link_libraries(app PRIVATE Gecode::gecodedriver) + ``` +- Version guard: + ```cmake + if(SOME_STRICT_OPTION AND Gecode_VERSION VERSION_LESS "6.4.0") + message(FATAL_ERROR "Gecode >= 6.4.0 required, found ${Gecode_VERSION}") + endif() + ``` +- Try installed package first with `find_package(Gecode CONFIG QUIET)` when discovery is optional. +- If the package is absent and project policy allows vendoring, use `FetchContent` and set Gecode cache options before `FetchContent_MakeAvailable(...)`. For a subproject, explicitly set `GECODE_INSTALL=OFF`, `GECODE_ENABLE_EXAMPLES=OFF`, and `BUILD_TESTING=OFF`; disable `GECODE_ENABLE_QT`, `GECODE_ENABLE_GIST`, or `GECODE_ENABLE_FLATZINC` when their dependencies are outside project scope. +- Require `TARGET Gecode::gecode` after resolution and fail fast with actionable error text if it is missing. +- Mark Gecode targets as `SYSTEM` in strict-warning projects to isolate third-party headers from local warning policy. +- For pinned source fallbacks, prefer stable release tags or commits over long-lived feature branches. +- When migrating away from custom discovery modules, remove manual imported-target composition and library probing once package config is the baseline. +- Keep strict version toggles if desired, but enforce them against `Gecode_VERSION`. + +## Pitfalls +- Calling `find_package(Gecode REQUIRED)` without `CONFIG` and accidentally resolving old module-mode shims. +- Assuming optional components exist without checking enabled modules in the installed build. +- Mixing custom imported-target composition with package-provided targets in the same code path. +- Parsing headers for version when package metadata already provides `Gecode_VERSION`. diff --git a/skills/gecode/references/debugging-workflow.md b/skills/gecode/references/debugging-workflow.md new file mode 100644 index 0000000..b3b3055 --- /dev/null +++ b/skills/gecode/references/debugging-workflow.md @@ -0,0 +1,41 @@ +# Gecode Debugging Workflow + +## Symptom: Search Tree Explodes +- First ask whether the model is weak or the branching is weak; the fix is often different. +- Check initial domains, missing implied constraints, and whether a stronger global can replace a weak decomposition. +- Compare nodes and failures before and after modeling changes; do not trust runtime alone. +- If propagation looks adequate but the tree is still huge, inspect branching order, value choice, and symmetry breaking. +- Use `DFS` as a baseline before judging restart or portfolio search. + +## Symptom: Branching or Recomputation Looks Wrong +- Suspect stale choices if behavior changes after another `choice()` call on the same space. +- Recheck choice compatibility: choices must be valid only for the originating space and its clones. +- Confirm `commit()` uses only archived, space-independent choice payload. +- If a bug appears only after deeper search, compare direct-clone behavior versus replayed recomputation behavior. +- When a custom brancher is involved, inspect queue order, assigned-view skipping, and brancher disposal timing. + +## Symptom: Propagation Is Wrong or Too Weak +- Separate correctness bugs from strength issues. Wrong answers or unexplained failure usually mean correctness; huge trees with valid answers usually mean weak propagation. +- Re-check subscriptions, propagation conditions, and whether the propagator actually reaches fixpoint when it should. +- Replace weak decompositions with stronger globals or a dedicated propagator when two ordinary constraints do not reason jointly enough. +- Use advisor-based localization only when the extra complexity is justified by change locality. +- If reification is involved, verify the exact semantics of the chosen decomposition rather than assuming the control literal forces a benign fallback. + +## Symptom: Memory or Clone Footprint Is Too Large +- Inspect what state is copied into every clone and move resize-heavy data away from space memory when appropriate. +- Prefer lazy construction for heavy internal state when many branches will never need it. +- Check external-resource ownership and `AP_DISPOSE` discipline before assuming the issue is search alone. +- Watch for per-choice heap allocations in hot paths, especially inside custom branchers. +- If recomputation is cheaper than cloning the current state, revisit search options and state layout together. + +## Observability Tools +- Use groups and tracing to narrow which parts of the model or search are active at the wrong time. +- Use Gist or CPProfiler when you need to see search-tree shape, branching behavior, and failure concentration rather than just counters. +- Measure nodes, failures, restarts, and memory-related symptoms alongside runtime. +- Improve in loops: baseline, strengthen propagation, improve branching, then tune search and memory strategy. + +## Escalation Paths +- For custom propagation mechanics, continue into `propagator-implementation.md`. +- For stale-choice, commit, or NGL issues, continue into `brancher-implementation.md`. +- For memory ownership and clone-footprint problems, continue into `memory-handling.md`. +- For restart, no-good, and completeness behavior, continue into `search-engines.md` or `search-engine-implementation.md` depending on whether you are using or implementing engines. diff --git a/skills/gecode/references/general-knowledge.md b/skills/gecode/references/general-knowledge.md new file mode 100644 index 0000000..f414afc --- /dev/null +++ b/skills/gecode/references/general-knowledge.md @@ -0,0 +1,11 @@ +# Gecode General Knowledge + +## Advanced Framing +- Use groups and tracing for observability and selective control. +- Improve models in loops: baseline, stronger propagation, better branching, then tuned search. +- Measure with nodes, time, and restarts, not runtime alone. +- Treat symmetry handling as a first-class design concern, not post-processing. + +## Conceptual Pitfalls +- Assuming parallel search preserves sequential solution order or runtime profile. +- Assuming one modeling pass is enough; most nontrivial case studies need staged refinement. diff --git a/skills/gecode/references/memory-handling.md b/skills/gecode/references/memory-handling.md new file mode 100644 index 0000000..d156c8d --- /dev/null +++ b/skills/gecode/references/memory-handling.md @@ -0,0 +1,37 @@ +# Gecode Memory Handling + +## Core +- Memory areas are space, region, heap, and space freelists. +- `alloc`, `realloc`, and `free` follow C++ object lifecycle semantics. +- Space memory is reclaimed automatically on space deletion and fits stable-size actor data. +- Region is a temporary arena with implicit free on region destruction. +- Heap is for frequently resized or dynamic structures. +- Search memory profile favors pristine clones, so allocation timing matters. +- Shared handles point to cross-space or cross-thread shared heap objects with reference counting. +- Local handles point to per-space shared objects copied on cloning. + +## Key Patterns +- Allocate fixed actor members in home space. +- Allocate resize-heavy buffers on the heap and free them in `dispose()`. +- Build heavy internal state lazily on first propagation when possible. +- Choose eager, lazy, or hybrid allocation based on clone footprint and expected hit rate. +- Use regions for short-lived iterators and temporary buffers. +- Call `Region::free()` at clear control-flow boundaries to maximize reuse. +- Use `SharedHandle` for immutable or global lookup data. +- Use `LocalHandle` for shared per-space mutable state. +- Use `IntSharedArray` or related shared arrays for read-only large data reused across clones. +- For brancher choices using heap buffers, pair allocation and free, and register `AP_DISPOSE`. +- Use `Region` for per-choice scratch arrays to avoid heap churn. + +## Exception Safety and Fault Injection +- Treat allocation, actor-copy, `AP_DISPOSE` registration, and partial-clone failures as normal exception paths. +- Make custom `copy()` and `dispose()` logic leave both the source and partially constructed clone recoverable when an allocation throws. +- Current Gecode main provides the CMake-only `GECODE_ENABLE_FAULT_INJECTION` option and an isolated single-threaded `check-fault` suite; use it to exercise ownership and clone-recovery paths rather than relying only on successful allocations. + +## Pitfalls +- Frequent resize in space memory causing fragmentation. +- Forgetting `home.notice(..., AP_DISPOSE)` for external or heap resources. +- Forgetting the matching `home.ignore(..., AP_DISPOSE)` in the dispose path. +- Assuming identical alignment guarantees across space, heap, and region memory. +- Leaking ownership assumptions across cloning boundaries. +- Allocating per-choice temporary arrays on the heap in hot paths. diff --git a/skills/gecode/references/modeling-cookbook.md b/skills/gecode/references/modeling-cookbook.md new file mode 100644 index 0000000..e56f69c --- /dev/null +++ b/skills/gecode/references/modeling-cookbook.md @@ -0,0 +1,36 @@ +# Gecode Modeling Cookbook + +## Strong Initial Domains +- Start with the tightest semantically correct domains you can justify; domain width is often the first-order search cost. +- Introduce helper variables only when they buy propagation, symmetry handling, or cleaner branching. +- Reuse shared arrays and tuple sets when structure repeats across many constraints. + +## Globals Versus Decompositions +- Prefer globals such as `distinct`, `count`, `binpacking`, `circuit`, or `extensional` when they capture the intended structure directly. +- Decompose only when there is no suitable global or when the decomposition is easier to maintain and the propagation loss is acceptable. +- When a decomposition is kept for simplicity, add implied constraints that recover some lost strength. + +## Channeling and Reification +- Use channeling when two views of the same decision space support different strong constraints or different branchers. +- Keep reification semantics explicit: full reification, half reification, and decomposed reification behave differently under failure. +- If Boolean control logic starts dominating the model, verify that the introduced structure is paying for its propagation cost. + +## Symmetry Templates +- Break symmetry structurally before trying heuristic tricks. +- Use anchors, ordering constraints, `precede`, or canonical placement rules to collapse equivalent solutions early. +- Re-check symmetry assumptions when mixing LDSB with static symmetry breaking or custom branchers. + +## Branching Playbooks +- Put branchers in intentional order; creation order matters. +- For optimization models, branch first on variables that drive the objective or major feasibility bottlenecks. +- When a generic brancher underperforms, ask whether the issue is value choice, variable choice, or missing model structure before jumping to a custom brancher. + +## Optimization Setup +- Use `BAB` as the default exact optimization engine. +- If best-solution search stalls, improve bounds and cost-driving propagation before assuming a different engine will rescue the model. +- Tune restart or portfolio search only after you understand the baseline `DFS` or `BAB` behavior. + +## Propagation Versus Search +- If failures come late and trees are wide, strengthen propagation first. +- If failures are early but node count is still high, the model may be fine and the branching may be poor. +- When a model already has strong globals and tight domains, branching quality often dominates the next gain. diff --git a/skills/gecode/references/modeling.md b/skills/gecode/references/modeling.md new file mode 100644 index 0000000..9ba7b55 --- /dev/null +++ b/skills/gecode/references/modeling.md @@ -0,0 +1,46 @@ +# Gecode Modeling + +## Related References +- Read `modeling-cookbook.md` for recipe-style modeling patterns and concrete construction templates. +- Read `set-and-float-modeling.md` when the task depends on set vars, float vars, or mixed-domain behavior. +- Read `scheduling-patterns.md` for cumulative/resource-style models and sequencing-heavy schedules. + +## Core +- Define the model as a `Space` subclass. +- Create typed variable arrays with tight domains early. +- Post constraints via post functions such as `rel`, `linear`, `distinct`, and the set/float variants. +- Post branching via `branch(...)`; variable and value strategy together define the tree shape. +- Use search engines such as `DFS`, `BAB`, or restart/portfolio variants according to the objective. +- MiniModel adds expression syntax via `expr(...)`, `rel(...)`, and matrix/channel helpers. +- Reified modeling supports full and half reification; decomposition semantics matter. + +## Key Patterns +- Prefer global constraints over manual decompositions when available. +- Keep branchings explicit and problem-specific for scale. +- Use multiple branchers intentionally; creation order matters. +- Tune search options such as recomputation distance, restarts, no-goods, and stop objects. +- Use tracing, Gist, or CPProfiler for diagnostics. +- Add implied constraints when semantics stay unchanged but propagation improves. +- Break symmetry structurally with order constraints, fixed anchors, `precede`, or monotone bins. +- Use LDSB only with supported branching and value configurations, and validate symmetry assumptions. +- Match propagation level to complexity; use `IPL_DOM` only where the payoff exceeds the cost. +- Replace weak decompositions with stronger globals such as `count`, `binpacking`, `circuit`, or `extensional`. +- Cache reusable heavy artifacts such as tuple sets or shared arrays keyed by shape and parameters. +- For arrays requiring non-shared variables, call `unshare(...)` once and reuse the result. +- Use branch filters and print functions for targeted branching and observability. +- When executing code between branchers, remember propagation is still explicit on recomputation paths. +- For optimization models, branch on cost-driving variables first and tie-break with objective structure. +- Current main optionally exposes counting-based search branching through `cbsbranch(...)` when built with `GECODE_ENABLE_CBS`; treat it as an optional, model-dependent alternative to generic branching. + +## Pitfalls +- Weak domains at model start causing huge trees. +- Forgetting to update every variable member in the cloning constructor. +- Assuming MiniModel nonlinear expressions stay monolithic; many decompose. +- Assuming reified non-functional decompositions imply `b=false`; they can fail instead. +- Treating Boolean variables as a subclass of integer variables. +- Ignoring exceptions from invalid arguments or overflow. +- Using domain propagation for `linear` indiscriminately when it can be exponential. +- Recomputing identical tuple sets or shared maps per post. +- Repeated implicit unsharing patterns that create unnecessary variables and propagators. +- Combining LDSB with unrelated static symmetry breaking without safety analysis. +- Leaving major value or variable symmetries unbroken. diff --git a/skills/gecode/references/propagator-implementation.md b/skills/gecode/references/propagator-implementation.md new file mode 100644 index 0000000..7c7a8f9 --- /dev/null +++ b/skills/gecode/references/propagator-implementation.md @@ -0,0 +1,35 @@ +# Gecode Propagator Implementation + +## Core +- Propagators compute on views, not model variables. +- Implement a post function and the actor lifecycle: `copy`, `dispose`, `cost`, `reschedule`, and `propagate`. +- Use `Home` for posting context and use fail/check macros. +- Return honest `ExecStatus`: `ES_FAILED`, `ES_FIX`, or `ES_NOFIX`; when a propagator is subsumed, return `home.ES_SUBSUMED(*this)` (or `home.ES_SUBSUMED_DISPOSED(...)` when the disposal size is part of the return). Never use the internal `ES_SUBSUMED_` enum value directly. +- Respect obligations around correctness, checking, contracting, monotonicity or waived monotonicity, subscription completeness, and update completeness. +- Respect implementation obligations: subsumption complete, cloning conservative, and subscription correct. +- Use standard patterns such as `Unary`, `Binary`, `Ternary`, `Nary`, or mixed variants to reduce boilerplate. + +## Key Patterns +- Do cheap pruning in `post()` and skip posting when already subsumed or failed. +- Select the weakest sound propagation condition: `*_VAL`, `*_BND`, or `*_DOM`. +- Prefer iterator-based domain operations such as `inter_r`, `narrow_r`, or `minus_r` for domain propagation. +- Use fixpoint reasoning deliberately; return `ES_FIX` only when justified. +- Use `ModEventDelta` and staging to combine cheap and expensive propagation phases. +- Use advisors for incremental change localization. +- Maintain council lifecycle correctly when advisors are present, including rescheduling and subscription completeness. +- Rewrite propagators with `GECODE_REWRITE` when state simplifies enough to switch representation. +- Use reified and rewriting patterns to remove reification overhead once control literals decide the mode. +- Template propagators on view types for reuse. +- If decomposition is propagation-weak, prefer a dedicated propagator or an extensional surrogate. +- Treat expensive support data as a cacheable object rather than recomputing it per post. + +## Pitfalls +- Modifying a view while iterating its domain iterator. +- Returning `ES_FIX` when the propagator is not actually at fixpoint. +- Returning `ES_NOFIX` when the propagator is idempotent and could finish inside the same `propagate()` call. +- Missing view updates or subscription cancellation during cloning or disposal. +- Using external resources without `AP_DISPOSE` notice/ignore discipline. +- Continuing execution after subsuming or disposing the actor. +- Failing to check modification-event failure after view updates. +- Breaking subscription completeness when using advisors or dynamic subscriptions. +- Expecting two weak propagators such as `distinct` plus `linear` to match the joint reasoning of one stronger constraint. diff --git a/skills/gecode/references/scheduling-patterns.md b/skills/gecode/references/scheduling-patterns.md new file mode 100644 index 0000000..fa1c797 --- /dev/null +++ b/skills/gecode/references/scheduling-patterns.md @@ -0,0 +1,22 @@ +# Gecode Scheduling Patterns + +## Resource-Style Models +- Start with the tightest time windows, durations, and resource bounds you can justify. +- Prefer cumulative or other structure-aware scheduling constraints over hand-built overlap decompositions when the resource semantics are standard. +- Add obvious implied constraints such as precedence, release/deadline tightening, or resource-balance bounds when they strengthen propagation without changing semantics. + +## Sequencing and Ordering +- Use explicit sequencing or order constraints when the model’s main difficulty is relative position rather than raw resource usage. +- Anchor interchangeable tasks or machines when symmetry would otherwise multiply equivalent schedules. +- When setup or transition structure matters, make it explicit in the model rather than burying it inside a weak objective. + +## Branching Guidance +- Branch first on bottleneck tasks, scarce resources, or tasks that most affect the objective. +- Prefer branchers that expose schedule structure over generic variable-value selection when the schedule has obvious critical-path or packing bottlenecks. +- If a schedule model has wide windows and weak propagation, strengthen the model before inventing a custom search strategy. + +## Common Failure Modes +- Weak overlap decompositions that fail to propagate resource pressure. +- Symmetric machines or interchangeable tasks creating huge equivalent subtrees. +- Objective-driven search without enough feasibility structure, causing the engine to explore many nearly identical schedules. +- Diagnosing schedule problems only by runtime rather than failures, nodes, and where the tree actually branches. diff --git a/skills/gecode/references/search-engine-implementation.md b/skills/gecode/references/search-engine-implementation.md new file mode 100644 index 0000000..7a5a55c --- /dev/null +++ b/skills/gecode/references/search-engine-implementation.md @@ -0,0 +1,34 @@ +# Gecode Search Engine Implementation + +## Core +- Implement engines against the `Space` interface: `status`, `choice`, `clone`, and `commit`. +- Maintain explicit ownership for all `Space*` and `Choice*` objects. +- Respect compatibility invariants: choices are valid only for clone-related spaces. +- Treat `choice()` as invalidating earlier choices on that space. +- Handle all `SpaceStatus` cases: `SS_FAILED`, `SS_SOLVED`, and `SS_BRANCH`. + +## Key Patterns +- Keep a clear split between exploration mode and recomputation mode. +- Use edge or path state to store choices, alternatives, and optional clones. +- For recomputation, replay commits from the nearest stored clone or the root clone. +- Apply LAO, the last-alternative optimization, to avoid unnecessary stored choices and commits. +- Use hybrid recomputation with a commit distance to cap replay cost. +- Use adaptive recomputation to add clones where repeated failures show that recomputation is too expensive. +- Integrate branch-and-bound by constraining future spaces against the current best solution. +- Keep restart and meta-engine hooks explicit, such as `master` and `slave`, when required. +- Wire statistics and stop-object checks consistently. +- In current main, stop objects and no-good state are copyable and parallel search uses atomic stop coordination plus completion handshakes; do not share mutable stop state non-atomically or destroy PBS workers before their completion signal. + +## Pitfalls +- Reusing stale choices after another `choice()` call. +- Mixing incompatible choices and spaces and triggering `SpaceNoBrancher`. +- Forgetting to delete choices and returned solution spaces. +- Assuming deterministic node order under parallel execution. +- Overusing no-good depth without accounting for memory and LAO tradeoffs. +- Reporting completeness when stop, cutoff, or meta-engine policy makes the run incomplete. + +## Invariants +- Recomputed spaces must follow the same decision path as the stored edge choices. +- Commit order must match original choice generation order. +- If recomputation fails due to nondeterminism or weak monotonicity effects, recover path state safely and continue search. +- Cloning and copying must never mutate model state outside the allowed operations. diff --git a/skills/gecode/references/search-engines.md b/skills/gecode/references/search-engines.md new file mode 100644 index 0000000..a27bce8 --- /dev/null +++ b/skills/gecode/references/search-engines.md @@ -0,0 +1,50 @@ +# Gecode Search Engines + +## Core +- This reference is for using built-in engines, not implementing custom engines. +- Base engines are `DFS`, `BAB`, and `LDS`. +- Meta engines are `RBS` for restart-based search and `PBS` for portfolio-based search. +- Key options live in `Search::Options`, including `threads`, `c_d`, `a_d`, `clone`, `stop`, `cutoff`, `nogoods_limit`, `assets`, `slice`, and `tracer`. +- For optimization, use `BAB`-style search with a valid model-side objective and constrain setup. + +## Key Patterns +- Use `DFS` for baseline complete search and enumeration. +- Use `BAB` for best-solution search. +- Use `LDS` when the branching heuristic is strong and discrepancy-ordered exploration is desirable. +- Use `RBS` to improve robustness through cutoffs, restart policies, and optional no-goods. +- Use `PBS` to improve robustness through asset diversification across heuristics, models, engines, or options. +- In restart search, let `master()` decide restart behavior and optional no-good posting. +- In restart search, let `slave()` signal completeness intentionally: `true` for complete slave search, `false` for deliberate incompleteness such as LNS neighborhoods. +- In portfolio search, remember `slave()` return value has no meaning. +- For restart-based best-solution assets in portfolios, add the restart builder to `SEBs`; `RBS` names the engine type, while `rbs(options)` creates the portfolio asset builder. For example: + ```cpp + Search::Options asset_options; + asset_options.cutoff = Search::Cutoff::constant(1000000); + SEBs assets(3); + assets[0] = bab