diff --git a/playground/public/llm-guide.html b/playground/public/llm-guide.html index 268635d..23c4cfe 100644 --- a/playground/public/llm-guide.html +++ b/playground/public/llm-guide.html @@ -126,6 +126,29 @@
You write Posecode, a small text language that describes one person's movement so a 3D figure can animate it.
When the request is representable, output ONLY the raw .posecode document: no Markdown fence and no prose. Before writing it, privately lock down the movement intent: lead side, moving side, every support/contact, the key pose at each phase boundary, and whether each phase accelerates, flows, or settles. Never let a cue claim a contact, direction, or body side that the commands do not encode.
If the request requires an unsupported capability—free flight, two people, arbitrary equipment, exact sign language, or detailed scapular/facial motion—do not fabricate a convincing-sounding document. Reply with one short sentence beginning Posecode cannot yet represent... and name the missing capability.
Posecode is an inspectable movement source format, not natural-language stage direction and not a physics simulator. The product parses the document in the browser, validates its closed vocabulary and configured range of motion, builds one key pose per step, interpolates between those poses, then applies declared contact solvers. The editor text remains the source of truth.
This has several consequences that must shape every generated document:
+ground-lock, reach, pin, or grip in every phase that needs it.reach, pin, grip, floor correction, and ground locking can adjust the final rendered pose after forward-kinematic joint targets are applied. Do not author contradictory FK and contact goals.clip "name", but clip names are host-provided assets and are not portable Posecode choreography. Do not emit clip unless the user or host explicitly supplies a supported clip name; phases must still fully describe the motion.When Posecode can represent the request, obey all of these rules:
+rig humanoid, one pose start, one or more steps, and one positive-integer repeat.: after every step header and after pose start only when it opens an override block. Put : after joint names, ground-lock, reach, pin, grip, turn, and travel; do not put : after cue.s, such as 0.5s or 2s. Angles are bare numbers: write 90, never 90° or 90deg.shoulder_right: extend 20, not flex -20.thing_left / thing_right names. Never invent left_knee, right-foot, leg_left, or an approximate synonym.Minimal valid shape:
+posecode exercise "Movement name"
+ rig humanoid
+ pose start = standing
+
+ step "Reach the key pose" 1s settle:
+ shoulders: flex 90
+ ground-lock: feet
+ cue "Raise both arms while both feet remain planted"
+
+ step "Return" 1s settle:
+ shoulders: flex 0
+ ground-lock: feet
+ cue "Lower both arms to the starting position"
+
+ repeat 1
posecode <kind> "<Name>" # kind = exercise | stretch | posture
rig humanoid
@@ -154,6 +177,37 @@ Contact mechanisms
Use only one of the root-solving families (ground-lock, pin, or grip) in a step. Add compatible secondary contacts with reach. For example, ground-lock: foot_right plus reach: knee_left floor is valid; adding pin: knee_left floor to that same step is a conflict and the parser rejects it.
cue is display-only coaching text. It may appear next to the current phase in a viewer, but it does not change joint targets, contacts, timing, validation, or solver behavior. Never rely on a cue to create motion.
Use a start-pose override block only when none of the built-ins is the exact opening shape. Its joint targets are ROM-clamped and layered sparsely over the built-in pose; they do not consume time or create a phase. Omitted channels keep the built-in value, and the composed pose is restored when the animation loops. Do not put contacts, cues, turn, or travel inside a start-pose block. Write exactly one pose start declaration; duplicate declarations are errors.
+Closed-vocabulary quick reference
+Do not infer new words from anatomy or English. Use only these canonical names.
+Document and timing words
+- Kinds:
exercise | stretch | posture - Rig:
humanoid - Start poses:
neutral | standing | first-position | plank | supine | prone | seated - Timing modes:
flow | settle | drive | snap | linear - Props:
chair | wall | bar | box | dip-bars
+Joint/action compatibility
+Joint names Allowed actions
+shoulders, shoulder_left, shoulder_rightflex extend abduct adduct rotate-in rotate-out
+elbows, forearms, elbow_left, elbow_rightflex extend supinate pronate
+wrists, wrist_left, wrist_rightflex extend abduct adduct
+hips, hip_left, hip_rightflex extend abduct adduct rotate-in rotate-out
+knees, knee_left, knee_rightflex extend
+ankles, ankle_left, ankle_rightdorsiflex plantarflex
+pelvishinge
+spineflex extend abduct adduct twist-left twist-right
+chestflex extend twist-left twist-right
+neck, headflex extend abduct adduct twist-left twist-right
+fingers, fingers_left, fingers_right, index_*, middle_*, ring_*, pinky_*flex extend
+thumb_left, thumb_rightflex extend abduct adduct
+
+hold neutral is valid on any recognized joint/group and resets every channel of that joint. The compatibility parser still reads older axial rotate-in/rotate-out, but new documents must use twist-left/twist-right.
+Contacts, effectors, targets, and anchors
+ground-lock accepts hands | hand_left | hand_right | forearms | elbow_left | elbow_right | feet | foot_left | foot_right | back.reach accepts grouped effectors hands | fists | forearms | knees | feet and side-specific hand_* | fist_* | elbow_* | knee_* | foot_*.pin accepts the same effectors plus pelvis.grip accepts only hands | hand_left | hand_right.- A
reach target may be floor, a declared prop anchor, or one of these body landmarks: pelvis spine chest neck head shoulder_* elbow_* wrist_* hip_* knee_* ankle_* thumb_* index_* middle_* ring_* pinky_*. - In that compact list,
* means the literal suffix left or right; never emit an asterisk in a Posecode document. - A
pin anchor may be only floor or a declared prop anchor. Never pin to a body landmark; it moves with the same body root.
+Prop declarations expose only these anchors:
+Declaration Available anchors Typical use
+prop chairseatsit, touch, or pin to the seat
+prop wallwallreach or pin to the wall surface
+prop barbar, bar_left, bar_rightoverhead hand contact
+prop boxboxstep-up foot contact
+prop dip-barsbars, bars_left, bars_rightrail support
+
+For grouped grips use the bare paired anchor: grip: hands bar or grip: hands bars. For one hand, use the matching side on both effector and anchor, such as grip: hand_left bar_left.
Joints
neck head spine chest pelvis and (singular or plural) shoulders elbows forearms wrists hips knees ankles. forearms is an anatomical alias for the two elbow bones when authoring palm roll. Plural names move both sides symmetrically; use elbow_left etc. for one side. Fingers: fingers (or fingers_left / fingers_right), and individually thumb_* index_* middle_* ring_* pinky_*.
Actions (degrees are absolute targets)
@@ -163,6 +217,27 @@ Rules
- Break the movement into 2–6 concurrent phases; each
step is the time taken to arrive at one key pose. A duration is not a dwell. - Set only the channels that change. Unset channels hold their previous value;
hold neutral deliberately resets the whole joint. - Stay within Posecode's configured range-of-motion bounds (for example, knee flex ≤ 144°, elbow flex ≤ 154°, shoulder flex ≤ 180°). These bounds constrain the rig; they do not certify a movement as safe. Do not stack a pelvis hinge and hip flexion past the hip's combined limit.
- Choose one lead/trail convention and keep it across joint targets, contacts, phase names, and cues. Mirror all four together when switching sides.
- Declare every load-bearing contact in every phase where it remains active. Contacts do not inherit. Use side-specific support for lunges, kneeling, and single-leg work;
ground-lock: feet is for two genuinely planted feet. Ground contacts are a closed vocabulary: use ground-lock: hands, feet in a high plank, ground-lock: forearms, feet in a forearm plank, and ground-lock: back for supine floor work. Do not invent contact names. - Use
ground-lock only for floor supports already meant to stay planted. Use reach to move a limb to a target, pin when a single contact must move the whole body, and grip for a bar or rails. Never combine ground-lock, pin, or grip root solvers in one step; use one primary support plus per-limb reach constraints for the remaining contacts. - Author the gross body position before adding
reach. A hand cannot reach a floor that the torso/legs leave outside the arm's reachable workspace. - Derive each cue from the actual commands. Remove phrases such as “foot forward,” “knee down,” “fist planted,” or “arm overhead” unless that exact side and constraint are encoded.
- Use
linear only for an unchanged dwell or intentionally mechanical motion. A moving phase that arrives at a landing or hold should normally settle. - Add an explicit unchanged step for a visible hold, repeating its active contacts. Then author a controlled recovery when the movement should loop.
- Declare a prop before using its anchor; never invent target names.
- Set
repeat to the requested repetition count.
Before returning the document, privately run this final check:
- Every word comes from the closed vocabulary, and every action is compatible with its joint.
- Each cue can be traced to a joint target or an active contact on the same body side.
- Every required support is declared in every phase where it remains active.
- The gross pose makes each reach plausible; a reach line is not permission to leave the target outside the limb's workspace.
- The final hold and recovery are explicit, warning-free, and do not rely on an undeclared prop or unsupported physics.
+Draft, validate, and repair privately
+Use this internal sequence before returning any document:
+- Choose the representation. Confirm it is one person, kinematic, floor-bound, and uses only supported props and coarse hand articulation.
- Write the support ledger. For every phase, list which surfaces carry weight and which new endpoint must arrive at a target. This determines
ground-lock vs reach vs pin vs grip before joint angles are written. - Write key poses. Describe the opening pose and each phase endpoint in plain language privately. Keep lead/trail sides fixed.
- Translate to absolute channels. Add only compatible joint/action pairs, using positive in-range magnitudes and explicit zero/neutral recovery.
- Check inheritance. Verify every omitted joint is meant to hold its last value; verify every still-active contact is repeated.
- Check cues against execution. Delete any cue claim that is not encoded by a joint, root directive, or contact in that phase.
- Simulate the loop. Compare the last key pose with the composed start pose. Add a recovery when an automatic reset would pop.
- Lint the text. Check colons, quotes, indentation,
s duration suffixes, canonical names, declared prop anchors, one primary root solver per phase, and a positive integer repeat.
+Common invalid output and the exact repair
+Invalid or misleading output Repair
+`step "Lift" 1 settle:` Add the duration suffix: `1s`.
+`cue: "Keep the foot down"` Remove the colon and encode the contact: `ground-lock: foot_left plus cue "..."`.
+`knees: bend 90` Use the closed action: `knees: flex 90`.
+`knee: flex 90 or left_knee: flex 90` Use `knees or knee_left`.
+`shoulder_right: flex -20` Use the directional action: `shoulder_right: extend 20`.
+`ankles: flex 20` Use `dorsiflex or plantarflex` within ankle ROM.
+`pelvis: flex 60` Use `pelvis: hinge 60 for a hip hinge, or pose the hips` for thigh flexion.
+`ground-lock: knee_left` Knees are not ground-lock contacts; use `reach: knee_left floor while approaching, then pin: knee_left floor` when it carries the body.
+`reach: pelvis floor` pelvis is not a reach effector; use `pin: pelvis floor`.
+`pin: hand_left knee_right` A body landmark is not a fixed anchor; use `reach: hand_left knee_right`.
+`grip: hands bar` without a prop Add top-level `prop bar`.
+`ground-lock: feet and pin: knee_left floor` in one step Keep one root solver; use one primary support plus compatible reach constraints.
+A hold expressed only as `cue "Hold"` Add a separate unchanged linear step and repeat its contacts.
+A cue says “right” while targets use _left Make the phase name, targets, contacts, and cue use the same side.
+A final bent pose followed by looping Add an explicit recovery to the start pose.
+
Timing modes
flow: carry momentum through an interior pose; use for continuous dance, locomotion, and multi-part sports motion.settle: decelerate into a real rest; use at a squat bottom, landing, hold, or final pose.drive: accelerate from rest; use for a jump, push, lift, or recoil.snap: arrive quickly and stop sharply; use for a strike or release.linear: constant timing; best for an unchanged dwell or mechanical motion.
The older names ease-in, ease-out, and ease-in-out still parse for compatibility, but do not author new documents with them.
diff --git a/playground/src/direct-manipulation.ts b/playground/src/direct-manipulation.ts
index 503c6dd..1971c13 100644
--- a/playground/src/direct-manipulation.ts
+++ b/playground/src/direct-manipulation.ts
@@ -27,6 +27,16 @@ export interface AngleRange {
max: number;
}
+interface SourceLineRange {
+ from: number;
+ to: number;
+}
+
+interface PreviewSegment {
+ start: number;
+ end: number;
+}
+
// Keep this deliberately stricter than syntax highlighting. Only complete,
// parser-valid joint target lines become controls; comments, turn/travel
// numbers, and half-written source remain ordinary editable text.
@@ -107,3 +117,24 @@ export function normalizeAngle(value: number, range: AngleRange): string {
const clamped = Math.min(range.max, Math.max(range.min, value));
return String(Math.round(clamped * 10) / 10);
}
+
+/**
+ * Resolve a directly edited source line to the key pose it controls. Joint
+ * targets in a start-pose override preview at time zero; targets in a step
+ * preview just inside that phase's endpoint so the looping sampler cannot wrap.
+ */
+export function previewTimeForLine(
+ line: number,
+ phaseRanges: readonly SourceLineRange[],
+ segments: readonly PreviewSegment[],
+): number | null {
+ const firstPhase = phaseRanges[0];
+ if (firstPhase && line < firstPhase.from) return 0;
+
+ const phaseIndex = phaseRanges.findIndex(
+ (range) => line >= range.from && line <= range.to,
+ );
+ const segment = phaseIndex < 0 ? undefined : segments[phaseIndex];
+ if (!segment) return null;
+ return Math.max(segment.start, segment.end - 1e-3);
+}
diff --git a/playground/src/editor.ts b/playground/src/editor.ts
index 8acba5f..7c120ee 100644
--- a/playground/src/editor.ts
+++ b/playground/src/editor.ts
@@ -607,7 +607,11 @@ export interface PosecodeEditor {
export interface PosecodeEditorOptions {
doc: string;
- onChange: (value: string, userInitiated: boolean) => void;
+ onChange: (
+ value: string,
+ userInitiated: boolean,
+ context?: { previewLine: number },
+ ) => void;
onJointSelect?: (joint: string | null, boneIds: readonly string[]) => void;
}
@@ -697,7 +701,25 @@ export function createPosecodeEditor(
(transaction) =>
transaction.annotation(Transaction.userEvent) !== undefined,
);
- opts.onChange(u.state.doc.toString(), userInitiated);
+ // Spinner edits are different from ordinary source typing: the
+ // author is manipulating one key pose and expects to see that pose
+ // immediately. Pass its resulting source line to the playground;
+ // main.ts will seek there after rebuilding the timeline.
+ const directAngleEdit = u.transactions.some((transaction) =>
+ transaction.effects.some(
+ (effect) => effect.is(setActiveAngle) && effect.value !== null,
+ ),
+ );
+ const activeAngle = directAngleEdit
+ ? u.state.field(activeAngleField)
+ : null;
+ opts.onChange(
+ u.state.doc.toString(),
+ userInitiated,
+ activeAngle
+ ? { previewLine: u.state.doc.lineAt(activeAngle.angleFrom).number }
+ : undefined,
+ );
}
}),
],
diff --git a/playground/src/main.ts b/playground/src/main.ts
index 27e8092..93eeab8 100644
--- a/playground/src/main.ts
+++ b/playground/src/main.ts
@@ -28,6 +28,7 @@ import type { PosecodeEditor } from "./editor.js";
import { ANIMATION_PROGRESS_MESSAGE, PRESETS } from "./presets.js";
import { prioritizeFeaturedMovement } from "./library-order.js";
import { SHOWCASE_CLIPS } from "./clips.js";
+import { previewTimeForLine } from "./direct-manipulation.js";
// During source-only typechecks the playground resolves posecode-render's last
// built declaration bundle. Keep the local extension explicit until the normal
@@ -98,6 +99,7 @@ let scrubDiagnosticsRefresh = 0;
let documentRevision = 1;
let pendingRenderTrigger: RenderTrigger = "initial";
let selectedBoneIds: readonly string[] = [];
+let pendingPreviewLine: number | null = null;
/** Keep the source selection and its live 3D joint markers in sync. */
function handleJointSelect(
@@ -295,13 +297,18 @@ function computePhaseRanges(
}
let debounce = 0;
-function scheduleRecompile(): void {
+function scheduleRecompile(previewLine?: number): void {
window.clearTimeout(debounce);
+ pendingPreviewLine = previewLine ?? null;
debounce = window.setTimeout(recompile, 250);
}
/** Keep the address bar and library label in sync with editor changes. */
-function handleEditorChange(source: string, userInitiated: boolean): void {
+function handleEditorChange(
+ source: string,
+ userInitiated: boolean,
+ context?: { previewLine: number },
+): void {
const editedDocumentKind = documentKind();
const preset = PRESETS.find((p) => p.source === source);
currentPresetId = preset?.id ?? null;
@@ -316,7 +323,7 @@ function handleEditorChange(source: string, userInitiated: boolean): void {
source.trim() ? "Custom movement" : "New movement",
);
history.replaceState(null, "", buildNicePlayPath(source));
- scheduleRecompile();
+ scheduleRecompile(context?.previewLine);
}
function recompile(): void {
@@ -352,8 +359,6 @@ function recompile(): void {
);
viewer.setLoop(loop.checked);
viewer.setSpeed(Number(speed.value));
- viewer.play();
- setPlaying(true);
const tl = viewer.getTimeline();
repeat = tl?.repeat ?? 1;
rep = 1;
@@ -364,6 +369,26 @@ function recompile(): void {
tl?.segments.length ?? 0,
);
ed.highlightPhase(null); // next onPhase paints the active block
+
+ const previewTime =
+ pendingPreviewLine !== null && tl
+ ? previewTimeForLine(pendingPreviewLine, phaseRanges, tl.segments)
+ : null;
+ pendingPreviewLine = null;
+ if (previewTime !== null && tl) {
+ // Direct manipulation is a pose inspection workflow: hold the affected
+ // keyframe so even a fast phase visibly responds to a one-degree edit.
+ viewer.seek(previewTime);
+ viewer.pause();
+ setPlaying(false);
+ scrub.value = String(Math.round((previewTime / (tl.duration || 1)) * 1000));
+ paintScrub();
+ clock.textContent = `${previewTime.toFixed(1)}s`;
+ scheduleScrubDiagnosticsRefresh();
+ } else {
+ viewer.play();
+ setPlaying(true);
+ }
}
}
diff --git a/playground/test/direct-manipulation.test.ts b/playground/test/direct-manipulation.test.ts
index 40930d8..e16e308 100644
--- a/playground/test/direct-manipulation.test.ts
+++ b/playground/test/direct-manipulation.test.ts
@@ -4,6 +4,7 @@ import {
angleTargetAt,
findAngleTargets,
normalizeAngle,
+ previewTimeForLine,
} from "../src/direct-manipulation.js";
describe("direct angle manipulation", () => {
@@ -51,4 +52,20 @@ describe("direct angle manipulation", () => {
expect(normalizeAngle(80.06, range)).toBe("80.1");
expect(normalizeAngle(999, range)).toBe("154");
});
+
+ it("previews the endpoint of the phase containing a direct angle edit", () => {
+ const ranges = [
+ { from: 5, to: 9 },
+ { from: 11, to: 15 },
+ ];
+ const segments = [
+ { start: 0, end: 0.5 },
+ { start: 0.5, end: 0.85 },
+ ];
+
+ expect(previewTimeForLine(7, ranges, segments)).toBeCloseTo(0.499);
+ expect(previewTimeForLine(13, ranges, segments)).toBeCloseTo(0.849);
+ expect(previewTimeForLine(3, ranges, segments)).toBe(0);
+ expect(previewTimeForLine(20, ranges, segments)).toBeNull();
+ });
});
diff --git a/scripts/documentation-contract.test.ts b/scripts/documentation-contract.test.ts
index 5089058..2b04e57 100644
--- a/scripts/documentation-contract.test.ts
+++ b/scripts/documentation-contract.test.ts
@@ -8,6 +8,7 @@ import {
PROP_TYPES,
RIG_NAMES,
START_POSE_NAMES,
+ parse,
} from "../packages/posecode-parser/src/index.js";
const specification = readFileSync(resolve(import.meta.dirname, "../spec/SPEC.md"), "utf8");
@@ -26,6 +27,34 @@ const closedVocabulary = [
];
describe("authoring documentation contract", () => {
+ it("keeps every Posecode example in the LLM guide parseable and warning-free", () => {
+ const fences = [...authoringGuide.matchAll(/^([ \t]*)```posecode[ \t]*\n([\s\S]*?)^\1```[ \t]*$/gm)];
+ expect(fences.length).toBeGreaterThan(0);
+
+ for (const [index, fence] of fences.entries()) {
+ const indent = fence[1] ?? "";
+ const source = (fence[2] ?? "")
+ .split("\n")
+ .map((line) => line.startsWith(indent) ? line.slice(indent.length) : line)
+ .join("\n");
+ const documentSource = source.trimStart().startsWith("posecode ")
+ ? source
+ : [
+ 'posecode posture "Guide snippet"',
+ " rig humanoid",
+ " pose start = standing",
+ "",
+ ...source.split("\n").map((line) => ` ${line}`),
+ "",
+ " repeat 1",
+ ].join("\n");
+ const { ir, errors, warnings } = parse(documentSource);
+ expect({ example: index + 1, errors }).toEqual({ example: index + 1, errors: [] });
+ expect({ example: index + 1, warnings }).toEqual({ example: index + 1, warnings: [] });
+ expect(ir).not.toBeNull();
+ }
+ });
+
it.each([
["the normative specification", specification],
["the pasteable LLM guide", authoringGuide],
diff --git a/spec/llm-authoring.md b/spec/llm-authoring.md
index 509ae77..3eed263 100644
--- a/spec/llm-authoring.md
+++ b/spec/llm-authoring.md
@@ -26,6 +26,84 @@ arbitrary equipment, exact sign language, or detailed scapular/facial motion—d
not fabricate a convincing-sounding document. Reply with one short sentence
beginning `Posecode cannot yet represent...` and name the missing capability.
+## Product mental model
+
+Posecode is an inspectable movement source format, not natural-language stage
+direction and not a physics simulator. The product parses the document in the
+browser, validates its closed vocabulary and configured range of motion, builds
+one key pose per `step`, interpolates between those poses, then applies declared
+contact solvers. The editor text remains the source of truth.
+
+This has several consequences that must shape every generated document:
+
+- A joint number is the **absolute target at the end of that step**, measured
+ from the rig's reference pose. It is not a relative “add 20 degrees” command.
+- All targets inside one step happen concurrently during that step. Source line
+ order inside the step is not a choreography sequence.
+- Omitted joint channels carry forward. Contacts do not: repeat every active
+ `ground-lock`, `reach`, `pin`, or `grip` in every phase that needs it.
+- A step duration is travel time to the next key pose, not time spent holding
+ it. A visible hold is a separate unchanged step.
+- `reach`, `pin`, `grip`, floor correction, and ground locking can adjust the
+ final rendered pose after forward-kinematic joint targets are applied. Do not
+ author contradictory FK and contact goals.
+- An out-of-range angle is clamped and produces a warning. Two different bad
+ requests can therefore render identically if both clamp to the same limit.
+- A fatal syntax/validation error produces no new movement IR, so a live viewer
+ may keep showing the last valid document. “The viewer did not change” can
+ mean the new source is invalid; generate source that passes diagnostics.
+- A document loops from its last phase back to its composed start pose. Author
+ an intentional recovery when that reset should not be visible.
+- A host may support `clip "name"`, but clip names are host-provided assets and
+ are not portable Posecode choreography. Do not emit `clip` unless the user or
+ host explicitly supplies a supported clip name; phases must still fully
+ describe the motion.
+
+## Non-negotiable output contract
+
+When Posecode can represent the request, obey all of these rules:
+
+1. Return raw Posecode only. Do not add a Markdown fence, explanation,
+ validation report, title outside the document, or trailing notes.
+2. Emit exactly one header, one `rig humanoid`, one `pose start`, one or more
+ steps, and one positive-integer `repeat`.
+3. Use a quoted, non-empty document name and quoted, non-empty step names.
+4. Put `:` after every `step` header and after `pose start` only when it opens an
+ override block. Put `:` after joint names, `ground-lock`, `reach`, `pin`,
+ `grip`, `turn`, and `travel`; do **not** put `:` after `cue`.
+5. Give every duration a positive number followed immediately by `s`, such as
+ `0.5s` or `2s`. Angles are bare numbers: write `90`, never `90°` or `90deg`.
+6. Use non-negative magnitudes with the correct directional action. Write
+ `shoulder_right: extend 20`, not `flex -20`.
+7. Put one directive on each line. Do not combine joint targets with commas,
+ semicolons, braces, bullets, JSON, YAML, or prose.
+8. Use canonical `thing_left` / `thing_right` names. Never invent `left_knee`,
+ `right-foot`, `leg_left`, or an approximate synonym.
+9. Use two-space indentation for top-level declarations and four spaces for
+ children of a step or start-pose override, following the template exactly.
+10. Prefer 2–6 meaningful phases. Add more only when each new phase creates a
+ distinct key pose, contact change, direction change, or deliberate hold.
+
+Minimal valid shape:
+
+```posecode
+posecode exercise "Movement name"
+ rig humanoid
+ pose start = standing
+
+ step "Reach the key pose" 1s settle:
+ shoulders: flex 90
+ ground-lock: feet
+ cue "Raise both arms while both feet remain planted"
+
+ step "Return" 1s settle:
+ shoulders: flex 0
+ ground-lock: feet
+ cue "Lower both arms to the starting position"
+
+ repeat 1
+```
+
## Grammar
```
@@ -74,6 +152,69 @@ the built-in value, and the composed pose is restored when the animation loops.
Do not put contacts, cues, turn, or travel inside a start-pose block.
Write exactly one `pose start` declaration; duplicate declarations are errors.
+## Closed-vocabulary quick reference
+
+Do not infer new words from anatomy or English. Use only these canonical names.
+
+### Document and timing words
+
+- Kinds: `exercise | stretch | posture`
+- Rig: `humanoid`
+- Start poses: `neutral | standing | first-position | plank | supine | prone | seated`
+- Timing modes: `flow | settle | drive | snap | linear`
+- Props: `chair | wall | bar | box | dip-bars`
+
+### Joint/action compatibility
+
+| Joint names | Allowed actions |
+| --- | --- |
+| `shoulders`, `shoulder_left`, `shoulder_right` | `flex extend abduct adduct rotate-in rotate-out` |
+| `elbows`, `forearms`, `elbow_left`, `elbow_right` | `flex extend supinate pronate` |
+| `wrists`, `wrist_left`, `wrist_right` | `flex extend abduct adduct` |
+| `hips`, `hip_left`, `hip_right` | `flex extend abduct adduct rotate-in rotate-out` |
+| `knees`, `knee_left`, `knee_right` | `flex extend` |
+| `ankles`, `ankle_left`, `ankle_right` | `dorsiflex plantarflex` |
+| `pelvis` | `hinge` |
+| `spine` | `flex extend abduct adduct twist-left twist-right` |
+| `chest` | `flex extend twist-left twist-right` |
+| `neck`, `head` | `flex extend abduct adduct twist-left twist-right` |
+| `fingers`, `fingers_left`, `fingers_right`, `index_*`, `middle_*`, `ring_*`, `pinky_*` | `flex extend` |
+| `thumb_left`, `thumb_right` | `flex extend abduct adduct` |
+
+`hold neutral` is valid on any recognized joint/group and resets every channel
+of that joint. The compatibility parser still reads older axial
+`rotate-in`/`rotate-out`, but new documents must use `twist-left`/`twist-right`.
+
+### Contacts, effectors, targets, and anchors
+
+- `ground-lock` accepts `hands | hand_left | hand_right | forearms |
+ elbow_left | elbow_right | feet | foot_left | foot_right | back`.
+- `reach` accepts grouped effectors `hands | fists | forearms | knees | feet`
+ and side-specific `hand_* | fist_* | elbow_* | knee_* | foot_*`.
+- `pin` accepts the same effectors plus `pelvis`.
+- `grip` accepts only `hands | hand_left | hand_right`.
+- A `reach` target may be `floor`, a declared prop anchor, or one of these body
+ landmarks: `pelvis spine chest neck head shoulder_* elbow_* wrist_* hip_*
+ knee_* ankle_* thumb_* index_* middle_* ring_* pinky_*`.
+- In that compact list, `*` means the literal suffix `left` or `right`; never
+ emit an asterisk in a Posecode document.
+- A `pin` anchor may be only `floor` or a declared prop anchor. Never pin to a
+ body landmark; it moves with the same body root.
+
+Prop declarations expose only these anchors:
+
+| Declaration | Available anchors | Typical use |
+| --- | --- | --- |
+| `prop chair` | `seat` | sit, touch, or pin to the seat |
+| `prop wall` | `wall` | reach or pin to the wall surface |
+| `prop bar` | `bar`, `bar_left`, `bar_right` | overhead hand contact |
+| `prop box` | `box` | step-up foot contact |
+| `prop dip-bars` | `bars`, `bars_left`, `bars_right` | rail support |
+
+For grouped grips use the bare paired anchor: `grip: hands bar` or
+`grip: hands bars`. For one hand, use the matching side on both effector and
+anchor, such as `grip: hand_left bar_left`.
+
## Joints
`neck head spine chest pelvis` and (singular or plural) `shoulders elbows
@@ -153,6 +294,49 @@ Before returning the document, privately run this final check:
- The final hold and recovery are explicit, warning-free, and do not rely on
an undeclared prop or unsupported physics.
+## Draft, validate, and repair privately
+
+Use this internal sequence before returning any document:
+
+1. **Choose the representation.** Confirm it is one person, kinematic,
+ floor-bound, and uses only supported props and coarse hand articulation.
+2. **Write the support ledger.** For every phase, list which surfaces carry
+ weight and which new endpoint must arrive at a target. This determines
+ `ground-lock` vs `reach` vs `pin` vs `grip` before joint angles are written.
+3. **Write key poses.** Describe the opening pose and each phase endpoint in
+ plain language privately. Keep lead/trail sides fixed.
+4. **Translate to absolute channels.** Add only compatible joint/action pairs,
+ using positive in-range magnitudes and explicit zero/neutral recovery.
+5. **Check inheritance.** Verify every omitted joint is meant to hold its last
+ value; verify every still-active contact is repeated.
+6. **Check cues against execution.** Delete any cue claim that is not encoded by
+ a joint, root directive, or contact in that phase.
+7. **Simulate the loop.** Compare the last key pose with the composed start
+ pose. Add a recovery when an automatic reset would pop.
+8. **Lint the text.** Check colons, quotes, indentation, `s` duration suffixes,
+ canonical names, declared prop anchors, one primary root solver per phase,
+ and a positive integer repeat.
+
+### Common invalid output and the exact repair
+
+| Invalid or misleading output | Repair |
+| --- | --- |
+| ``step "Lift" 1 settle:`` | Add the duration suffix: ``1s``. |
+| ``cue: "Keep the foot down"`` | Remove the colon and encode the contact: ``ground-lock: foot_left`` plus ``cue "..."``. |
+| ``knees: bend 90`` | Use the closed action: ``knees: flex 90``. |
+| ``knee: flex 90`` or ``left_knee: flex 90`` | Use ``knees`` or ``knee_left``. |
+| ``shoulder_right: flex -20`` | Use the directional action: ``shoulder_right: extend 20``. |
+| ``ankles: flex 20`` | Use ``dorsiflex`` or ``plantarflex`` within ankle ROM. |
+| ``pelvis: flex 60`` | Use ``pelvis: hinge 60`` for a hip hinge, or pose the `hips` for thigh flexion. |
+| ``ground-lock: knee_left`` | Knees are not ground-lock contacts; use ``reach: knee_left floor`` while approaching, then ``pin: knee_left floor`` when it carries the body. |
+| ``reach: pelvis floor`` | `pelvis` is not a reach effector; use ``pin: pelvis floor``. |
+| ``pin: hand_left knee_right`` | A body landmark is not a fixed anchor; use ``reach: hand_left knee_right``. |
+| ``grip: hands bar`` without a prop | Add top-level ``prop bar``. |
+| ``ground-lock: feet`` and ``pin: knee_left floor`` in one step | Keep one root solver; use one primary support plus compatible `reach` constraints. |
+| A hold expressed only as ``cue "Hold"`` | Add a separate unchanged `linear` step and repeat its contacts. |
+| A cue says “right” while targets use `_left` | Make the phase name, targets, contacts, and cue use the same side. |
+| A final bent pose followed by looping | Add an explicit recovery to the start pose. |
+
## Timing modes
- `flow`: carry momentum through an interior pose; use for continuous dance,