Skip to content

fix(extractor): stabilize names across build environments - #764

Draft
owjs3901 wants to merge 23 commits into
mainfrom
fix/environment-stable-names
Draft

owjs3901 wants to merge 23 commits into
mainfrom
fix/environment-stable-names

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Refs #694 (G: 환경 및 컴파일러 처리 순서와 무관한 클래스·동적 변수 이름). #751(aaab756) 위에 쌓았으며 #743을 포함한 #751이 먼저 병합되어야 합니다. Base는 main이며 자동 병합하지 않습니다.

요약

환경마다 import 값이 정상적으로 달라도 프로그램을 계속 컴파일하면서 이름과 최종 CSS/JS를 처리 순서에 독립적으로 만듭니다. 환경 간 사전 수집, resolver 의미 변경, 새 의존성 또는 styling runtime은 없습니다. ce6296e0은 static/scope/naming-root/원본 counter/cache v4를 함께 검증한 커밋이고, a9c804a3은 동적 typography와 class-only 평가 순서를 별도로 고친 커밋입니다. 두 커밋 모두 격리된 전체 트리의 실제 hooks를 통과하고 푸시했습니다. 현재 남은 release gate는 Linux 100% coverage입니다.

동작

  • 파일 자체의 소스와 테마, 같은 파일 안에서 닫힌 initializer chain으로 CSS 내용·존재가 결정되는 Own atom은 원본 D9 파일의 짧은 counter를 씁니다. Named/default/namespace/re-export/require/dynamic import에 의존하는 값·스타일 선택 조건·selector·key·spread·반응형 형태/길이는 Risky content 이름이며 counter slot을 쓰지 않습니다. 같은 내용이어도 Own/Risky identity를 합치지 않습니다. Devup package 경계는 정확한 이름 또는 / subpath이며 lookalike는 외부 import입니다.
  • Canonical bucket에 여러 원본 파일이 합쳐져도 CounterOwner는 원본 D9 identity를 유지합니다. 번호 없는 원본이 번호 있는 canonical root의 counter를 빌리지 않습니다. Class allocation과 registry가 같은 owner/eligibility 규칙을 사용하며 진단 위치는 identity에 영향을 주지 않습니다.
  • Content 이름은 실제 effective 선언/selector/at-rule/breakpoint/order/layer 및 keyframes 순서의 내용만으로 정합니다. <O|R|K><L|H><payload>에서 L은 lossless, H는 dependency-free 고정 SHA-256 앞 80 bits의 16자리 padded base-37입니다. 더 짧은 이름을 고르고 동률은 L입니다. 두 문법은 겹치지 않으며 fingerprint claim은 전체 descriptor를 정확히 비교합니다. Native/WASM golden 및 production-default 80, 좁은 폭의 실제 충돌 회귀가 있습니다.
  • Birthday bound는 n(n-1)/(2*2^80)입니다. 백만 이름에서 약 4.14e-13, 1,150개에서 약 5.46e-19이며 72-bit 백만 이름은 약 1.06e-10입니다. 확률로 정확성을 대신하지 않습니다. 실제 서로 다른 descriptor가 같은 이름을 가지면 빌드가 실패합니다.
  • Content naming은 이름만 바꾸며 #751의 CSS sheet placement, 전달 횟수와 cascade를 바꾸지 않습니다. Per-file atom은 <prefix><D9-base37>-<O|R><L|H>… 또는 <prefix><F><L|H>…-<O|R><L|H>…로 구분됩니다. Unprefixed 공유 이름은 이미 한 sheet로 전달되는 order0 base, singleCss, hoisted sheet 및 keyframes에만 씁니다. Specificity bump, breakpoint cascade layer, runtime ordering은 없습니다. 새 atom grammar의 길이는 prefix+27(D9) / prefix+37(비번호), 기존 anti-ad splice를 포함합니다.
  • setNamingRoot(root, relative_base?)는 WASM의 이름 전용 입력입니다. Vite root, webpack/Rspack context와 기존 cwd-relative ID basis, Rsbuild root, Next project cwd(각 compiler/loader), Bun root를 추출 전에 전달합니다. 같은 build의 환경들은 같은 root를 씁니다. Resolver/extraction/canonical/route key, placement, D9 API는 바꾸지 않으며 reset은 이 입력을 지웁니다. 비번호 file key는 project-relative forward slash이고 외부 파일의 ../를 유지합니다. 새 scope claim은 root 절대경로 대신 이 key를 저장합니다.
  • 런타임 값의 CSS 변수는 content 이름이 아닙니다. 문법은 ---<prefix>S(<D9-base37>|UL<escaped-normalized-source>|UH<16-base37>)-<position-base37>[-<nonzero-role-base37>]입니다. File part는 최대18 ASCII bytes, 전체는 prefix+50(native64)/+38(wasm32), role0은 +36/+30입니다. 숫자 alphabet과 separator/domain 문법은 겹치지 않으며 theme 변수와도 분리됩니다.
  • Source position은 Devup이 받은 소스의 offset에서 자체 prelude, 선행 BOM 하나와 이전 CR만 뺀 값입니다. Upstream source map으로 바꾸지 않습니다. LF/CRLF/BOM의 전체 출력이 같습니다. 같은 소스 텍스트와 테마/이름 설정을 받는 환경에서는 이름이 같고, 편집으로 site가 이동하면 dev 이름은 바뀔 수 있습니다. 비번호 source fingerprint에는 전체 정규화 source claim을 먼저 검사합니다.
  • 생성 변수는 요소 자신의 dynamic class에 unconditional initial reset 하나를 둡니다. Plain base rule에 합치거나 별도 plain rule로 내며 pseudo-element/descendant 소비자 또는 media/hover 조건 안에만 reset하지 않습니다. Theme 변수는 reset하지 않습니다. 같은 site/다른 site의 중첩, 상속 property, pseudo/descendant, responsive/hover 소비자가 Chromium gate에 포함됩니다.
  • Version4 exportSheet는 claims, class/file map, original IDs, atom plan을 포함하는 self-contained snapshot이며 유일한 restore authority입니다. Companion map은 스스로 state를 추가하지 않습니다. 버전 누락/이전 버전/해독 불가/구조 오류/companion 불일치는 전체 cache miss로 cold와 같은 출력으로 다시 씁니다. Reset-consumer proof, dense allocator와 선언/slot/source 대응을 검사하고 rollback은 fresh seed 호출 순서까지 보존합니다. 유효한 현재 claim의 실제 충돌만 located sticky 오류이며 reset까지 유지됩니다. 실제 main cache, 보존한 landing4 파일, WASM에 도달한 truncated 값 및 양쪽 restore 순서를 검증했습니다.
  • 평가가 없는 literal/local-constant/theme 배열은 #751처럼 static className으로 컴파일하며 불필요한 tuple/IIFE를 만들지 않습니다. 평가가 있는 표현식만 source의 순서·횟수·lazy branch를 유지합니다. Import된 JSX render condition이 양쪽 potential element의 추출을 바꾸지 않으면 그 아래 literal atom은 Own을 유지합니다. 실제 true/false/unresolved provider 모두 같은 CSS/class를 만드는 회귀가 있습니다.
  • 동적 typography의 identifier/member/computed/call/conditional 및 className 앞/뒤 위치는 한 번 평가한 typo class selection입니다. CSS 변수나 typography: 선언으로 내리지 않습니다. Responsive/scoped preset, positioning의 조건부 styleOrder, template coercion와 기존 spread-after 계약을 유지합니다. Body14px/heading24px의 실제 서로 다른 선언으로 선택/범위 assertions를 유지하고 226 native 및 92 fresh-WASM 회귀를 검증했습니다.
  • 기존 a && b style guard는 controller가 false/null/undefined/0/빈 문자열/NaN이면 스타일 없음입니다. a는 한 번, b는 실행하지 않습니다. a=0에서 일반 JavaScript가 0을 반환하는 것과 다른 기존 convention을 명시합니다.

새로 생기는 오류

  • 서로 다른 content/source/scope descriptor가 같은 fingerprint 이름이면 sheet mutation 전에 오류입니다. 양쪽 전체 내용과 수정 방법을 출력하며 위치는 exact source witness → 사용자가 작성한 가장 바깥 생산 호출의 실제 Boa 위치(produced by the call at file:line:col) → stylesheet 파일/export binding(position unavailable) 순서입니다. 생성 코드나 추측 좌표를 출력하지 않습니다.
  • 하나의 sparse role에 다른 assignment가 들어오면 실제 위치/표현식을 포함한 identity 오류입니다. 환경별 합법적 값 차이는 오류가 아닙니다. 이전/손상 cache 자체는 새 빌드 오류가 아니며 cold로 재생성됩니다. 새 JavaScript wrapper는 없습니다.

남는 한계

  • fix(extractor): preserve JavaScript semantics across values and element calls #756 unknown spread의 explicit/undefined/style/className precedence, JSX-02 fallback 및 spread-only w={({...theme.more})[key]}는 여기서 구현하거나 테스트하지 않습니다. Supported overwritten-key 경로는 유지하며 두 PR 병합 후 결합 gate가 필요합니다.
  • #750이 고치는 inherited lexical shadowing, fix(extractor): locate diagnostics and enforce deterministic evaluation #757 lookalike package의 end-to-end constant inlining은 별도 스택입니다. Unknown call로 탈출한 객체를 잘못 constant-fold하는 inherited 문제도 별도 수정으로 이관합니다.
  • 다른 drive/filesystem root의 파일은 normalized absolute key로 fallback하므로 그 경우 checkout-independent 상대 key를 보장하지 않습니다. 관찰 불가능한 eval/callback은 module/export 위치만 보고하며 위치를 얻으려고 코드를 추가 실행하지 않습니다.
  • Plugin/loader가 WASM 호출 전에 JSON.parse에 실패한 파일은 main의 기존 JSON 오류를 유지합니다. Atomic cache write/parse-as-absent는 fix(plugin-core): align bundler resolution, roots and WASM diagnostics #755/fix(next-plugin): isolate coordinator sessions and enforce complete CSS #752 follow-up입니다. 일반 authored .js를 제외하는 Bun filter는 inherited fix(plugin-core): align bundler resolution, roots and WASM diagnostics #755/W20e 한계이며 이 PR에서 바꾸지 않습니다.
  • #755와 #752가 같은 plugin 파일을 바꾸므로 나중에 병합되는 쪽이 setNamingRoot call site를 reconcile해야 합니다. Vite fix(vite-plugin): keep server page css dependencies in vinext/RSC builds #747 보호 영역은 origin/main과 byte-identical입니다.
  • Next16.3.6 webpack은 Devup 없는 ordinary-CSS control에서도 absolute config/appDir/route paths와 path-derived module IDs/chunk hashes를 넣으므로 두 checkout의 전체 bundle byte equality는 성립하지 않습니다. 승인된 gate는 모든 Devup CSS, 모든 post-loader user module source, 모든 route server HTML의 class/CSS-variable 이름이 byte-identical이며 same-physical-roots control보다 추가 differing role/chunk가0임을 검사합니다. Control receipt W31h-approved-next-tUU2wH/approved-receipt.json은 양쪽8 differing roles/추가0을 보존합니다. 다른4 plugin은 모든 CSS/JS 파일 이름과 raw hash 전체를 비교합니다.

크기

같은 Bun gzipSync per-file 합산으로 CSS/JS raw·gzip을 측정했습니다. 8-route fixture는 원래 w31-measure.mjs의 8 routes/32 shared×80 atoms/route당25 private atoms와 Bun.Transpiler-minified extracted source이며 bundled JS가 아닙니다. 이 fixture는 historical method처럼 NODE_ENV를 unset했습니다. Landing은 각 원본 cwd의 실제 vinext dist/client 전부이며 production 환경입니다. #751은 독립 w31f751의 자체 binary/dependencies/cwd에서 같은 mode로 다시 측정했습니다. 각 mode 전에4 generated cache JSON과 기존 dist를 고유 T backup으로 보존하여 cold로 빌드했습니다. Variable/reset/file-prefix/evaluation bytes를 모두 포함합니다.

AtomHoist2에만 각 쪽의 Devup options 한 줄을 잠시 바꾸고 즉시 원래 bytes로 복구했습니다. 양쪽 rawSHA 81e66b8378fd646841281a1aa6ff6aea98f9dbb6734bea938429e206d8befe70, current diff empty 및 HEAD blob 54635f970fdd24c5d95a790369bf5735171be370을 검증했습니다. Generated/ignored next-env.d.ts는 source pin에서 제외하되 전후 hash를 output receipt에 별도로 기록했습니다. 모든 authored source/package/config pin은 동일하며 시작한 process는 종료했습니다.

Fixture / Mode #751 CSS Raw / Gzip Current CSS Raw / Gzip #751 JS Raw / Gzip Current JS Raw / Gzip CSS+JS Gzip Δ
8 routes ordinary 69604 / 19138 69604 / 19138 206438 / 14081 206438 / 14081 0.00%
8 routes singleCss 6310 / 1465 10806 / 3792 200694 / 13876 244560 / 34235 +147.88%
8 routes atomHoist2 30352 / 7180 11526 / 6336 346366 / 20718 241670 / 32384 +38.79%
landing ordinary 60265 / 28819 73443 / 33321 686594 / 219818 716659 / 228739 +5.40%
landing singleCss 35639 / 8389 48394 / 15308 680758 / 218628 709234 / 230346 +8.21%
landing atomHoist2 346589 / 34180 68026 / 23055 905078 / 237024 716113 / 228693 -7.17%

Ordinary static fixture CSS/JS는 #751과 크기가 같습니다. 공유 namespace의 결정론적 content 이름과 runtime site/reset/evaluation 보존 비용은 표에서 숨기지 않았습니다. Landing ordinary/singleCss의 합산 gzip 증가가1%를 넘으므로 전체 표를 coordinator에게 먼저 알렸습니다. Historical W31f landing 값도 별도로 보존했습니다: ordinary CSS60265/28815 JS686594/219800; single CSS35639/8392 JS680758/218625; atom CSS355317/34322 JS911062/237139. 표는 새 original-cwd/cold before와 비교하며 이전 값으로 조용히 대체하지 않습니다. 모든 실제12 rows와 asset hashes는 W31h-size-original-7XzdEu/six-mode-comparison-historical-env.json에 있습니다.

Landing 비용 분해

새 build 없이 보존한 ordinary/singleCss client assets의 원래 SHA/raw/gzip을 먼저 검증하고 w31h-size-breakdown.mjs로 계산했습니다(4.28초). 표의 각 칸은 raw/gzip bytes입니다. CSS selector에 있는 content class token, 그 file-scope prefix, CSS에 선언/참조되는 변수 이름, 생성된 triple-hyphen initial reset 선언을 분리했습니다. 기존 counter/theme/keyframe/typography class 이름은 other에 남습니다. Reset은 변수 이름까지 포함한 전체 선언이며 변수 항목과 중복하지 않습니다.

Gzip은 category를 독립 압축한 숫자가 아닙니다. 각 원래 파일에서 reset → variable name → file prefix → lossless atom → fingerprint atom 순서로 text를 제거하며 per-file Bun gzip의 차이를 계산한 순서 의존적 marginal ablation입니다. Other는 마지막 remainder와 gzip overhead입니다. 각 category의 raw/gzip 합은 실제 전체 합과 정확히 같고, 아래 delta의 합도 실제 증가분과 같습니다. 분석용 제거 결과를 실행하거나 CSS/JS 기대를 바꾸지 않았습니다.

Category Ordinary #751 → Current Raw/Gzip Ordinary Δ Raw/Gzip SingleCss #751 → Current Raw/Gzip SingleCss Δ Raw/Gzip
Content class atoms (L+H) 0/0 → 16533/10707 +16533/+10707 0/0 → 29034/18097 +29034/+18097
Content atom file prefixes 0/0 → 15537/654 +15537/+654 0/0 → 0/0 0/0
CSS variable names 4452/1785 → 5314/2022 +862/+237 3378/812 → 4294/1049 +916/+237
Generated resets 0/0 → 103/25 +103/+25 0/0 → 103/22 +103/+22
Other code / existing names 742407/246852 → 752615/248652 +10208/+1800 713019/226205 → 724197/226486 +11178/+281
Total 746859/248637 → 790102/262060 +43243/+13423 716397/227017 → 757628/245654 +41231/+18637

이 정의에서 fingerprint atom은 ordinary12924 raw/9278 gzip bytes, singleCss22392 raw/15592 gzip bytes입니다. 각각 합산 gzip 증가13423/18637 bytes의69.12%/83.66%를 차지합니다. 해시 이름의 압축 비용을 숨기지 않습니다. 전체 asset별 hashes/분해 수치는 W31h-size-breakdown.json에 보존했습니다.

결정론성을 유지할 수 있는 후속 lever는 compression-aware lossless/fingerprint 선택(현재 raw-shortest 정책 변경에 대한 별도 결정 필요), 동일 prefix/name 반복을 줄이는 정적 출력 형식, 원본 번호를 더 넓게 제공하는 plugin pre-pass(#755/#752 소유 범위) 등입니다. 이름에 필요한 content/범위/충돌 증명을 지우거나 counter eligibility를 넓히는 방식은 제안하지 않습니다. 이 PR에서는 해당 최적화나 새 runtime/precollection을 구현하지 않습니다. Coordinator는 size를 release blocker로 삼지 않고 비용을 사용자가 리뷰하도록 정확히 공개하기로 결정했습니다.

검증

  • Combined: 일반 native2847, freshWASM/root, Bun5589/0 및 함수/라인100%, fmt/lint/Rust1.99 all-target Clippy, current/old/main cache33+exact-main2+original-owner5, 네 종류 full JS/maps/CSS 양방향 probes, 실제 multi-env/compiler plugin matrix, 5 plugin 두-checkout build gate 통과.
  • Cascade: ordinary/singleCss/atomHoist, 번호/비번호, AB/BA extraction 및 명시적 stylesheet loading order, 320/1440px의 Chromium51개 통과. Cold ordinary Next와 실제 old-cache upgrade Next에서 landing221개씩 통과했습니다. Header link/footer row 및 기존 visual 기대/스크린샷은 변경하지 않았습니다.
  • Typography: 일반 native3073/0(226 class-only 포함), fresh WASM92/0, Bun5589/0 함수/라인100%, fmt/lint/1.99 Clippy 및 실제 Next landing221개 통과. Snapshot을 추가 수락하거나 기대를 약화하지 않았습니다.
  • 118 Rust 및 Textarea snapshot5 bodies는 개별 source/descriptor/whole forward-inverse 증명으로 승인했고 최종 accepted hashes를 재검증했습니다. Source-faithful 읽기/횟수/순서, rule placement, reset/DOM/effective declaration을 보존했습니다. 이름 scope 변화와 closed-literal capture 제거만 해당 수락 이유입니다.
  • 현재 head a9c804a3, CI 37498939552: benchmark, RSC CSS, landing-next-e2e 성공; publish는 Linux99.01%(12148/12270) coverage로 실패하여122 uncovered lines를 실제 CI wide-format source에 매핑했습니다. 의미 있는 회귀/coverage 후속 커밋으로100%를 완료해야 합니다. Codecov-only outage가 아니며 workflow/rerun loop를 바꾸지 않았습니다.
  • Rust LSP daemon timeout과 미설치/설치 거절된 TS/Biome 때문에 editor diagnostics-clean은 주장하지 않습니다. Native compiler/Clippy와 실제 surface gates의 결과를 구분해 기록합니다. 최종 CI100% 전에는 PR을 ready로 표시하지 않습니다.

owjs3901 and others added 23 commits October 1, 2026 01:50
…nd StyleX imports

The generated stylesheet always resolved to an empty module, so Bun.build dropped every style, and the pre-parse gate only accepted import declarations of @devup-ui/react, so files using the alias packages, StyleX, subpaths or re-exports ran the runtime stubs. DevupUI() now serves the complete sheet to the CSS loader after defer(), and the gate reads the real imports with Bun.Transpiler.scanImports.

Refs #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… built stylesheets

A stylesheet module is built on its first import, and the shared base is read from disk past the plugin, so styles extracted later were missing from webpack and Rsbuild output; Rsbuild also served a module-wide string kept only when it grew. The files the entries reach are now extracted before the build, the base on disk is kept current, and a compilation whose stylesheets changed is run once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet.

Refs #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…into feat/deterministic-numbering

# Conflicts:
#	packages/next-plugin/src/loader.ts
…ing order

Refs #694

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… forwarding alone

Refs #694

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694, #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #694

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…tion

Refs #694

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…e order

Refs #694

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Preserve whole styled argument creation-time evaluation and qualify the inherited naming/extractor coverage union, including sticky getCss proof.

Refs #694

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant