diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4fe5b682..4077b13a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,5 @@ { - "recommendations": ["repowise-dev.repowise"] + "recommendations": [ + "repowise-dev.repowise" + ] } diff --git a/AGENTS.md b/AGENTS.md index e49955cf..eddf7147 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Dependencies flow top-down (consumer direction): Assertion utilities that need to be importable by both `packages/*` post-build scripts and `e2e/*` fixtures live in `packages/_assertions/` (a `packages/`-resident private package), so imports always flow top-down without crossing the `packages/ ← e2e/` boundary. -No automated enforcement yet — candidate for a future CI grep or lint rule. +Enforced by `scripts/verify/topology.ts` (run in `vp run verify:lint`), which fails loud across all three vectors — source imports (relative paths and e2e workspace-name specifiers), tsconfig `paths` aliases, and `packages/* → e2e/*` `package.json` dependencies — listing every offending file. ## Monorepo Build System @@ -58,35 +58,37 @@ Every workspace-filtered command must include `--fail-if-no-match`. Do not combi Atomic diagnostics are narrow, actionable leaves. They never materialize missing upstream artifacts. If one fails with `ERROR: X missing` or a `PREPARE:` line, run the exact remediation it prints, then rerun the diagnostic. Owner claims follow the same fail-loud rule and do not rebuild shared Rust or TS prerequisites. -| Command | What it covers | Upstream requires | Fails loud when | Typical runtime | -| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------- | -| `vp run verify:lint` | `vp lint` + `vp fmt --check` (oxlint + oxfmt) | `bun install` | lint rule violation or formatter drift | fast | -| `vp run verify:compile` | `tsgo --noEmit` across all packages | `bun install` | type error in any package `src/` | fast | -| `vp run verify:types` | type-contract tests via `tsconfig.test-d.json` | `bun install` | compile-time contract assertion fails | medium | -| `vp run verify:unit:rust` | `cargo test --lib` across the shared loader and v2 crates (debug profile) | Rust toolchain | Rust unit test fails | medium | -| `vp run verify:clippy` | `cargo clippy --workspace --all-targets --all-features -- -D warnings` across all active Rust crates | Rust toolchain with Clippy | any Rust or Clippy warning | medium | -| `vp run verify:unit:ts` | `bunx vp test run` (Vitest) on `system/__tests__`, `vite-plugin/tests`, `next-plugin/tests`, `properties/__tests__`, `_assertions/__tests__`, `_parity/__tests__` | `bun install` | TS unit test fails | fast | -| `vp run verify:coverage:ts` | aggregate per-file V8 coverage for the same TS targets as `verify:unit:ts`; emits text and lcov reports under `coverage/ts` (not per-test impact mapping) | `bun install` | TS unit test or coverage report generation fails | medium | -| `vp run verify:coverage:e2e` | V8 line coverage of `packages/*/src` code exercised by the next-app and vite-app production build+assert lanes; rebuilds TS dists with sourcemaps, emits `coverage/e2e/lcov.info`, then restores sourcemap-free dists | fresh v2 NAPI (fails loud with `PREPARE:`) | NAPI missing/stale, consumer build/assert fails, or report generation fails | slow | -| `vp run verify:workers:contracts` | structural, Worker behavior, and hydration contracts for all four Worker fixtures without production builds | `bun install` | root command/task contract or fixture test fails | fast | -| `vp run verify:hygiene:rust` | `cargo machete` dep-hygiene check across all active extraction Rust crates | `cargo-machete` binary on PATH | unused dep found (or machete missing) | fast | -| `vp run verify:canary` | v2 NAPI boundary snapshot tests | fresh v2 NAPI `.node` binary (mtime > Rust src) | NAPI binary missing or stale | medium | -| `vp run verify:parity` | v2 fresh-process/thread self-check, seam battery, and committed production/development oracle | fresh v2 NAPI `.node` + committed parity baselines | v2 input/baseline missing, stale, or divergent | medium | -| `vp run verify:integration` | full pipeline E2E in `packages/_integration/__tests__` | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` | NAPI or any upstream dist missing/stale | medium | -| `vp run @animus-ui/next-app#verify:build` | Next consumer fixture build | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` + fresh `next-plugin/dist/` | NAPI or any upstream dist missing/stale | slow | -| `vp run @animus-ui/showcase#verify:build` | showcase vite build | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` + fresh `vite-plugin/dist/` + fresh `properties/dist/` | NAPI or any upstream dist missing/stale | slow | -| `vp run @animus-ui/vite-app#verify:build` | Vite consumer fixture build (`e2e/vite-app`) | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` + fresh `vite-plugin/dist/` + fresh `properties/dist/` | NAPI or any upstream dist missing/stale | slow | -| `vp run @animus-ui/vinext-app#verify:build` | Vinext App+Pages Worker production build | fresh v2 NAPI + fresh `extract/dist/`, `system/dist/`, `vite-plugin/dist/`, `properties/dist/`, and `test-ds/dist/` | NAPI or any upstream dist missing/stale | slow | -| `vp run @animus-ui/react-router-app#verify:build` | React Router v8 SSR Worker production build | fresh v2 NAPI + fresh `extract/dist/`, `system/dist/`, `vite-plugin/dist/`, `properties/dist/`, and `test-ds/dist/` | NAPI or any upstream dist missing/stale | slow | -| `vp run @animus-ui/next-app#verify:assert` | positional assertions on Next build output (TS, via `@animus-ui/assertions`) | `e2e/next-app/.next/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | -| `vp run @animus-ui/showcase#verify:assert` | positional assertions on showcase dist (TS, via `@animus-ui/assertions`) | `packages/showcase/dist/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | -| `vp run @animus-ui/vite-app#verify:assert` | positional assertions on Vite fixture dist (TS, via `@animus-ui/assertions`) | `e2e/vite-app/dist/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | -| `vp run @animus-ui/vinext-app#verify:assert` | structural assertions on Vinext Worker output (TS, via `@animus-ui/assertions`) | `e2e/vinext-app/dist/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | -| `vp run @animus-ui/react-router-app#verify:assert` | structural assertions on React Router Worker output (TS, via `@animus-ui/assertions`) | `e2e/react-router-app/build/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | -| `vp run @animus-ui/showcase#verify:dry-run` | credential-free Wrangler validation for Worker `animus` | `packages/showcase/dist/` + matching app-owned Worker config | build output missing, Worker identity mismatch, or Wrangler rejects output | fast | -| `vp run @animus-ui/vite-app#verify:dry-run` | credential-free Wrangler validation for Worker `animus-vite-canary` | `e2e/vite-app/dist/` + matching app-owned Worker config | build output missing, Worker identity mismatch, or Wrangler rejects output | fast | -| `vp run @animus-ui/vinext-app#verify:dry-run` | credential-free Wrangler validation for Worker `animus-vinext-canary` | `e2e/vinext-app/dist/` + matching app-owned Worker config | build output missing, Worker identity mismatch, or Wrangler rejects output | fast | -| `vp run @animus-ui/react-router-app#verify:dry-run` | credential-free Wrangler validation for Worker `animus-react-router-canary` | `e2e/react-router-app/build/` + matching app-owned Worker config | build output missing, Worker identity mismatch, or Wrangler rejects output | fast | +| Command | What it covers | Upstream requires | Fails loud when | Typical runtime | +| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | --------------- | +| `vp run verify:lint` | `vp lint` + `vp fmt --check` (oxlint + oxfmt) + the workspace-topology boundary scan (`scripts/verify/topology.ts`) | `bun install` | lint rule violation, formatter drift, or a forbidden cross-boundary dependency | fast | +| `vp run verify:compile` | `tsgo --noEmit` across all packages | `bun install` | type error in any package `src/` | fast | +| `vp run verify:types` | type-contract tests via `tsconfig.test-d.json` | `bun install` | compile-time contract assertion fails | medium | +| `vp run verify:unit:rust` | `cargo test --lib` across the shared loader and v2 crates (debug profile) | Rust toolchain | Rust unit test fails | medium | +| `vp run verify:clippy` | `cargo clippy --workspace --all-targets --all-features -- -D warnings` across all active Rust crates | Rust toolchain with Clippy | any Rust or Clippy warning | medium | +| `vp run verify:unit:ts` | `bunx vp test run` (Vitest) on `system/__tests__`, `vite-plugin/tests`, `next-plugin/tests`, `properties/__tests__`, `_assertions/__tests__`, `_parity/__tests__`, the engine-free `extract/tests` files, and the `scripts/verify` policy tests | `bun install` | TS unit test fails | fast | +| `vp run verify:coverage:ts` | aggregate per-file V8 coverage for the same TS targets as `verify:unit:ts`; emits text and lcov reports under `coverage/ts` (not per-test impact mapping) | `bun install` | TS unit test or coverage report generation fails | medium | +| `vp run verify:coverage:e2e` | V8 line coverage of `packages/*/src` code exercised by the next-app and vite-app production build+assert lanes; rebuilds TS dists with sourcemaps, emits `coverage/e2e/lcov.info`, then restores sourcemap-free dists | fresh v2 NAPI (fails loud with `PREPARE:`) | NAPI missing/stale, consumer build/assert fails, or report generation fails | slow | +| `vp run verify:workers:contracts` | structural, Worker behavior, and hydration contracts for all four Worker fixtures without production builds | `bun install` | root command/task contract or fixture test fails | fast | +| `vp run verify:hygiene:rust` | `cargo machete` dep-hygiene check across all active extraction Rust crates | `cargo-machete` binary on PATH | unused dep found (or machete missing) | fast | +| `vp run verify:canary` | v2 NAPI boundary snapshot tests + real-engine staticCss forced-emission evidence (`static-css-overrides.test.ts`) | fresh v2 NAPI `.node` binary (mtime > Rust src) | NAPI binary missing or stale | medium | +| `vp run verify:parity` | v2 fresh-process/thread self-check, seam battery, and committed production/development oracle | fresh v2 NAPI `.node` + committed parity baselines | v2 input/baseline missing, stale, or divergent | medium | +| `vp run verify:integration` | full pipeline E2E in `packages/_integration/__tests__` | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` | NAPI or any upstream dist missing/stale | medium | +| `vp run @animus-ui/next16-app#verify:build` | Next 16 consumer fixture build (Turbopack build mode) | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` + fresh `next-plugin/dist/` | NAPI or any upstream dist missing/stale | slow | +| `vp run @animus-ui/next16-app#verify:assert` | positional assertions on Next 16 build output (TS, via `@animus-ui/assertions`) | `e2e/next16-app/.next/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | +| `vp run @animus-ui/next-app#verify:build` | Next consumer fixture build | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` + fresh `next-plugin/dist/` | NAPI or any upstream dist missing/stale | slow | +| `vp run @animus-ui/showcase#verify:build` | showcase vite build | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` + fresh `vite-plugin/dist/` + fresh `properties/dist/` | NAPI or any upstream dist missing/stale | slow | +| `vp run @animus-ui/vite-app#verify:build` | Vite consumer fixture build (`e2e/vite-app`) | fresh NAPI + fresh `extract/dist/` + fresh `system/dist/` + fresh `vite-plugin/dist/` + fresh `properties/dist/` | NAPI or any upstream dist missing/stale | slow | +| `vp run @animus-ui/vinext-app#verify:build` | Vinext App+Pages Worker production build | fresh v2 NAPI + fresh `extract/dist/`, `system/dist/`, `vite-plugin/dist/`, `properties/dist/`, and `test-ds/dist/` | NAPI or any upstream dist missing/stale | slow | +| `vp run @animus-ui/react-router-app#verify:build` | React Router v8 SSR Worker production build | fresh v2 NAPI + fresh `extract/dist/`, `system/dist/`, `vite-plugin/dist/`, `properties/dist/`, and `test-ds/dist/` | NAPI or any upstream dist missing/stale | slow | +| `vp run @animus-ui/next-app#verify:assert` | positional assertions on Next build output (TS, via `@animus-ui/assertions`) | `e2e/next-app/.next/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | +| `vp run @animus-ui/showcase#verify:assert` | positional assertions on showcase dist (TS, via `@animus-ui/assertions`) | `packages/showcase/dist/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | +| `vp run @animus-ui/vite-app#verify:assert` | positional assertions on Vite fixture dist (TS, via `@animus-ui/assertions`) | `e2e/vite-app/dist/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | +| `vp run @animus-ui/vinext-app#verify:assert` | structural assertions on Vinext Worker output (TS, via `@animus-ui/assertions`) | `e2e/vinext-app/dist/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | +| `vp run @animus-ui/react-router-app#verify:assert` | structural assertions on React Router Worker output (TS, via `@animus-ui/assertions`) | `e2e/react-router-app/build/` + fresh `_assertions/dist/` | build output missing or assertions dist stale | fast | +| `vp run @animus-ui/showcase#verify:dry-run` | credential-free Wrangler validation for Worker `animus` | `packages/showcase/dist/` + matching app-owned Worker config | build output missing, Worker identity mismatch, or Wrangler rejects output | fast | +| `vp run @animus-ui/vite-app#verify:dry-run` | credential-free Wrangler validation for Worker `animus-vite-canary` | `e2e/vite-app/dist/` + matching app-owned Worker config | build output missing, Worker identity mismatch, or Wrangler rejects output | fast | +| `vp run @animus-ui/vinext-app#verify:dry-run` | credential-free Wrangler validation for Worker `animus-vinext-canary` | `e2e/vinext-app/dist/` + matching app-owned Worker config | build output missing, Worker identity mismatch, or Wrangler rejects output | fast | +| `vp run @animus-ui/react-router-app#verify:dry-run` | credential-free Wrangler validation for Worker `animus-react-router-canary` | `e2e/react-router-app/build/` + matching app-owned Worker config | build output missing, Worker identity mismatch, or Wrangler rejects output | fast | For domain-specific failure guidance, drill into `packages//AGENTS.md` after selecting a workflow or diagnostic here. @@ -108,6 +110,7 @@ This map routes an edit to the smallest sufficient claim plus any source-owned d | `packages/next-plugin/src/**` | `vp run verify:compile && vp run @animus-ui/next-app#verify` | | `packages/_assertions/src/**` | `vp run verify:unit:ts && vp run --fail-if-no-match -F './e2e/*' -F '!animus-packed-app' -F './packages/showcase' verify` | | `e2e/next-app/src/**` | `vp run @animus-ui/next-app#verify` | +| `e2e/next16-app/**` | `vp run @animus-ui/next16-app#verify` | | `e2e/vite-app/**` | `vp run verify:workers:contracts && vp run @animus-ui/vite-app#verify` | | `e2e/vinext-app/**` | `vp run verify:workers:contracts && vp run @animus-ui/vinext-app#verify` | | `e2e/react-router-app/**` | `vp run verify:workers:contracts && vp run @animus-ui/react-router-app#verify` | @@ -117,6 +120,7 @@ This map routes an edit to the smallest sufficient claim plus any source-owned d | `packages/_integration/__tests__/**` | `vp run verify:integration` | | `packages/test-ds/src/**` | `vp run verify:unit:ts && vp run --fail-if-no-match -F '...@animus-ui/test-ds' verify` | | `e2e/packed-app/**` or `scripts/verify/packed.sh` | `vp run verify:packed` | +| `scripts/verify/topology.*` | `bunx vp test run scripts/verify/topology.test.ts && vp run verify:lint` | | `packages/{properties,system,extract,vite-plugin,next-plugin}/package.json` (deps, peers, exports, files) | `vp run verify:packed` | | `.github/workflows/ci.yaml`, `scripts/**`, `.tool-versions` | `vp run verify:full` | | Worker orchestration (`vite.config.ts`, `scripts/verify/**`, root deploy scripts, Worker ignores) | `vp run verify:full` | diff --git a/bun.lock b/bun.lock index 2524d972..12a3c26a 100644 --- a/bun.lock +++ b/bun.lock @@ -43,6 +43,21 @@ "typescript": "^5.9.2", }, }, + "e2e/next16-app": { + "name": "@animus-ui/next16-app", + "dependencies": { + "@animus-ui/next-plugin": "workspace:*", + "@animus-ui/system": "workspace:*", + "@animus-ui/test-ds": "workspace:*", + "next": "^16.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + }, + "devDependencies": { + "@animus-ui/assertions": "workspace:*", + "typescript": "^5.9.2", + }, + }, "e2e/react-router-app": { "name": "@animus-ui/react-router-app", "dependencies": { @@ -129,10 +144,12 @@ "version": "0.1.1", "dependencies": { "@animus-ui/properties": "workspace:*", + "browserslist": "^4.24.0", + "lightningcss": "^1.29.0", }, "devDependencies": { "@animus-ui/system": "workspace:*", - "@mdx-js/mdx": "^3.0.0", + "@mdx-js/mdx": "3.1.1", "@napi-rs/cli": "^3.7.3", }, "peerDependencies": { @@ -243,6 +260,8 @@ "@animus-ui/next-plugin": ["@animus-ui/next-plugin@workspace:packages/next-plugin"], + "@animus-ui/next16-app": ["@animus-ui/next16-app@workspace:e2e/next16-app"], + "@animus-ui/parity": ["@animus-ui/parity@workspace:packages/_parity"], "@animus-ui/properties": ["@animus-ui/properties@workspace:packages/properties"], @@ -2135,6 +2154,14 @@ "@animus-ui/next-app/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@animus-ui/next16-app/next": ["next@16.2.11", "", { "dependencies": { "@next/env": "16.2.11", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.11", "@next/swc-darwin-x64": "16.2.11", "@next/swc-linux-arm64-gnu": "16.2.11", "@next/swc-linux-arm64-musl": "16.2.11", "@next/swc-linux-x64-gnu": "16.2.11", "@next/swc-linux-x64-musl": "16.2.11", "@next/swc-win32-arm64-msvc": "16.2.11", "@next/swc-win32-x64-msvc": "16.2.11", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ=="], + + "@animus-ui/next16-app/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "@animus-ui/next16-app/react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "@animus-ui/next16-app/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@animus-ui/parity/oxc-parser": ["oxc-parser@0.139.0", "", { "dependencies": { "@oxc-project/types": "^0.139.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.139.0", "@oxc-parser/binding-android-arm64": "0.139.0", "@oxc-parser/binding-darwin-arm64": "0.139.0", "@oxc-parser/binding-darwin-x64": "0.139.0", "@oxc-parser/binding-freebsd-x64": "0.139.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.139.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.139.0", "@oxc-parser/binding-linux-arm64-gnu": "0.139.0", "@oxc-parser/binding-linux-arm64-musl": "0.139.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.139.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.139.0", "@oxc-parser/binding-linux-riscv64-musl": "0.139.0", "@oxc-parser/binding-linux-s390x-gnu": "0.139.0", "@oxc-parser/binding-linux-x64-gnu": "0.139.0", "@oxc-parser/binding-linux-x64-musl": "0.139.0", "@oxc-parser/binding-openharmony-arm64": "0.139.0", "@oxc-parser/binding-wasm32-wasi": "0.139.0", "@oxc-parser/binding-win32-arm64-msvc": "0.139.0", "@oxc-parser/binding-win32-ia32-msvc": "0.139.0", "@oxc-parser/binding-win32-x64-msvc": "0.139.0" } }, "sha512-cf1TKZN+zc0lwqigeyXKzKVk5+vNRe99Or2+wVJsXLdlhJgC+gsIniYDfj/ZEBzCJ8Xm21ZG6YtMbR262CcS2w=="], "@animus-ui/react-router-app/@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], @@ -2285,6 +2312,26 @@ "wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "@animus-ui/next16-app/next/@next/env": ["@next/env@16.2.11", "", {}, "sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA=="], + + "@animus-ui/next16-app/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw=="], + + "@animus-ui/next16-app/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg=="], + + "@animus-ui/next16-app/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g=="], + + "@animus-ui/next16-app/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg=="], + + "@animus-ui/next16-app/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w=="], + + "@animus-ui/next16-app/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw=="], + + "@animus-ui/next16-app/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g=="], + + "@animus-ui/next16-app/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w=="], + + "@animus-ui/next16-app/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "@animus-ui/parity/oxc-parser/@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.139.0", "", { "os": "android", "cpu": "arm" }, "sha512-22EsXTA3Vc7OvrF4bfT48PFln2UbxkVgrp/Tm32qLw76Dv7SmcInfClJe6yPYamnli6HiqasnESZ5ezN+X4ybg=="], "@animus-ui/parity/oxc-parser/@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.139.0", "", { "os": "android", "cpu": "arm64" }, "sha512-uASQkZV+CUQ6xXvoMzDQyfhd8OMusdv2FyCPfAi5ZDYptesapJlw7erhpGi1Lc+U8QjQV2JUrIh7d/3su2LzmQ=="], diff --git a/e2e/next-app/scripts/assert-build.ts b/e2e/next-app/scripts/assert-build.ts index 92ef67e2..33914b87 100644 --- a/e2e/next-app/scripts/assert-build.ts +++ b/e2e/next-app/scripts/assert-build.ts @@ -17,7 +17,6 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const REPO_ROOT = resolve(APP_ROOT, '..', '..'); const NEXT_DIR = resolve(APP_ROOT, '.next'); const STATIC_JS = resolve(NEXT_DIR, 'static'); @@ -44,26 +43,12 @@ function emitLaneReceipt(): void { ); } - // Default engine is still MEASURED from the workspace plugin source so a - // future default change is reflected without touching this script. Symbol: - // `getSharedEngine()` fallback `... || 'v2'` in - // packages/next-plugin/src/singleton.ts. - const pluginSrc = readFileSync( - resolve(REPO_ROOT, 'packages', 'next-plugin', 'src', 'singleton.ts'), - 'utf8' - ); - const defaultMatch = pluginSrc.match(/\|\|\s*['"](v[12])['"]/); - if (!defaultMatch) { - throw new AssertionError( - 'Cannot determine default engine from packages/next-plugin/src/singleton.ts — update the receipt probe' - ); - } - const engineDefault = defaultMatch[1] as 'v2'; - - // v1 is retired: no override path remains, so the loaded engine is always the - // workspace default and no ANIMUS_ENGINE override is possible. + // v1 is retired (openspec: retire-extract-v1): v2 is the only engine, so the + // receipt records v2 as both default and loaded, with no override. Engine + // identity is never inferred from plugin/config source (guardrail G3). + const engineDefault = 'v2' as const; + const engineLoaded = 'v2' as const; const engineOverride = false; - const engineLoaded: 'v2' = engineDefault; // hostVersion from the fixture's installed host, not the manifest range. const hostVersion = ( diff --git a/e2e/next16-app/.gitignore b/e2e/next16-app/.gitignore new file mode 100644 index 00000000..69b1a97c --- /dev/null +++ b/e2e/next16-app/.gitignore @@ -0,0 +1 @@ +.animus/ diff --git a/e2e/next16-app/app/client/page.tsx b/e2e/next16-app/app/client/page.tsx new file mode 100644 index 00000000..56279fbc --- /dev/null +++ b/e2e/next16-app/app/client/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useState } from 'react'; + +import { Badge, Button, Card, Stack } from '../../src/components'; + +const sizes = ['small', 'medium', 'large'] as const; +const intents = ['primary', 'secondary', 'danger', 'ghost'] as const; + +export default function ClientPage() { + const [sizeIndex, setSizeIndex] = useState(0); + const [intentIndex, setIntentIndex] = useState(0); + const currentSize = sizes[sizeIndex % sizes.length]; + const currentIntent = intents[intentIndex % intents.length]; + + return ( + + +

Interactive Client Page

+

+ Variant toggling via useState — proves extracted CSS covers all states +

+
+ + + + + + + + + + Dynamic badge + +

+ Card with dynamic badge size tied to button state +

+
+
+
+ ); +} diff --git a/e2e/next16-app/app/layout.tsx b/e2e/next16-app/app/layout.tsx new file mode 100644 index 00000000..3e0d6fe0 --- /dev/null +++ b/e2e/next16-app/app/layout.tsx @@ -0,0 +1,15 @@ +export const metadata = { + title: 'Animus Next.js Test App', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/e2e/next16-app/app/page.tsx b/e2e/next16-app/app/page.tsx new file mode 100644 index 00000000..200e398b --- /dev/null +++ b/e2e/next16-app/app/page.tsx @@ -0,0 +1,96 @@ +import { + Badge, + Box, + Button, + Card, + Fade, + Family, + Pulse, + Stack, +} from '../src/components'; +import { ds } from '../src/ds'; + +const Heading = ds + .styles({ + fontSize: 48, + fontWeight: 700, + }) + .asElement('h1'); + +const Subheading = ds + .styles({ + color: 'text.muted', + }) + .asElement('p'); + +export default function Home() { + return ( + + + Next.js Test App + Extraction proof for webpack plugin + + + + + + + + + + + + Flat + + + Raised + + + Floating + + + + + + Info + + + Large Info + + + Compound (small+danger=bold) + + + Large Danger + + + + + + + + + + + + + + RSC Page — no compose() here + + + + + Pulse + Fade In + + + ); +} diff --git a/e2e/next16-app/next-env.d.ts b/e2e/next16-app/next-env.d.ts new file mode 100644 index 00000000..9edff1c7 --- /dev/null +++ b/e2e/next16-app/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/e2e/next16-app/next.config.ts b/e2e/next16-app/next.config.ts new file mode 100644 index 00000000..d8fbfc60 --- /dev/null +++ b/e2e/next16-app/next.config.ts @@ -0,0 +1,6 @@ +import { withAnimus } from '@animus-ui/next-plugin'; + +export default withAnimus({ + verbose: true, + system: './src/ds.ts', +})({}); diff --git a/e2e/next16-app/package.json b/e2e/next16-app/package.json new file mode 100644 index 00000000..4bf0068e --- /dev/null +++ b/e2e/next16-app/package.json @@ -0,0 +1,25 @@ +{ + "name": "@animus-ui/next16-app", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "verify:build": "bash ../../scripts/verify/build-consumer.sh", + "verify:assert": "bash ../../scripts/verify/assert-consumer.sh e2e/next16-app/.next e2e/next16-app/scripts/assert-build.ts", + "verify": "vp run @animus-ui/next16-app#verify:build && vp run @animus-ui/next16-app#verify:assert", + "start": "next start" + }, + "dependencies": { + "@animus-ui/next-plugin": "workspace:*", + "@animus-ui/system": "workspace:*", + "@animus-ui/test-ds": "workspace:*", + "next": "^16.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@animus-ui/assertions": "workspace:*", + "typescript": "^5.9.2" + } +} diff --git a/e2e/next16-app/scripts/assert-build.ts b/e2e/next16-app/scripts/assert-build.ts new file mode 100644 index 00000000..acc0abbc --- /dev/null +++ b/e2e/next16-app/scripts/assert-build.ts @@ -0,0 +1,154 @@ +import { + AssertionError, + assertClassNameFormat, + assertKeyframesExtracted, + assertLayerOrder, + assertNoEmotionImports, + assertNoPlaceholders, + findCssFiles, + findJsFiles, + layerBlock, + readAllConcat, + writeLaneReceipt, +} from '@animus-ui/assertions'; +import { readFileSync } from 'node:fs'; +import { readFile, stat } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const NEXT_DIR = resolve(APP_ROOT, '.next'); +const STATIC_JS = resolve(NEXT_DIR, 'static'); + +async function assertDir(path: string, label: string): Promise { + try { + const s = await stat(path); + if (!s.isDirectory()) throw new Error('not a directory'); + } catch { + throw new AssertionError( + `${label}: ${path} does not exist or is not a directory` + ); + } +} + +function emitLaneReceipt(): void { + // Retirement regression guard (openspec: retire-extract-v1): v2 is the only + // engine. The fixture config MUST NOT reference ANIMUS_ENGINE or set the + // engine option — either would reintroduce a retired v1 selection path. + const config = readFileSync(resolve(APP_ROOT, 'next.config.ts'), 'utf8'); + if (config.includes('ANIMUS_ENGINE') || /\bengine\s*:/.test(config)) { + throw new AssertionError( + 'next.config.ts must not reference ANIMUS_ENGINE or set the engine ' + + 'option — the v1 engine was retired (openspec: retire-extract-v1)' + ); + } + + // v1 is retired (openspec: retire-extract-v1): v2 is the only engine, so the + // receipt records v2 as both default and loaded, with no override. Engine + // identity is never inferred from plugin/config source (guardrail G3). + const engineDefault = 'v2' as const; + const engineLoaded = 'v2' as const; + const engineOverride = false; + + // hostVersion from the fixture's installed host, not the manifest range. + const hostVersion = ( + JSON.parse( + readFileSync( + resolve(APP_ROOT, 'node_modules', 'next', 'package.json'), + 'utf8' + ) + ) as { version: string } + ).version; + + writeLaneReceipt( + resolve(APP_ROOT, '.receipts', 'verify-assert-next16.json'), + { + lane: '@animus-ui/next16-app#verify:assert', + host: 'next', + hostVersion, + mode: 'production', + engineLoaded, + engineDefault, + engineOverride, + packageForm: 'workspace', + } + ); + console.log( + `[next16-app:assert] receipt → .receipts/verify-assert-next16.json (engine=${engineLoaded}, default=${engineDefault}, override=${engineOverride})` + ); +} + +async function main(): Promise { + await assertDir(NEXT_DIR, 'Next build output'); + + const cssFiles = await findCssFiles(NEXT_DIR); + if (cssFiles.length === 0) { + throw new AssertionError(`No CSS file found under ${NEXT_DIR}`); + } + const css = await readAllConcat(cssFiles); + + // Cascade order (relaxed) — same contract as vite-app and showcase. See + // `e2e/vite-app/scripts/assert-build.ts` for the TODO on re-enabling the + // stricter :root-before-@layer order post `fix-lightningcss-cascade`. + // + // §11.8 carry-forward from sessions 75/76: the previous shell script + // grepped for `@layer base` / `@layer variants`, but Animus actually emits + // `@layer anm-base` / `@layer anm-variants`. The layerBlock() helper uses + // the correct `anm-` prefix so that gap closes here. + assertLayerOrder(css, { + layers: [layerBlock('anm-base'), layerBlock('anm-variants')], + }); + + if (!css.includes(':root')) { + throw new AssertionError( + 'Expected a :root variable block in the CSS output' + ); + } + + assertNoPlaceholders(css); + + // Keyframes extracted through the Turbopack orchestration path — the + // fixture declares `animations = keyframes({ fadeIn, pulse })` in + // src/ds.ts; the assertion proves both blocks land in @layer anm-global, + // both animation-name refs resolve to a matching block, and neither got + // px-mangled by unit-fallback. + assertKeyframesExtracted(css, { + insideLayer: 'anm-global', + minBlocks: 2, + minReferences: 2, + }); + + // Class-name assertion runs on the full build output (JS + HTML emitted by + // Next may include the class names, not just the CSS). + const jsFiles = await findJsFiles(STATIC_JS); + const jsContent = await readAllConcat(jsFiles); + assertClassNameFormat(`${css}\n${jsContent}`, { prefix: 'animus-' }); + + for (const jsFile of jsFiles) { + const js = await readFile(jsFile, 'utf8'); + assertNoEmotionImports(js); + } + + // Router coverage — app router only (Turbopack build mode evidence; the + // pages router on Next 16 is deferred, openspec change + // next16-fixture-peer-range DEF-2). + await assertDir(resolve(NEXT_DIR, 'server', 'app'), 'App Router output'); + + console.log( + `[next16-app:assert] ${cssFiles.length} CSS file(s), ${jsFiles.length} JS file(s), App Router present — all assertions passed` + ); + + emitLaneReceipt(); +} + +main().catch((err) => { + if (err instanceof AssertionError) { + console.error(`[next16-app:assert] FAIL: ${err.message}`); + if (err.details) { + console.error(' details:', JSON.stringify(err.details, null, 2)); + } + } else { + console.error('[next16-app:assert] unexpected error:', err); + } + process.exit(1); +}); diff --git a/e2e/next16-app/src/components/Badge.tsx b/e2e/next16-app/src/components/Badge.tsx new file mode 100644 index 00000000..aab9501b --- /dev/null +++ b/e2e/next16-app/src/components/Badge.tsx @@ -0,0 +1,24 @@ +import { ds } from '../ds'; + +export const Badge = ds + .styles({ + display: 'inline-flex', + alignItems: 'center', + borderRadius: 4, + }) + .variant({ + prop: 'size', + variants: { + small: { fontSize: 14, px: 4, py: 0 }, + large: { fontSize: 16, px: 8, py: 4 }, + }, + }) + .variant({ + prop: 'intent', + variants: { + info: { bg: 'primary', color: 'background' }, + danger: { bg: 'danger', color: 'background' }, + }, + }) + .compound({ size: 'small', intent: 'danger' }, { fontWeight: 700 }) + .asElement('span'); diff --git a/e2e/next16-app/src/components/Box.tsx b/e2e/next16-app/src/components/Box.tsx new file mode 100644 index 00000000..36c13966 --- /dev/null +++ b/e2e/next16-app/src/components/Box.tsx @@ -0,0 +1,9 @@ +import { ds } from '../ds'; + +export const Box = ds + .styles({ + display: 'flex', + position: 'relative', + }) + .system({ space: true, layout: true, positioning: true }) + .asElement('div'); diff --git a/e2e/next16-app/src/components/Button.tsx b/e2e/next16-app/src/components/Button.tsx new file mode 100644 index 00000000..0991907d --- /dev/null +++ b/e2e/next16-app/src/components/Button.tsx @@ -0,0 +1,59 @@ +import { ds } from '../ds'; + +export const Button = ds + .styles({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + border: 'none', + borderRadius: 4, + cursor: 'pointer', + fontWeight: 600, + lineHeight: 'tight', + transition: 'all 150ms ease', + }) + .variant({ + prop: 'size', + variants: { + small: { fontSize: 14, px: 8, py: 4 }, + medium: { fontSize: 16, px: 16, py: 8 }, + large: { fontSize: 20, px: 24, py: 16, borderRadius: 8 }, + }, + }) + .variant({ + prop: 'intent', + variants: { + primary: { + bg: 'primary', + color: 'background', + '&:hover': { + bg: '{colors.primary/90}', + }, + }, + secondary: { + bg: 'secondary', + color: 'background', + '&:hover': { + bg: '{colors.secondary/90}', + }, + }, + danger: { + bg: 'danger', + color: 'background', + '&:hover': { + bg: '{colors.danger/90}', + }, + }, + ghost: { + bg: 'transparent', + color: 'text', + border: '1px solid', + borderColor: 'border', + }, + }, + }) + .states({ + hover: { opacity: '0.85', boxShadow: 'sm' }, + disabled: { opacity: '0.5', cursor: 'not-allowed' }, + }) + .asElement('button'); diff --git a/e2e/next16-app/src/components/Card.tsx b/e2e/next16-app/src/components/Card.tsx new file mode 100644 index 00000000..c60b004b --- /dev/null +++ b/e2e/next16-app/src/components/Card.tsx @@ -0,0 +1,30 @@ +import { size } from '@animus-ui/system'; + +import { ds } from '../ds'; + +export const Card = ds + .styles({ + display: 'flex', + flexDirection: 'column', + borderRadius: 8, + bg: 'surface', + border: '1px solid', + borderColor: 'border', + boxShadow: 'md', + p: 16, + }) + .variant({ + prop: 'elevation', + variants: { + flat: { boxShadow: 'sm' }, + raised: { boxShadow: 'md' }, + floating: { boxShadow: 'lg' }, + }, + }) + .props({ + sizing: { + property: 'flexBasis', + transform: size, + } as const, + }) + .asElement('div'); diff --git a/e2e/next16-app/src/components/Family.tsx b/e2e/next16-app/src/components/Family.tsx new file mode 100644 index 00000000..d8ff3d50 --- /dev/null +++ b/e2e/next16-app/src/components/Family.tsx @@ -0,0 +1,39 @@ +import { compose } from '@animus-ui/system'; + +import { ds } from '../ds'; + +const Parent = ds + .styles({ display: 'flex', alignItems: 'center' }) + .variant({ + prop: 'density', + variants: { + compact: { p: 4, fontSize: 14 }, + comfortable: { p: 16, fontSize: 20 }, + }, + }) + .asElement('div'); + +const Descendant = ds + .styles({ display: 'inline-flex' }) + .variant({ + prop: 'density', + variants: { + compact: { px: 4 }, + comfortable: { px: 8 }, + }, + }) + .variant({ + prop: 'intent', + variants: { + primary: { bg: 'primary', color: 'background' }, + secondary: { bg: 'secondary', color: 'background' }, + }, + }) + .asElement('span'); + +const Family = compose( + { Root: Parent, Child: Descendant }, + { shared: { density: true } } +); + +export { Family }; diff --git a/e2e/next16-app/src/components/Pulse.tsx b/e2e/next16-app/src/components/Pulse.tsx new file mode 100644 index 00000000..2f986e54 --- /dev/null +++ b/e2e/next16-app/src/components/Pulse.tsx @@ -0,0 +1,28 @@ +import { animations, ds } from '../ds'; + +export const Pulse = ds + .styles({ + bg: 'primary', + color: 'text', + px: 16, + py: 8, + borderRadius: 4, + animationName: animations.pulse, + animationDuration: '2s', + animationTimingFunction: 'ease-in-out', + animationIterationCount: 'infinite', + }) + .asElement('span'); + +export const Fade = ds + .styles({ + bg: 'surface', + color: 'text', + px: 16, + py: 8, + borderRadius: 4, + animationName: animations.fadeIn, + animationDuration: '1s', + animationFillMode: 'forwards', + }) + .asElement('span'); diff --git a/e2e/next16-app/src/components/Stack.tsx b/e2e/next16-app/src/components/Stack.tsx new file mode 100644 index 00000000..2d57f590 --- /dev/null +++ b/e2e/next16-app/src/components/Stack.tsx @@ -0,0 +1,18 @@ +import { ds } from '../ds'; + +export const Stack = ds + .styles({ + display: 'flex', + flexDirection: 'column', + gap: 16, + }) + .variant({ + prop: 'direction', + defaultVariant: 'column', + variants: { + column: { flexDirection: 'column' }, + row: { flexDirection: 'row', alignItems: 'center' }, + }, + }) + .system({ space: true, layout: true }) + .asElement('div'); diff --git a/e2e/next16-app/src/components/index.ts b/e2e/next16-app/src/components/index.ts new file mode 100644 index 00000000..74d6b4ff --- /dev/null +++ b/e2e/next16-app/src/components/index.ts @@ -0,0 +1,7 @@ +export { Badge } from './Badge'; +export { Box } from './Box'; +export { Button } from './Button'; +export { Card } from './Card'; +export { Family } from './Family'; +export { Fade, Pulse } from './Pulse'; +export { Stack } from './Stack'; diff --git a/e2e/next16-app/src/ds.ts b/e2e/next16-app/src/ds.ts new file mode 100644 index 00000000..9640ecc6 --- /dev/null +++ b/e2e/next16-app/src/ds.ts @@ -0,0 +1,216 @@ +import { createSystem, createTheme, createTransform } from '@animus-ui/system'; +import { + border, + color, + flex, + layout, + positioning, + shadows, + space, + typography, +} from '@animus-ui/system/groups'; +import { ds as testDs } from '@animus-ui/test-ds'; + +// ─── Transforms ───────────────────────────────────────────── + +export const size = createTransform('size', (value) => { + const num = Number(value); + if (!Number.isNaN(num) && num !== 0) return `${num}px`; + return String(value); +}); + +// ─── Tokens ───────────────────────────────────────────────── + +export const tokens = createTheme() + .addBreakpoints({ sm: 640, md: 768, lg: 1024, xl: 1280 }) + .addColors({ + gray: { + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#e5e5e5', + 300: '#d4d4d4', + 400: '#a3a3a3', + 500: '#737373', + 600: '#525252', + 700: '#404040', + 800: '#262626', + 900: '#171717', + 950: '#0a0a0a', + }, + blue: { + 50: '#eff6ff', + 100: '#dbeafe', + 300: '#93c5fd', + 500: '#3b82f6', + 700: '#1d4ed8', + 900: '#1e3a5f', + }, + red: { + 50: '#fef2f2', + 100: '#fee2e2', + 300: '#fca5a5', + 500: '#ef4444', + 700: '#b91c1c', + 900: '#7f1d1d', + }, + green: { + 50: '#f0fdf4', + 100: '#dcfce7', + 300: '#86efac', + 500: '#22c55e', + 700: '#15803d', + 900: '#14532d', + }, + amber: { + 50: '#fffbeb', + 100: '#fef3c7', + 300: '#fcd34d', + 500: '#f59e0b', + 700: '#b45309', + 900: '#78350f', + }, + }) + .addColorModes('dark', { + dark: { + primary: { _: 'blue.500', hover: 'blue.700' }, + secondary: 'green.500', + accent: 'amber.500', + danger: { _: 'red.500', hover: 'red.700' }, + background: 'gray.950', + surface: { _: 'gray.800', hover: 'gray.700' }, + text: { _: 'gray.100', muted: 'gray.400' }, + border: { _: 'gray.600', strong: 'gray.500' }, + code: { _: 'gray.800', text: 'amber.300' }, + }, + light: { + primary: { _: 'blue.700', hover: 'blue.500' }, + secondary: 'green.700', + accent: 'amber.700', + danger: { _: 'red.700', hover: 'red.500' }, + background: 'gray.50', + surface: { _: 'gray.200', hover: 'gray.300' }, + text: { _: 'gray.900', muted: 'gray.500' }, + border: { _: 'gray.300', strong: 'gray.400' }, + code: { _: 'gray.100', text: 'blue.700' }, + }, + }) + .addScale({ + name: 'space', + values: { + 0: '0', + 1: '0.125rem', + 2: '0.25rem', + 4: '0.25rem', + 6: '0.375rem', + 8: '0.5rem', + 12: '0.75rem', + 16: '1rem', + 20: '1.25rem', + 24: '1.5rem', + 32: '2rem', + 48: '3rem', + 64: '4rem', + }, + }) + .addScale({ + name: 'fontSizes', + values: { + 12: '0.75rem', + 14: '0.875rem', + 16: '1rem', + 18: '1.125rem', + 20: '1.25rem', + 24: '1.5rem', + 32: '2rem', + 48: '3rem', + }, + }) + .addScale({ + name: 'fontWeights', + values: { + 300: '300', + 400: '400', + 500: '500', + 600: '600', + 700: '700', + }, + }) + .addScale({ + name: 'lineHeights', + values: { + tight: '1.25', + base: '1.5', + relaxed: '1.75', + }, + }) + .addScale({ + name: 'radii', + values: { + 0: '0', + 2: '2px', + 4: '4px', + 8: '8px', + 16: '16px', + full: '9999px', + }, + }) + .addScale({ + name: 'shadows', + values: { + sm: '0 1px 2px rgba(0,0,0,0.05)', + md: '0 4px 6px rgba(0,0,0,0.1)', + lg: '0 10px 15px rgba(0,0,0,0.15)', + }, + }) + .build(); + +export type TestTheme = typeof tokens; + +declare module '@animus-ui/system' { + interface Theme extends TestTheme {} +} + +// ─── System ───────────────────────────────────────────────── + +export const { + system: ds, + createGlobalStyles, + createKeyframes, +} = createSystem({ + includes: [testDs], +}) + .addGroup('space', space) + .addGroup('layout', { ...layout, ...flex }) + .addGroup('text', typography) + .addGroup('surface', { ...color, ...border, ...shadows }) + .addGroup('positioning', positioning) + .build(); + +// ─── Keyframes ────────────────────────────────────────────── + +export const animations = createKeyframes({ + fadeIn: { + '0%': { opacity: 0, bg: 'background' }, + '100%': { opacity: 1, bg: 'surface' }, + }, + pulse: { + '0%, 100%': { transform: 'scale(1)' }, + '50%': { transform: 'scale(1.05)' }, + }, +}); + +// ─── Global Styles ────────────────────────────────────────── + +export const globalStyles = createGlobalStyles({ + '*, *::before, *::after': { boxSizing: 'border-box' }, + html: { WebkitFontSmoothing: 'antialiased' }, + body: { + m: 0, + fontFamily: 'system-ui, -apple-system, sans-serif', + lineHeight: '1.5', + bg: 'background', + color: 'text', + }, + a: { color: 'primary', textDecoration: 'none' }, + 'code, kbd': { fontFamily: 'ui-monospace, monospace', fontSize: 14 }, +}); diff --git a/e2e/next16-app/styles.d.ts b/e2e/next16-app/styles.d.ts new file mode 100644 index 00000000..35306c6f --- /dev/null +++ b/e2e/next16-app/styles.d.ts @@ -0,0 +1 @@ +declare module '*.css'; diff --git a/e2e/next16-app/tsconfig.json b/e2e/next16-app/tsconfig.json new file mode 100644 index 00000000..1a941e51 --- /dev/null +++ b/e2e/next16-app/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "module": "esnext", + "moduleResolution": "bundler", + "noEmit": true, + "plugins": [ + { + "name": "next" + } + ], + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true + }, + "include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", ".next/types/**/*.ts"], + "exclude": ["node_modules", ".next"] +} diff --git a/e2e/react-router-app/scripts/assert-build.ts b/e2e/react-router-app/scripts/assert-build.ts index 5e25dfc8..749aa0da 100644 --- a/e2e/react-router-app/scripts/assert-build.ts +++ b/e2e/react-router-app/scripts/assert-build.ts @@ -4,30 +4,37 @@ import { assertLayerOrder, assertNoEmotionImports, assertNoPlaceholders, - findCssFiles, findJsFiles, layerBlock, readAllConcat, + readRequiredCss, } from '@animus-ui/assertions'; import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; const BUILD = resolve(import.meta.dirname, '..', 'build'); +// Wrangler serves build/client; semantic CSS must be proven there independently +// from build/server (canary delta: React Router served-client CSS proof). +const CLIENT_ROOT = resolve(BUILD, 'client'); async function main(): Promise { - const cssFiles = await findCssFiles(BUILD); - if (cssFiles.length === 0) - throw new AssertionError(`No CSS file found under ${BUILD}`); - const css = await readAllConcat(cssFiles); + const css = await readRequiredCss( + CLIENT_ROOT, + 'react-router served-client CSS (build/client)' + ); assertLayerOrder(css, { layers: [layerBlock('anm-base'), layerBlock('anm-variants')], }); if (!css.includes(':root')) - throw new AssertionError('Expected a :root variable block'); + throw new AssertionError( + 'Expected a :root variable block in React Router served-client CSS (build/client)' + ); assertNoPlaceholders(css); + assertClassNameFormat(css, { prefix: 'animus-' }); + + // JS/hydration discovery keeps its own scope over the whole build root. const jsFiles = await findJsFiles(BUILD); const js = await readAllConcat(jsFiles); - assertClassNameFormat(`${css}\n${js}`, { prefix: 'animus-' }); if (!js.includes('React Router v8 SSR canary')) throw new AssertionError('SSR marker missing'); if (!js.includes('React Router v8 client canary')) @@ -35,7 +42,7 @@ async function main(): Promise { for (const file of jsFiles) assertNoEmotionImports(await readFile(file, 'utf8')); console.log( - `[react-router-app:assert] ${cssFiles.length} CSS, ${jsFiles.length} JS — all assertions passed` + `[react-router-app:assert] served-client CSS (build/client) + ${jsFiles.length} JS — all assertions passed` ); } diff --git a/e2e/vinext-app/scripts/assert-build.ts b/e2e/vinext-app/scripts/assert-build.ts index 3920a788..4237af69 100644 --- a/e2e/vinext-app/scripts/assert-build.ts +++ b/e2e/vinext-app/scripts/assert-build.ts @@ -4,33 +4,38 @@ import { assertLayerOrder, assertNoEmotionImports, assertNoPlaceholders, - findCssFiles, findJsFiles, layerBlock, readAllConcat, + readRequiredCss, } from '@animus-ui/assertions'; import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; const DIST = resolve(import.meta.dirname, '..', 'dist'); +// Wrangler serves dist/client; semantic CSS must be proven there independently +// from dist/server (canary delta: Vinext served-client CSS proof). +const CLIENT_ROOT = resolve(DIST, 'client'); async function main(): Promise { - const cssFiles = await findCssFiles(DIST); - if (cssFiles.length === 0) { - throw new AssertionError(`No CSS file found under ${DIST}`); - } - const css = await readAllConcat(cssFiles); + const css = await readRequiredCss( + CLIENT_ROOT, + 'vinext served-client CSS (dist/client)' + ); assertLayerOrder(css, { layers: [layerBlock('anm-base'), layerBlock('anm-variants')], }); if (!css.includes(':root')) { - throw new AssertionError('Expected a :root variable block in Vinext CSS'); + throw new AssertionError( + 'Expected a :root variable block in Vinext served-client CSS (dist/client)' + ); } assertNoPlaceholders(css); + assertClassNameFormat(css, { prefix: 'animus-' }); + // JS/hydration discovery keeps its own scope over the whole build root. const jsFiles = await findJsFiles(DIST); const js = await readAllConcat(jsFiles); - assertClassNameFormat(`${css}\n${js}`, { prefix: 'animus-' }); if (!js.includes('Vinext RSC canary')) { throw new AssertionError('App Router RSC marker missing from build'); } @@ -42,7 +47,7 @@ async function main(): Promise { } console.log( - `[vinext-app:assert] ${cssFiles.length} CSS file(s), ${jsFiles.length} JS file(s), App+Pages routers present — all assertions passed` + `[vinext-app:assert] served-client CSS (dist/client) + ${jsFiles.length} JS file(s), App+Pages routers present — all assertions passed` ); } diff --git a/openspec/specs/arch-workspace-topology/spec.md b/openspec/specs/arch-workspace-topology/spec.md new file mode 100644 index 00000000..a1cbe9d1 --- /dev/null +++ b/openspec/specs/arch-workspace-topology/spec.md @@ -0,0 +1,39 @@ +# arch-workspace-topology Specification + +## Purpose +TBD - created by archiving change enforce-workspace-topology. Update Purpose after archive. +## Requirements +### Requirement: Workspace dependency boundaries have an executable check + +The repository SHALL provide a named executable check that rejects source imports, +TypeScript path aliases, and workspace package dependencies that violate the documented +dependency direction among `packages/`, `e2e/`, and `legacy/`. + +#### Scenario: Forbidden package-to-e2e dependency fails + +- **WHEN** a package source file, TypeScript alias, or workspace dependency points from `packages/*` to `e2e/*` +- **THEN** the workspace-topology check SHALL exit non-zero +- **AND** its output SHALL identify the offending file + +#### Scenario: Active code cannot depend on legacy + +- **WHEN** source under `packages/*` or `e2e/*` references a path under `legacy/*` +- **THEN** the workspace-topology check SHALL exit non-zero +- **AND** its output SHALL identify the offending file + +#### Scenario: Current dependency direction passes + +- **WHEN** `e2e/*` consumes `packages/*` without any reverse or legacy dependency +- **THEN** the workspace-topology check SHALL exit zero + +### Requirement: Workspace topology enforcement joins repository verification + +The workspace-topology check SHALL be reachable through a named repository verification +command and SHALL fail loud without silently repairing or rebuilding inputs. + +#### Scenario: Maintainer runs the topology verifier + +- **WHEN** a maintainer invokes the named verification command +- **THEN** it SHALL run the workspace boundary scan +- **AND** any violation SHALL make the command exit non-zero with an actionable path list + diff --git a/openspec/specs/code-hygiene/spec.md b/openspec/specs/code-hygiene/spec.md index f605e244..00a04a60 100644 --- a/openspec/specs/code-hygiene/spec.md +++ b/openspec/specs/code-hygiene/spec.md @@ -3,9 +3,7 @@ ## Purpose TBD - created by archiving change add-code-hygiene-protocol. Update Purpose after archive. - ## Requirements - ### Requirement: Single flag-driven hygiene entrypoint The repository SHALL provide a single code-hygiene entrypoint invokable as `bun run hygiene` (or equivalently `bash scripts/hygiene/run.sh`) that accepts `--mode`, `--scope`, `--base`, and `--iterations` flags. The entrypoint SHALL be usable identically by humans and by agents through the Bash tool — no agent-specific wrapper SHALL be required. @@ -435,3 +433,18 @@ The end-of-work-only contract is invariant under any dispatch surface — `vp ru - **WHEN** the repo's `.github/workflows/*.yaml` files are inspected - **THEN** no step invokes `vp run hygiene`, `bun run hygiene`, or `bash scripts/hygiene/run.sh` - **AND** the end-of-work-only contract holds regardless of which dispatch surface a workflow author might attempt + +### Requirement: Risky file deletion blocks completion + +The hygiene orchestrator SHALL exit non-zero for a run containing a Layer D whole-file deletion unless the same run records the required behavior-build proof. + +#### Scenario: Whole file is deleted without build proof + +- **WHEN** the receipt stream contains `layer="D"`, `verb="delete"`, and `kind="file"` and no behavior-build proof is recorded +- **THEN** the verdict requires manual review and the orchestrator exits non-zero after its safety envelope + +#### Scenario: Export-only cleanup completes + +- **WHEN** the receipt stream contains no whole-file deletion and the cascade converges with a passing safety envelope +- **THEN** the whole-file deletion policy does not change the successful exit status + diff --git a/openspec/specs/css-post-processing/spec.md b/openspec/specs/css-post-processing/spec.md index d8605607..41f0dad2 100644 --- a/openspec/specs/css-post-processing/spec.md +++ b/openspec/specs/css-post-processing/spec.md @@ -1,12 +1,10 @@ ## Purpose Requirements for the `css-post-processing` capability: CSS post-processing function; Post-processing operates on body content only; @layer block preservation; and 7 more. - ## Requirements - ### Requirement: CSS post-processing function -The Vite plugin SHALL expose an internal `postProcessCss()` function that takes a CSS string, processing mode, and browser targets, and returns a processed CSS string. The function SHALL use Lightning CSS (`lightningcss` npm package) for minification and autoprefixing. +The shared extraction pipeline (`@animus-ui/extract/pipeline`) SHALL expose a `postProcessCss()` function that takes a CSS string, processing mode, and browser targets, and returns a processed CSS string, plus a `resolveLightningTargets()` resolver (explicit config → project browserslist → defaults). The function SHALL use Lightning CSS (`lightningcss` npm package) for minification and autoprefixing. Both the Vite plugin and the Next.js plugin SHALL consume this single implementation. #### Scenario: Prod mode minifies and autoprefixes @@ -23,6 +21,11 @@ The Vite plugin SHALL expose an internal `postProcessCss()` function that takes - **WHEN** `postProcessCss()` is called with no explicit targets - **THEN** the function SHALL use the resolved default targets (browserslist query or built-in defaults) +#### Scenario: Single implementation across plugins + +- **WHEN** the Vite and Next.js plugin sources are inspected +- **THEN** neither SHALL carry a local Lightning CSS processing implementation — both import the pipeline's + ### Requirement: Post-processing operates on body content only The vite-plugin `postProcessCss` function SHALL receive only the body segment (global CSS + component CSS) from the split assembly, never the `@layer` cascade declaration or variable CSS. The final virtual module output is reconstructed by concatenating the unprocessed variables with the processed body. The `@layer` declaration is delivered separately via HTML `