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
10 changes: 10 additions & 0 deletions .agents/docs/taffyjs-yoga-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ This ledger records only judgments that Yunfei explicitly expressed about `@taff

**Source:** Yunfei (`@hyfdev`), 2026-08-14; required differences to be documented, accepted explicit unsupported behavior, vouched the original consolidated verification rules, and later clarified that the base design must not preselect Ink or another external consumer.

### Official Yoga compatibility corpus

**Ruling:** Compatibility verification must run the official Yoga JavaScript test snapshot under `tests/taffyjs-yoga/yoga-official/` against every maintained Yoga backend. The directory name remains stable while the exact Yoga version, tag, and commit live in its README. Cases covered by a published Different or Unsupported classification must be registered as explicit expected failures; every other active upstream case must pass.

**Limits:** The snapshot is a bounded regression corpus, not a compatibility percentage and not a replacement for focused TaffyJS differential, rejection, type, state, or failure-atomicity tests. Yoga's benchmarks and Chrome/Selenium generator are not part of the normal suite. The copied upstream sources stay unchanged, retain their license, and are replaced from one reviewed commit when the compatibility baseline changes. An unexpected failure or an expected failure that begins passing requires reviewing the implementation, classification, focused tests, and documentation together rather than silently changing the expected-failure list.

**Why:** The official suite provides broad, maintained combinations of Yoga's JavaScript API and layout behavior, while the existing focused tests explain and enforce TaffyJS-specific compatibility boundaries. Keeping the version in the README avoids renaming the directory on a future baseline update.

**Source:** Yunfei (`@hyfdev`), 2026-08-16; asked to migrate Yoga's official JavaScript tests as compatibility coverage, selected `tests/taffyjs-yoga/yoga-official/`, required the version to live in its README, and accepted the proposed pinned-snapshot and classified-failure approach.

### Upstream work and current compatibility

[VOUCHED @hyfdev 2026-08-16]
Expand Down
9 changes: 8 additions & 1 deletion .agents/docs/taffyjs-yoga-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ This matrix answers whether the public capability has a credible implementation
No first-release feasibility blocker remains in the inventoried Yoga 3.2.1 surface. The grouped result is:

- Package entries, factories, ordinary Config state, tree ownership and mutation, declaration getters and setters, calculation on any live Node, logical dirty and new-layout flags, ordinary measure callbacks, Yoga-shaped computed output, and per-Node point-grid rounding are Compatible through bounded JavaScript or TypeScript mapping.
- Yoga cache artifacts, MeasureMode-sensitive measurement results, aspect-ratio layout, min/max combined with flex growth or shrinkage, shrink factors totaling below one, percentage main-axis gaps in auto-sized containers, main-axis auto margins combined with justification offsets, and the documented reversed-axis, overflowing, or zero-size distribution cases are Different. The same API remains callable and returns Taffy's stable geometry under the documented trigger.
- Yoga cache artifacts, MeasureMode-sensitive measurement results, aspect-ratio layout, min/max combined with flex growth or shrinkage, shrink factors totaling below one, percentage main-axis gaps in auto-sized containers, main-axis auto margins combined with justification offsets, oversized cross-axis auto margins, and the documented reversed-axis, overflowing, or zero-size distribution cases are Different. The same API remains callable and returns Taffy's stable geometry under the documented trigger.
- Display.Contents, PositionType.Static, non-None Errata values, reference-baseline selection, property-invalid Align values, positive or negative infinite calculation owner dimensions, and undeclared broken setter coercions are Unsupported. Omit a wholly unsupported member where practical and always reject it at runtime.

### Package entries, factories, and public value shapes
Expand Down Expand Up @@ -356,6 +356,7 @@ Compatible numeric comparisons allow an absolute error below `0.0001` for finite
| Oversized single line on a reversed main axis with distribution-style justifyContent | Different | When a RowReverse or ColumnReverse no-wrap line overflows and justifyContent is SpaceBetween, SpaceAround, or SpaceEvenly, Yoga and Taffy choose different negative-free-space fallback placement. In a 240-point RowReverse line with 140, 130, and 120-point children, Yoga left positions are 100, -30, and -150 while Taffy reports 250, 120, and 0. This affects sibling distribution and is not an output-only correction. |
| Zero-cross-size wrapped line under Stretch item alignment | Different | A wrapped line whose items all resolve to zero cross size stays on Yoga's cross-start or cross-end edge, while Taffy distributes the zero-size line under FlexEnd, Center, SpaceAround, or SpaceEvenly alignContent. For a 180-point cross axis, ordinary Wrap plus Center places Yoga at 0 and Taffy at 90; WrapReverse plus Center places Yoga at 180 and Taffy at 90. Reconstructing this degenerate line placement would duplicate Flex logic. |
| Main-axis auto margins on a reversed axis or combined with a nonzero justifyContent offset | Different | Yoga lets positive-free-space main-axis auto margins consume the remaining space and can suppress justifyContent, while Taffy can distribute the margin and retain an additional offset. In a width-240 RowReverse, a width-60 child with margin-left auto has left 180 in Yoga and 360 in Taffy even under FlexStart. In a 240x180 Column with justifyContent Center, Yoga child tops are 65 and 160 while Taffy reports 130 and 225. Correcting either sibling distribution would duplicate Flex logic, so Taffy's geometry is retained and computed auto margins remain masked to zero. |
| Oversized child with one or both cross-axis margins set to auto | Different | In a 52x52 default Column with justifyContent Center, a 72x72 child with auto left and right margins has left 0 in Yoga LTR and -20 in Yoga RTL, while Taffy centers it at -10 in both directions; the top remains -10. The one-auto and mixed fixed/auto variants follow the same overflow-alignment split. Correcting them would reproduce cross-axis Flex alignment after negative free-space resolution, so Taffy's stable geometry is retained. |
| Relative insets, including reversed axes | Compatible | Relative positioning does not participate in normal-flow sizing. When Taffy and Yoga apply opposite signs on a reversed axis, the projection can remove Taffy's declaration-derived offset and apply Yoga's declaration-derived offset to the Node's projected position and descendant absolute accumulation without redistributing siblings or calculating Flex geometry. A combination that also triggers the main-axis auto-margin difference remains covered by the Different row above. |

### Yoga-shaped output projection
Expand Down Expand Up @@ -454,6 +455,12 @@ For supported APIs, @taffyjs/yoga preserves Yoga 3.2.1's published declaration s
- calculateLayout requires width and height arguments in TypeScript while allowing each to be explicitly undefined; the runtime wrapper also permits the arguments to be omitted and supplies NaN, NaN, and LTR defaults.
- The generic setter wrapper attempts to accept Value-like objects and comments that a getter result can be passed back to a setter, but the published Value is a plain object without a numeric valueOf implementation. A published 3.2.1 getWidth-to-setWidth round trip resets the value to Unit.Undefined rather than preserving it.

## Official Yoga compatibility corpus

The package-boundary suite includes an unchanged snapshot of Yoga's official JavaScript tests at [`tests/taffyjs-yoga/yoga-official/`](../../tests/taffyjs-yoga/yoga-official/README.md). It runs through the same bare `yoga-layout` imports against both the native and Wasm packages.

Cases covered by a published Different or Unsupported classification are explicit expected failures, and every other active upstream case must pass. An expected failure that begins passing is a review signal rather than a reason to silently remove it. This bounded corpus provides broad regression coverage rather than a compatibility percentage; focused local fixtures remain authoritative for the exact TaffyJS rejection or stable Different result. The snapshot README is the single source for its version, provenance, contents, counts, license, and refresh procedure.

## Ongoing maintenance

- Re-audit the complete capability matrix before deliberately adopting a Yoga version newer than 3.2.1; no supported release may contain an unclassified method, value, or result trigger.
Expand Down
2 changes: 2 additions & 0 deletions .agents/docs/technology-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ The root Vite+ task graph builds the napi-rs artifacts into `packages/taffyjs-wa

For `packages/taffyjs-yoga`, `vp pack` builds the root and `/load` entries plus shared chunks and declarations over the public `@taffyjs/node` dependency. Its `dist` is ignored build output: the native task graph builds it before runtime and declaration tests, and any future publication workflow must build it before packing or publishing. Test-only ambient declarations resolve authored Yoga test types to source so a clean checkout does not require `dist` without remapping the public runtime specifiers that Bun, Deno, and Node resolve; exact Vite aliases, subprocess entry injection, runtime smoke checks, and the dedicated public-declaration check still exercise the built package entries.

The Yoga consumer suite also runs the pinned official JavaScript snapshot under `tests/taffyjs-yoga/yoga-official` through exact bare-import aliases against both Yoga backends. Upstream test sources are excluded from formatting, linting, and TaffyJS type checking to preserve the reviewed snapshot; the local classifier converts only documented Different and Unsupported cases into required failures, while its manifest and setup remain normal maintained TypeScript.

For `packages/taffyjs-yoga-wasm`, Vite+ builds those same sibling source entries and redirects only the exact bare `@taffyjs/node` import to the external public `@taffyjs/wasm` dependency. The package owns no facade source and emits no second Wasm payload. Its `dist` is ignored build output; the Wasm verification graph builds it before type, package, tarball, Node, bundle, or Chromium checks.

`tools/api-codegen` owns source generation that must keep Rust and TypeScript API facts aligned. Its first maintained input is `api/numeric-families.json`; `vp run codegen` updates both language outputs. CI runs `vp run check:codegen`, which regenerates and rejects any resulting Git diff.
Expand Down
1 change: 1 addition & 0 deletions packages/taffyjs-yoga/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ These rows are deliberate, version-specific differences with regression fixtures
| An oversized no-wrap reversed main axis with `SpaceBetween`, `SpaceAround`, or `SpaceEvenly` | Keeps Taffy's negative-free-space fallback. Widths 140, 130, and 120 in a width-240 `RowReverse` with `SpaceBetween` have left positions 250, 120, and 0. | The same left positions are 100, -30, and -150. |
| A wrapped line whose stretched items resolve to zero cross size under distribution-style `alignContent` | Keeps Taffy's line distribution. Two 60-point, flex-basis-zero children in a 100-by-180 Row with `alignContent: Center` both have top 90 for Wrap and WrapReverse. | Both children have top 0 for Wrap and 180 for WrapReverse. |
| Main-axis auto margins on a reversed axis or combined with a nonzero `justifyContent` offset | Keeps Taffy's distribution and masks computed auto margins to zero. A width-60 child with `margin-left: auto` in a width-240 `RowReverse` has left 360; the Center fixture has child tops 130 and 225. | Yoga places the reversed child at left 180; auto margins suppress the Center offset and produce tops 65 and 160. |
| An oversized child with one or both cross-axis margins set to auto | Keeps Taffy's overflow alignment. For both-auto, left-auto, left-10/right-auto, and left-auto/right-10 fixtures, left is respectively -10, -20, 10, and -30 in both LTR and RTL. Their top is -10. | The same left positions are respectively 0/-20, 0/-20, 10/-20, and 0/-30 in LTR/RTL. Their top remains -10. |
| Measure callback count, order, and exact mode trace | Maps every Taffy request independently. A constant 30-by-10 callback for a child in a 100-by-40 Row runs four times in the pinned fixture and still produces the same 30-by-40 layout. | Runs once with width `AtMost(100)` and height `Exactly(40)` in the same fixture. |
| A Measure callback whose result changes with `MeasureMode` | Keeps Taffy's multi-stage result. In the pinned 100-by-40 Row, a callback returning width 80 for `Undefined` and 20 for `AtMost` produces width 80. | Calls the callback with `AtMost(100)` and produces width 20. |
| Replace or remove a Measure callback on an otherwise clean Node | Privately invalidates Taffy's measurement cache. Replacing a cached width-10 callback with width 20 produces 20 on the next calculation; removing it then produces zero. | Leaves the Node publicly clean and reuses the cached width 10 after both operations. |
51 changes: 37 additions & 14 deletions tests/taffyjs-yoga-wasm/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,43 @@ const yogaWasmLoadEntry = resolve(
);

export default defineConfig({
resolve: {
alias: [
{ find: /^yoga-layout$/, replacement: yogaWasmEntry },
{ find: /^yoga-layout\/load$/, replacement: yogaWasmLoadEntry },
{ find: /^@taffyjs\/node$/, replacement: wasmEntry },
],
},
test: {
include: ["../taffyjs-yoga/*.test.mts"],
retry: 0,
sequence: { concurrent: true },
env: {
TAFFYJS_TEST_ENTRY: pathToFileURL(wasmEntry).href,
TAFFYJS_YOGA_TEST_ENTRY: pathToFileURL(yogaWasmEntry).href,
},
projects: [
{
resolve: {
alias: [
{ find: /^yoga-layout$/, replacement: yogaWasmEntry },
{ find: /^yoga-layout\/load$/, replacement: yogaWasmLoadEntry },
{ find: /^@taffyjs\/node$/, replacement: wasmEntry },
],
},
test: {
name: "maintained",
include: ["../taffyjs-yoga/*.test.mts"],
retry: 0,
sequence: { concurrent: true },
env: {
TAFFYJS_TEST_ENTRY: pathToFileURL(wasmEntry).href,
TAFFYJS_YOGA_TEST_ENTRY: pathToFileURL(yogaWasmEntry).href,
},
},
},
{
resolve: {
alias: [{ find: /^yoga-layout$/, replacement: yogaWasmEntry }],
},
test: {
name: "official",
globals: true,
include: [
"../taffyjs-yoga/yoga-official/tests/*.test.ts",
"../taffyjs-yoga/yoga-official/tests/generated/*.test.ts",
],
retry: 0,
sequence: { concurrent: true },
setupFiles: [resolve(import.meta.dirname, "../taffyjs-yoga/yoga-official/setup.ts")],
},
},
],
},
});
66 changes: 66 additions & 0 deletions tests/taffyjs-yoga/differences.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,72 @@ test("Different: main-axis auto margins retain Taffy's justify offset", () => {
}
});

for (const fixture of [
{ name: "both auto", left: "auto", right: "auto", taffyLeft: -10, yogaLeft: [0, -20] },
{ name: "left auto", left: "auto", right: undefined, taffyLeft: -20, yogaLeft: [0, -20] },
{ name: "left 10, right auto", left: 10, right: "auto", taffyLeft: 10, yogaLeft: [10, -20] },
{ name: "left auto, right 10", left: "auto", right: 10, taffyLeft: -30, yogaLeft: [0, -30] },
] as const) {
test(`Different: oversized cross-axis auto margins keep Taffy's alignment (${fixture.name})`, () => {
const config = Yoga.Config.create();
const oracleConfig = OracleYoga.Config.create();
config.setPointScaleFactor(0);
oracleConfig.setPointScaleFactor(0);
const root = Yoga.Node.createWithConfig(config);
const child = Yoga.Node.createWithConfig(config);
const oracleRoot = OracleYoga.Node.createWithConfig(oracleConfig);
const oracleChild = OracleYoga.Node.createWithConfig(oracleConfig);
try {
root.setWidth(52);
root.setHeight(52);
root.setJustifyContent(Justify.Center);
child.setWidth(72);
child.setHeight(72);
oracleRoot.setWidth(52);
oracleRoot.setHeight(52);
oracleRoot.setJustifyContent(OracleJustify.Center);
oracleChild.setWidth(72);
oracleChild.setHeight(72);

for (const [value, edge, oracleEdge] of [
[fixture.left, Edge.Left, OracleEdge.Left],
[fixture.right, Edge.Right, OracleEdge.Right],
] as const) {
if (value === "auto") {
child.setMarginAuto(edge);
oracleChild.setMarginAuto(oracleEdge);
} else if (value !== undefined) {
child.setMargin(edge, value);
oracleChild.setMargin(oracleEdge, value);
}
}

root.insertChild(child, 0);
oracleRoot.insertChild(oracleChild, 0);
for (const [index, direction, oracleDirection] of [
[0, Direction.LTR, OracleDirection.LTR],
[1, Direction.RTL, OracleDirection.RTL],
] as const) {
root.calculateLayout(undefined, undefined, direction);
oracleRoot.calculateLayout(undefined, undefined, oracleDirection);
assert.deepEqual(
[child.getComputedLeft(), child.getComputedTop()],
[fixture.taffyLeft, -10],
);
assert.deepEqual(
[oracleChild.getComputedLeft(), oracleChild.getComputedTop()],
[fixture.yogaLeft[index], -10],
);
}
} finally {
root.freeRecursive();
oracleRoot.freeRecursive();
config.free();
oracleConfig.free();
}
});
}

test("Different: reversed-axis auto margins keep Taffy's distribution", () => {
const config = Yoga.Config.create();
const oracleConfig = OracleYoga.Config.create();
Expand Down
2 changes: 1 addition & 1 deletion tests/taffyjs-yoga/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["*.test.mts", "vite.config.ts", "yoga-layout-source.d.ts"]
"include": ["*.test.mts", "vite.config.ts", "yoga-layout-source.d.ts", "yoga-official/*.ts"]
}
Loading
Loading