Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions .claude/skills/create-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,24 @@ python3 .claude/skills/create-skill/scripts/validate-skill.py .claude/skills
Bestaande skills die nog geen CEDA-metadata dragen komen langs als `LEGACY` — dat is verwacht
en niet jouw probleem in deze PR.

## Let op: het totale aantal overlap-waarschuwingen is geen maat

De overlapcheck gooit eerst woorden weg die in meer dan `int(0.12 × aantal skills)`
descriptions voorkomen, want die dragen geen triggersignaal. Die drempel is dus
corpusafhankelijk: bij 56 skills is het `>6`, bij 59 skills `>7`. Eén skill toevoegen kan de
drempel laten verspringen, waarna woorden die eerst wegvielen weer meetellen en er
waarschuwingen bijkomen tussen paren waar jij niets mee te maken hebt.

Gemeten: 56 skills gaf 61 waarschuwingen, dezelfde collectie plus drie nieuwe skills gaf er 86
— en geen van die 25 noemde een van de nieuwe skills.

Vergelijk daarom **niet** de totalen. Kijk of jouw skillnaam voorkomt in een waarschuwing:

```bash
python3 .claude/skills/create-skill/scripts/validate-skill.py .claude/skills \
| awk '/^(OK|LEGACY|FOUT|WAARSCHUWING)/{cur=$2} /overlappende/{if($0 ~ /<naam>/) print cur": "$0}'
```

### 9. Toon de draft en wacht op akkoord

Presenteer:
Expand Down Expand Up @@ -327,8 +345,12 @@ leest de body.
python3 .claude/skills/create-skill/scripts/validate-skill.py .claude/skills/<naam>
```

exit code 0 geeft, en de collectie-brede run geen nieuwe overlap-waarschuwing oplevert die er
voor deze PR niet was.
exit code 0 geeft, en de collectie-brede run geen nieuwe overlap-waarschuwing oplevert **waarin
de nieuwe skill genoemd wordt**.

Die laatste formulering is precies bedoeld. Zie de gotcha hieronder: het totale aantal
waarschuwingen verschuift ook als jouw skill nergens bij betrokken is, dus "minder
waarschuwingen dan eerst" is geen bruikbare drempel.

## Gebundelde bestanden

Expand All @@ -348,6 +370,6 @@ voor deze PR niet was.
`scope: project` schrijf je in de projectrepo zelf.
- Genereer nooit een skill die automatisch deployt, publiceert of force-pusht zonder
expliciete bevestigingsstap in de gegenereerde skill.
- Deze skill maakt en herziet skills. Voor het *beoordelen* van een skill-PR van iemand
anders, het opruimen van duplicaten of het auditen van een externe skill bestaan nog geen
workflows — zie `docs/skill-gaps.md`, Gap 6.
- Deze skill maakt en herziet skills. Voor het *beoordelen* van een skill-PR van iemand anders
is er `review-skill`, voor het opruimen van duplicaten `dedup-skills`, en voor het nalopen
van een overgenomen skill de reference `externe-skill-audit`.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ runners. Alle drie kloppen ze niet buiten SDP. Wat er hoort te staan:
Vuistregel: kan iemand met een *vergelijkbaar maar ander* systeem deze skill per ongeluk
binnenhalen? Dan hoort vorm (b) erin, met de aannames die dan wegvallen.

**De validator toetst deze twee vormen, en alleen deze twee.** Vorm (a) telt pas als de clause
de andere skill daadwerkelijk *noemt* — in backticks of als `skill <naam>`. "Gebruik dan deze
skill" wijst naar zichzelf en verbreedt de trigger in plaats van hem te begrenzen; dat is geen
clause. Vorm (b) telt pas met een scope erachter: `niet voor X`, niet een kale "niet
gebruiken". Reden: met losse trefwoorden matchte de check ook op "buiten**wereld**" en op
"fill in the template **instead** of leaving it blank", en verzweeg hij daardoor het enige
paar in de collectie met een byte-identieke description.

## Vorm

```
Expand Down
42 changes: 42 additions & 0 deletions .claude/skills/create-skill/references/vorm-patronen.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,48 @@ moet er doorheen lezen en vindt niets.
Elk patroon verwijst naar een skill in de collectie die het al goed doet. Lees die als
voorbeeld in plaats van het patroon na te bouwen uit deze beschrijving.

## Eerst: welke vorm hoort bij welk soort falen

Voor je een patroon kiest, benoem wat er misgaat als de skill er niet is. De vorm die het ene
soort falen dichttimmert, maakt het andere aantoonbaar erger.

| Wat de agent zonder skill doet | Vorm die werkt | Vorm die averechts werkt |
|---|---|---|
| kent de regel en slaat 'm over onder druk | verbod, plus een tabel met de smoezen en het antwoord erop | zachte sturing ("overweeg", "bij voorkeur") |
| doet het wel, maar de output heeft de verkeerde vorm | recept: benoem waar de output **uit bestaat**, in volgorde | verbodenlijst ("niet samenvatten", "geen inleiding") |
| laat een verplicht onderdeel weg uit iets dat hij toch al maakt | structureel: een verplicht veld of slot in de template | een herinnering in proza bij de template |
| moet zich anders gedragen afhankelijk van de situatie | conditie op iets waarneembaars ("staat er een `SKILL.md` in de diff, dan…") | één regel met uitzonderingsclausules |

Waarom een verbod averechts werkt bij een vormprobleem: onder een concurrerende prikkel gaat
een model onderhandelen met "doe X niet". Een recept laat niets te onderhandelen over — de
output heeft de beschreven vorm of niet. Twee vervolgregels, ongeacht welke vorm je kiest:

- **Geen nuanceclausule.** "Doe X niet, tenzij het uitmaakt" heropent de onderhandeling.
Is er een echte uitzondering, schrijf die dan als eigen conditie op een waarneembaar signaal.
- **Uitzonderingen begrenzen niet.** "Deze limiet geldt niet voor codeblokken" onderdrukt de
codeblokken alsnog. Moet een deel van de output erbuiten vallen, herstructureer dan zo dat
de regel er niet bij kan.

Herkomst: `superpowers:writing-skills`, de upstream van deze skill. Daar staat het als uitkomst
van één head-to-head vergelijking, niet als wet — behandel het zo.

## Toets je formulering vóór je 'm uitrolt — met een control

Wil je weten of een formulering iets doet, dan is er één ding dat je niet mag overslaan: een
run **zonder** de guidance.

> Vertoont de control het probleem niet, dan is er niets te repareren. Schrijf de guidance
> dan niet.

Dat is de goedkoopste rem op skill-inflatie die er is, en het is precies de fout die anders
gemaakt wordt: je schrijft op wat jij denkt dat er misgaat in plaats van wat er misgaat.

Hoe: vijf of meer losse runs per variant met een verse context, de skill als systeemprompt en
een taak die tot het falen verleidt. Lees elke treffer met de hand na — geciteerde
tegenvoorbeelden en echo's van de template tellen automatisch mee als je alleen telt. En
behandel spreiding als uitkomst: vijf runs die vijf kanten op gaan betekent dat de formulering
niet bindt, en dan help je jezelf niet met méér woorden maar met een strakkere vorm.

## Kopjes die het feit noemen

**Toelatingsvraag:** is er één specifiek feit dat de lezer moet vinden terwijl hij iets
Expand Down
60 changes: 55 additions & 5 deletions .claude/skills/create-skill/scripts/validate-skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,29 @@
"skill", "skills", "claude", "ceda", "cedanl", "repo", "repos",
}

EXCLUSION_MARKERS = (
"let op", "in plaats", "niet gebruiken", "gebruik dan", "gebruik niet", "niet voor",
"instead", "not for", "do not use", "don't use", "rather than", "tenzij", "buiten",
# An exclusion-clause takes one of two documented forms (references/description-schrijven.md):
# bounding ("niet voor X") or referring ("gebruik dan `andere-skill`"). Matching the marker
# words as bare substrings recognised neither reliably: `buiten` fired inside "buitenwereld",
# "gebruik dan" fired on "gebruik dan deze skill" (which widens the trigger instead of
# bounding it), and "instead" fired on "fill in the template instead of leaving it blank".
# That mattered: it suppressed the overlap warning on the one pair in the collection whose
# descriptions are byte-identical. So the bounding form needs a scope word after it, and the
# referring form has to actually name another skill.
BOUNDING_CLAUSE = re.compile(
r"\bniet(?: te)? gebruiken (?:voor|bij|als)\b|\bniet voor\b|\bnooit voor\b|"
r"\bgebruik niet\b|\bnot for\b|\bdo(?:n't| not) use (?:for|when|this)\b",
re.IGNORECASE,
)
REFERRING_MARKER = re.compile(
r"\blet op\b|\bin plaats\b|\bgebruik(?: dan| je)?\b|\binstead\b|\brather than\b|\btenzij\b",
re.IGNORECASE,
)
# A backticked name, or "skill <naam>" — but not "deze/dit/this skill", which points at itself.
NAMES_OTHER_SKILL = re.compile(
r"`[a-z0-9][a-z0-9_-]*`|(?<!deze )(?<!dit )(?<!this )\bskill\s+[a-z0-9][a-z0-9_-]*\b",
re.IGNORECASE,
)
CLAUSE_WINDOW = 120

NO_VERIFY_MOTIVATION = re.compile(r"geen verificatie|no verification", re.IGNORECASE)

Expand Down Expand Up @@ -157,6 +176,7 @@ class Result:
warnings: list[str] = field(default_factory=list)
legacy: bool = False
description: str = ""
declared_name: str = ""

def err(self, msg: str) -> None:
self.errors.append(msg)
Expand All @@ -171,8 +191,13 @@ def trigger_tokens(description: str) -> set[str]:


def has_exclusion_clause(description: str) -> bool:
low = (description or "").lower()
return any(marker in low for marker in EXCLUSION_MARKERS)
desc = description or ""
if BOUNDING_CLAUSE.search(desc):
return True
return any(
NAMES_OTHER_SKILL.search(desc[m.start(): m.start() + CLAUSE_WINDOW])
for m in REFERRING_MARKER.finditer(desc)
)


# --------------------------------------------------------------------------- #
Expand Down Expand Up @@ -201,6 +226,7 @@ def validate_skill(skill_dir: Path) -> Result:

# --- spec: name -----------------------------------------------------------
name = fm.get("name") or ""
res.declared_name = name
res.description = fm.get("description") or ""
if not name:
res.err("`name` ontbreekt")
Expand Down Expand Up @@ -337,6 +363,29 @@ def validate_skill(skill_dir: Path) -> Result:
return res


def check_duplicate_names(results: list[Result]) -> None:
"""Two directories claiming the same `name` in their frontmatter.

The per-skill name!=directory rule catches this only by accident, and only
on the copy that was renamed. Stating it as its own error names the actual
problem: there are two artefacts competing for one identity, so `ceda-id`,
the evaluation and every cross-reference land on whichever one wins.
"""
by_declared: dict[str, list[Result]] = {}
for r in results:
if r.declared_name:
by_declared.setdefault(r.declared_name, []).append(r)
for declared, group in by_declared.items():
if len(group) < 2:
continue
dirs = ", ".join(f"`{r.name}`" for r in group)
for r in group:
r.err(
f"`name: {declared}` wordt door meerdere directories geclaimd ({dirs}) — "
"twee kopieën van één identiteit; voeg samen of deprecate, zie `dedup-skills`"
)


def check_overlap(results: list[Result]) -> None:
"""Overlapping trigger words without an exclusion-clause on either side.

Expand Down Expand Up @@ -394,6 +443,7 @@ def main(argv: list[str]) -> int:

results = [validate_skill(d) for d in dirs]
if cross:
check_duplicate_names(results)
check_overlap(results)

n_err = n_warn = n_legacy = 0
Expand Down
Loading