You are extending the system's vocabulary. A new primitive is a long-term commitment — once shipped, presets will depend on it. Your job is to ship it correctly the first time, not to ship it fast.
What was passed in
$ARGUMENTS is the primitive's category-qualified name (e.g.,
motion.createOrbit, shapes.createStar, filters.createTurbulence).
If the argument is missing or unparseable, stop and ask the calling thread
to clarify with format <category>.<name>.
Why this skill exists
Without a gatekept extension path, lib/primitives/ quickly accumulates
inconsistent signatures, undocumented functions, and primitives used by no
one. The discipline below is what makes the library compound rather than
sprawl. See D:\github_repository\svg-animation-studio\docs\extension-protocol.md
for the rationale.
Step 1 — Read the existing category
Before adding to lib/primitives/<category>.js, read the whole file. Match:
- Naming:
create<Verb>for actions (createJump),create<Noun>for static elements (createMusicalNote). - Return shape:
motion.*→{ css: string, className: string }shapes.*→ raw SVG elementstringfilters.*→<filter>elementstring
- Opts pattern: single
opts = {}object; destructure with defaults at the top; never positional args. - ID generation:
motion.*usesuniqueId(prefix)and includes the className in the keyframe name to avoid collisions.shapes.*andfilters.*acceptidfrom the caller. - Counter reset: not your concern — the preset's
compose()callsmotion.resetIdCounter()at start.
Step 2 — Write the function
Add the new function to lib/primitives/<category>.js and to its
module.exports. Include a 1-line JSDoc above the function explaining what
motion / shape / filter it produces and what the key params control. Do
not pad the JSDoc with @param lines for every option — the lib-api.md
table is the contract for that.
Example skeleton (motion):
/** Orbital sweep: an element traces a circle around a center point. */
function createOrbit(opts = {}) {
const {
duration = '4s', easing = 'linear',
radius = 20, centerX = 0, centerY = 0,
direction = 'cw'
} = opts;
const cls = uniqueId('cv-orbit');
const kf = `kf-${cls}`;
const sign = direction === 'cw' ? 1 : -1;
const css = `.${cls} {
animation: ${kf} ${duration} ${cssEasing[easing]} infinite;
transform-origin: ${centerX}px ${centerY}px;
}
@keyframes ${kf} {
0% { transform: rotate(0deg) translateX(${radius}px) rotate(0deg); }
100% { transform: rotate(${sign * 360}deg) translateX(${radius}px) rotate(${-sign * 360}deg); }
}`;
return { css, className: cls };
}
Step 3 — Use it from at least one preset
A primitive that no preset uses is unverified vocabulary. Pick an existing preset that legitimately needs the new primitive, OR — if the user is adding the primitive for a brand-new animation — make sure they're about to write that preset.
If no preset will use it within this session, stop and report:
Need primitive
<name>but no consuming preset is being built. Either wait until building one needs it, or write a placeholder preset that demonstrates the primitive (recommended: name it<name>-demo).
Step 4 — Run snapshots
npm test
Three outcomes:
- All pass → great. Move to step 5.
- A previously-passing snapshot now differs → you changed a primitive
signature used elsewhere, or the export order in
module.exportsshifted something. Re-examine your diff. If the change is intentional, runnpm run snapshot:updateAND in your final report flag every affected preset so the caller does a visual recheck. - A test errors out (not just snapshot mismatch) → you broke an import. Fix before proceeding.
Step 5 — Document
Two files MUST be updated:
docs/lib-api.md— add an H3 section under the right category with:- one-paragraph description
- opts table (option / type / default)
- the consuming preset(s)
- source line range if reverse-engineered from a seed SVG
CLAUDE.md— add a one-line entry underlib/primitives/<category>.jsin the "lib API index" section. Format:- \createOrbit` — orbital sweep around a center point`
Increment the count in CLAUDE.md (e.g., "(10)" → "(11)").
Step 6 — Report
Return a one-block report. Keep it short.
EXTENDED: motion.createOrbit
Signature: createOrbit({ duration, easing, radius, centerX, centerY, direction })
Used by: <preset slug>
Tests: <pass | updated snapshots: list>
Docs: docs/lib-api.md, CLAUDE.md
What you must NOT do
- ❌ Edit
lib/primitives/*.jsfor any reason other than this skill. - ❌ Add a primitive with no consuming preset.
- ❌ Skip the docs update — undocumented primitives become invisible to future sessions and rot quickly.
- ❌ Use positional args. The convention is one
optsobject. - ❌ Forget
module.exports— the function exists in memory but no one can use it. - ❌ Run
npm run snapshot:updatewithout explicit reason. Snapshots that silently change are bug seeds.
When the primitive doesn't fit any category
If motion, shapes, filters doesn't seem right — stop and propose a
new category. This is rare (we've seen easing cover one edge case).
Adding a fourth category is a design decision, not a routine extension.
Ask the calling thread before proceeding.