feat(sdk): can() returns CanResult; T4 dispatch-boundary tests#1426
Conversation
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Verified at HEAD d4d2899.
Big, clean diff. The CanResult = {ok:true} | {ok:false, code, message, hint?} discriminator is the right shape — exhaustive at the type level, stable codes (E_TARGET_NOT_FOUND / E_NO_ROOT / E_NO_GSAP_SCRIPT / E_NO_GSAP_TIMELINE / E_UNKNOWN_OP) callers can switch on, optional hint for human-facing UIs. The CAN_OK constant + canErr() helper keep validateOp switch arms tight without re-allocating object literals on every call. T4 dispatch-boundary test suite (213 lines, new file) covers the boundaries the lens called out: invalid target, no current state path (E_NO_GSAP_SCRIPT on non-GSAP composition), happy-path GSAP op on GSAP composition, batch-collapse, custom origin propagation, override-set accumulation.
The mutate.ts refactor extracting resolveKeyframe() from handleSetGsapKeyframe / handleRemoveGsapKeyframe deduplicates four lines of acorn-parse boilerplate cleanly. The new "bail if two keyframes share the same percentage" guard in handleRemoveGsapKeyframe is a quiet but real correctness fix — removeKeyframeFromScript matches by percentage, so two keyframes at the same pct would have removed the wrong one.
The parseDeclarations quote-aware tokenizer fix (cssWriter.ts) is a separate latent-bug fix: previously a semicolon inside a quoted CSS value (e.g. content: "a; b") would have terminated the declaration early. Worth the inclusion since CanResult tests touch setClassStyle, but it widens the PR's apparent scope; brief mention in the body would help the reviewer.
The setGsapScript(document, "") → existing?.remove() change (model.ts) is correct: previously setGsapScript would have written an empty <script> tag. The GSAP cascade in #1431 depends on this — when removing the last animation that uses the script, the cascade-removed script should disappear from the DOM, not leave an empty tag.
Concerns
- PR body empty (the template is intact, no What/Why/How filled in). For a +418/-116 PR that changes a public-API signature (
can(op): boolean → CanResult) this is a real reviewer-cost hit. Worth filling in even briefly — call out the breaking-shape change so downstream consumers (the playground in this very stack, #1429 / #1443) know to grep. - Discriminator-code stability:
code: stringis typed asstring, not a string-literal union. The JSDoc lists the codes, but atype CanErrorCode = 'E_TARGET_NOT_FOUND' | ...would give consumers exhaustiveness checking on switch. Minor — nice-to-have, not blocking. .fallowrc.jsonccomplexity-suppression onfiles.ts: the comment explains the inherited-fingerprint shift problem clearly, but flag it for follow-up — leavingexecuteGsapMutationat cyclomatic-58 with the file silenced is debt accruing.- Cross-stack coherence: PR body claims (and the override-set test confirms)
removeElementemits a removal marker on the override-set. #1431 implements the cascade cleanup of property keys for that removed element. The two PRs are wire-coherent; just noting for the stack reviewer.
Determinism: no Date.now() / Math.random() / fetch() / performance.now() in dispatch-boundary or validateOp paths. MY_ORIGIN = Symbol("ai-agent") in tests is fine — tests only.
Verdict: approve-with-concerns. Fill in the PR body before merge.
Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
CI blocked by the oxfmt failure in #1423. Code change is substantive and well-reviewed below.
Strengths:
CanResultshape ({ ok: true } | { ok: false; code: string; reason: string }) is a better public API than bare boolean — callers can surface meaningful error messages without a separatewhyCantI()query.E_NO_GSAP_SCRIPTandE_UNKNOWN_OPerror codes are stable, greppable strings — good for telemetry and conditional recovery.- T4 dispatch-boundary tests (
packages/sdk/src/engine/dispatch-boundary.test.ts) are thorough: they cover the happy path, the GSAP-required path, and the unknown-op path without needing full E2E setup.
Important:
packages/sdk/src/session.tsvalidateOprefactor — the oldreturn !PHASE3B_OPS.has(op.type)default:path returnedtrue(allowing) for types not in the set. The newdefault: return { ok: false, code: E_UNKNOWN_OP, ... }changes the semantics: unknown op types previously passed silently throughcan(). This is intentional (the PR title says "dispatch boundary"), but if any caller relied on the old allow-through for forwards-compat ops, they'll now getcan()→false. Confirm this is a deliberate contract tightening and document in CHANGELOG or release notes.
Nit:
packages/sdk/src/types.ts—CanResultis exported butE_NO_GSAP_SCRIPT/E_UNKNOWN_OPstring constants are not. Consumers who want toswitchonresult.codeneed the constants exported too, otherwise they magic-string match. Export them from the package root.
Verdict: APPROVE (pending #1423 oxfmt fix)
Reasoning: CanResult is a cleaner contract than boolean; T4 tests are solid. The unknown-op semantic change is flagged as important but appears intentional.
— magi
d22af04 to
a531d34
Compare
d4d2899 to
23563d4
Compare
a531d34 to
c80335d
Compare
23563d4 to
85f4ba6
Compare
|
@james-russo-rames-d-jusso — concerns addressed PR body: filled in (What/Why/How/Test plan now populated).
PR head has been rebased (same content, new base) — force pushed to align with the stack after the stage 1–6 rebase. |
0df98a6 to
59793ee
Compare
The base branch was changed.
85f4ba6 to
ff19b27
Compare
- setGsapScript: remove element when newScript="" (fixes undo/redo duplicate-script bug)
- parseDeclarations: track quotes so ; inside CSS values (data URIs) doesn't split
- handleRemoveGsapKeyframe: guard against duplicate-percentage ambiguity (return EMPTY)
- resolveKeyframe: return kfs so callers can check uniqueness
- handleSetClassStyle: emit op:"add" (not "replace") when no prior <style> element
- FsAdapter listVersions: Number(f.split("_")[0]) — was NaN due to underscore in key
- FsAdapter doWrite: split try/catch so appendVersion failure doesn't fire error handlers
- FileAdapter playground: add content:"" field to satisfy PersistVersionEntry contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… result.code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
ff19b27 to
f81b3c6
Compare

What
Changes
can(op)return type frombooleantoCanResult = {ok:true} | {ok:false, code, message, hint?}.Adds
CAN_OKconstant andcanErr()helper sovalidateOpswitch arms stay tight. Exports stable error codes (E_TARGET_NOT_FOUND,E_NO_ROOT,E_NO_GSAP_SCRIPT,E_NO_GSAP_TIMELINE,E_UNKNOWN_OP) that callers can switch on.Also includes:
resolveKeyframe()extraction from twohandleSet/RemoveGsapKeyframehandlers (dedup), a quiet correctness fix forremoveKeyframewhen two keyframes share the same percentage, and aparseDeclarationstokenizer fix for semicolons inside quoted CSS values.Why
booleanresult gives callers no way to surface actionable error messages.CanResultlets agent callers and UI both switch onresult.codeand showresult.hintin error UX.How
validateOpin mutate.ts now returnsCanResultfrom a switch over op typecan(op)in session.ts wrapsvalidateOpand retains the publiccan(): booleanpath for backward compat until callers migrateTest plan