Preserve the continuous QUEEN hive and connect source specs to the shared core - #971
Preserve the continuous QUEEN hive and connect source specs to the shared core#971gHashTag wants to merge 62 commits into
Conversation
|
Independent read-only review of final head 3a5d9bd: PASS. All real paired spec placements were checked using full hex bounds at 1440x720, 390x500, 390x300 and 600x250; the short-mobile fit floor is fixed and matched by wheel/zoom-out. Exact local Node 20 build entry: index-C9jvDaqz.js, SHA256 92c690ec2c75b615736644cbf3bb8f46e6e25df7d162b091dfd6b5cc8948abe7. Canonical corpus remains 760 source files with unchanged manifest. Automated Claude review failed at execution initialization (is_error:true), not a code verdict. Documentation Consistency checks an already absent base-main src/hslm/tjepa.zig. Website checks are still running; final corrected-build visual checks are awaiting recovery of the owned BrowserOS QA session. No merge or publication yet. |
|
Website checks 34142096601 completed SUCCESS on final head 3a5d9bd, including EN/RU and mobile audits plus render-check. General Build & Test, format validation and VIBEE codegen also pass. The remaining release hold is final corrected-build visual acceptance after BrowserOS lost ownership of the single user-approved QA tab. Source is saved; no merge/publication has been performed. Resume with the same PR/head and the existing apex publisher, without rerunning corpus sync or touching unrelated backend/hosting state. |
A region label is centred on its portal and hangs above it, so half of it sat outside the layer whenever a portal drifted to an edge: at 390x844 trinity-fpga rendered at left:-31 and read as "ity-fpga". Only the vertical side was clamped. The label box is now clamped on both axes against the measured layer. At 320x568 the five portals project into the same few hundred pixels and the labels stacked into one unreadable pile. Placement now runs widest portal first, looks for a free vertical slot, and drops a label only when no slot is left; the portal stays on the canvas and its label returns as soon as zoom separates them. The narrow-screen Repository button was 44x40 while the select beside it was already 44, so it now matches the 44px touch floor. Measured at 320x568, 390x844, 768x1024 and 1440x900: no label clipped and no label overlapping another. Подписи репозиториев больше не обрезаются краем поля на телефонах. Метка центрируется на своём портале и висит над ним, поэтому у края половина её уходила за пределы слоя: на 390x844 trinity-fpga рисовалась на left:-31 и читалась как "ity-fpga" — ограничение было только по вертикали. Теперь бокс метки ограничен по обеим осям по измеренному слою. На 320x568 пять порталов проецируются в одну область, и метки складывались в нечитаемую кучу. Раскладка идёт от самого широкого портала, ищет свободный слот по вертикали и отбрасывает метку только если слота не осталось; портал остаётся на канвасе, а подпись возвращается при зуме. Кнопка Repository на узких экранах была 44x40 при соседнем select в 44 — приведена к порогу касания 44px. Проверено на 320x568, 390x844, 768x1024 и 1440x900: обрезанных и перекрывающихся подписей нет. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
npm run dev died with `The "path" argument must be of type string. Received undefined` whenever the shell's Node was 18: @vitejs/plugin-react-swc reads import.meta.dirname, which lands in Node 20.11, and is undefined before it, so join() threw and every module failed to transform. .nvmrc said `20`, which still permits 20.0 through 20.10 — versions without that property. It now pins 20.19.0, and engines records the real floor, so `nvm use` in this directory selects a Node that can actually build. Закреплена минимальная версия Node, которая нужна swc-плагину. npm run dev падал с `The "path" argument must be of type string. Received undefined`, если в шелле был Node 18: @vitejs/plugin-react-swc читает import.meta.dirname, появившийся в Node 20.11, а до него это undefined — join() бросал ошибку, и ни один модуль не трансформировался. В .nvmrc стояло `20`, что допускало 20.0–20.10, где этого свойства ещё нет. Теперь зафиксирована 20.19.0, а engines фиксирует реальный порог, так что `nvm use` в этой папке выбирает версию, на которой сборка идёт. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
At 844x390 five stacked chrome bands (worlds nav, status, viewport head, catalog toolbar, command rail) left the field 130px of 390 — a third of the screen for the thing the screen is for. Height is the scarce axis on a landscape phone and width is not, so below 500px of height the viewport head and the catalog toolbar stop claiming their own rows and float over the field. --hud-vp-head-float is the contract between them: the toolbar and the label layer offset by it instead of guessing where the head ends. The field goes from 130px to 226px, 33% to 58%. Touch targets keep their 44px floor, the page still does not scroll, and nothing changes above 500px of height: at 390x844 both bars remain static. Поле возвращает себе экран на телефонах в альбомной ориентации. На 844x390 пять полос хрома (навигация миров, статус, заголовок вьюпорта, панель каталога, командная рейка) оставляли полю 130px из 390 — треть экрана на то, ради чего экран и нужен. В альбомной ориентации дефицитна высота, а не ширина, поэтому ниже 500px по высоте заголовок вьюпорта и панель каталога перестают занимать собственные ряды и ложатся поверх поля. --hud-vp-head-float — контракт между ними: панель и слой меток смещаются на неё, а не угадывают, где кончается заголовок. Поле выросло со 130px до 226px, с 33% до 58%. Порог касания 44px сохранён, страница по-прежнему не скроллится, выше 500px по высоте ничего не меняется: на 390x844 обе полосы остаются static. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The homepage carried the whole TNF article: 21 sections, 31 393px, about 35 screens, with the .t27 goal somewhere in the middle of it. None of those sections existed anywhere else, so the page was both the landing and the paper. Every section keeps its content and moves to the page it belongs to — formats, visuals and calculators to /gft; ladder, theorems and limits to /proof; frontier and reproduce to /verification; decision to /select; landscape and findings to /cases; lineage and author to /about; faq and publications to /resources; start to /start; invest to /ip. Nothing is deleted: all seventeen were checked present on their new pages. The landing is now Hero, the Queen block, the claim and the service entry: 4 510px, five screens. The dock pointed at homepage anchors that no longer exist there, so it points at routes instead, and leads with the map and the specs — the thing the game is for. Its labels move with it in all five locales. Статья уезжает с главной на свои страницы. Главная несла всю статью TNF: 21 секция, 31 393px, около 35 экранов, и цель .t27 терялась где-то в середине. Ни одна из секций не существовала больше нигде, то есть страница была одновременно и лендингом, и статьёй. Каждая секция сохраняет содержание и переезжает туда, где ей место: formats, visuals, calculators → /gft; ladder, theorems, limits → /proof; frontier, reproduce → /verification; decision → /select; landscape, findings → /cases; lineage, author → /about; faq, publications → /resources; start → /start; invest → /ip. Ничего не удалено: все семнадцать проверены на новых страницах. Лендинг теперь — Hero, блок Queen, тезис и вход в сервис: 4 510px, пять экранов. Док вёл на якоря главной, которых там больше нет, поэтому ведёт на маршруты и начинается с карты и спек — того, ради чего игра. Подписи дока переехали вместе с ним во всех пяти локалях. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The homepage block was a launch card: three decorative CSS hexagons, a headline about "one map", and two links. The hive is a one-screen module, so the landing now mounts that module — the same QueenCatalogHive the #/queen route mounts, lazily, sharing the atlas the block already fetched. Only one instance ever exists, because the homepage and #/queen are different routes; the block unmounts on the way in. The copy says what the game is for: the goal is a .t27 spec, gold cells are sources, red ones the issues that pay for them. The counts lead with .t27 rather than repositories, and a third action opens the Spec Explorer at /specs. The block gets a screen of height for the scene instead of the 290px the decorative cells sat in, and the orphaned cell rules are gone. This changes the standing rule against a second Babylon scene on the homepage, so the instruction file is updated with it: reuse that component and the shared atlas, and still never fork atlas logic or write a second scene. The landing pays for Babylon in a lazy chunk after first paint. На лендинге теперь сам улей, а не его изображение. Блок на главной был карточкой запуска: три декоративных CSS-шестиугольника, заголовок про «одну карту» и две ссылки. Улей — одноэкранный модуль, поэтому лендинг монтирует именно этот модуль: тот же QueenCatalogHive, что и маршрут #/queen, лениво и на том же атласе, который блок уже загружал. Экземпляр всегда один, потому что главная и #/queen — разные маршруты. Текст говорит, ради чего игра: цель — спека .t27, золотые соты это источники, красные — issues, которые за них платят. Счётчики начинаются с .t27, а третье действие открывает Обозреватель спек на /specs. Сцена получает экран высоты вместо 290px, где сидели декоративные соты, а осиротевшие правила удалены. Это меняет действующее правило про вторую сцену Babylon на главной, поэтому инструкция обновлена вместе с кодом: переиспользовать компонент и общий атлас и по-прежнему не форкать логику атласа и не писать вторую сцену. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The landing said the goal is a .t27 spec and then never showed one. It now carries the Explorer as its own block under the hive, opened on specs/demos/hello_world.t27 — the smallest spec that still shows every part of the language, which is why the manifest describes it as the place to start. It is the real Explorer, not a second source viewer: an iframe at ?embed=1, the same way the Queen inspector embeds it, with the path built by specExplorerHash so it is validated and fails closed like every other link into the catalog. embed=1 drops the page header, so the block does not grow a second title bar. Sized against what a two-pane application needs rather than a decorative ratio, and measured on every device class: 1126x682 at 1440x900, 696x700 at 768x1024, 318x560 at 390x844, 248x443 at 320x568, and 772x320 on a 844x390 landscape phone, where height is scarce and the 460px floor would have pushed the frame off screen. No horizontal scroll at any of them, inside the frame or outside it, and both actions keep the 44px touch floor. Обозреватель спек теперь на самом лендинге. Лендинг говорил, что цель — спека .t27, и ни одной не показывал. Теперь Обозреватель стоит отдельным блоком под ульем и открыт на specs/demos/hello_world.t27 — самой маленькой спеке, где видна каждая часть языка; манифест прямо называет её местом, с которого начинают. Это настоящий Обозреватель, а не второй просмотрщик исходников: iframe с ?embed=1, ровно так же, как его встраивает инспектор карты, а путь строит specExplorerHash — значит он валидируется и падает закрыто, как любая другая ссылка в каталог. embed=1 убирает заголовок страницы, поэтому у блока не появляется вторая титульная полоса. Размер задан тем, что нужно двухпанельному приложению, и проверен на всех классах устройств: 1126x682 на 1440x900, 696x700 на 768x1024, 318x560 на 390x844, 248x443 на 320x568 и 772x320 на альбомном 844x390, где высота дефицитна и порог в 460px вытолкнул бы рамку за экран. Горизонтального скролла нет нигде — ни внутри рамки, ни снаружи, — а обе кнопки держат порог касания 44px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… slabs Back to back the two blocks were the same beat twice: eyebrow, a 72px headline, a paragraph, buttons, a large dark panel — 1368px and 1412px of nearly identical shape, and two headlines the same size saying the goal is a .t27 spec and then that this is what a .t27 spec is. They are one idea in two beats, so they now read as one. The hive keeps the headline; the spec answers at 39px instead of 72 and lays its lede beside the prose rather than stacking a second hero. The rule between them is gone and the pair runs continuous. The hive also stops offering "Read the .t27 corpus", because the corpus is the block directly beneath it, and the spec block already carries the link. Landscape phones got the same floor problem the spec frame had: a 420px minimum on a 390px screen meant the map alone outgrew the viewport. Both panels are 320px there now and both fit. The spec block loses 241px, the landing 242px, and the pair is 41% of the page instead of 43% of a taller one. Улей и спека складываются в пару, а не в две плиты. Подряд два блока звучали одинаково: eyebrow, заголовок 72px, абзац, кнопки, большая тёмная панель — 1368px и 1412px почти одинаковой формы и два заголовка одного кегля, говорящие, что цель — спека .t27, а следом что вот что такое спека .t27. Это одна мысль в два такта, и теперь она читается как одна. Заголовок остаётся у улья; спека отвечает 39px вместо 72 и кладёт зачин рядом с текстом, а не собирает второй герой-блок. Линейка между ними убрана, пара идёт встык. Улей перестал предлагать «Читать корпус .t27», потому что корпус — это блок прямо под ним, и ссылка уже есть в нём самом. В альбомной ориентации у карты была та же беда с порогом, что у рамки спеки: минимум 420px на экране 390px, и блок сам по себе перерастал экран. Теперь обе панели там по 320px и обе помещаются. Блок спеки стал легче на 241px, лендинг на 242px, а пара занимает 41% страницы вместо 43% более длинной. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The map looked broken outside /queen: a white system select, default grey buttons, a bare text input, a blue underlined starfield credit, and the law line collapsed into one run of text — "TRI-27 · S3AI DNA651 issues760 .t275 repositories". Nothing was wrong with the scene. The hive's chrome — queen27-hive-law, queen-hive-inspect-tools, queen-hive-displays, queen-starfield-source — is styled in Queen.css, and Queen.css arrived only because the /queen route imported it. On the landing the component mounted without it and fell back to raw browser controls. QueenCatalogHive.css imports it now, so the component carries its styles wherever it is mounted. Of the 706 selectors in that file, 19 are not scoped to a queen27 or queen class, and every one of those needs either a queen27 descendant or body.queen-shell, which the landing does not set — so none of it reaches a page that does not render the hive. Улей приносит свои стили на лендинг. Карта выглядела сломанной вне /queen: белый системный select, серые кнопки по умолчанию, голое поле ввода, синяя подчёркнутая ссылка на каталог звёзд и строка закона, слипшаяся в один поток — «TRI-27 · S3AI DNA651 issues760 .t275 repositories». Со сценой всё было в порядке. Хром улья — queen27-hive-law, queen-hive-inspect-tools, queen-hive-displays, queen-starfield-source — описан в Queen.css, а Queen.css попадал на страницу только потому, что его импортировал маршрут /queen. На лендинге компонент монтировался без него и откатывался к системным контролам. Теперь его импортирует QueenCatalogHive.css, и компонент несёт свои стили всюду, где смонтирован. Из 706 селекторов файла 19 не привязаны к классам queen27 или queen, и каждому из них нужен либо потомок queen27, либо body.queen-shell, которого лендинг не ставит, — значит ничего не протекает на страницы без улья. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Embedded, the Explorer showed a couple of lines of code under a stack of chrome. Three things took the room, and none of them was the code. The spec brief ran 284px of an 826px column, more than the code pane's 332. Embedded it is now a closed disclosure at 36px and the code gets 580. On its own page it stays open and its summary stays hidden, so /specs is unchanged: brief 220px, code 726px, as before. The site's 80px body padding-bottom exists for the mobile CTA and means nothing inside a frame, but it made the document 778px against a 698px frame, so the whole app scrolled and left a strip of background under the code — the empty field. Embedded, that padding and the document scrollbar are gone: body 698, frame 698, nothing scrolls. The Queen shell drops the same padding for the same reason. On the Queen SPECS view the right column was showing the intel feed, the overview and the sectors — all of which describe the board, not the spec on screen — while the directive sat in a strip above the frame and cost it another 200px. The directive takes the column instead and the frame takes the height: the code pane goes to 634px. Narrow screens have no such column, so there the directive stays above the frame. Код получает экран обратно в обоих встраиваниях. Во встроенном виде Обозреватель показывал пару строк кода под стопкой хрома. Место занимали три вещи, и ни одна из них не была кодом. Панель описания занимала 284px из колонки в 826px — больше, чем сам код с его 332. Во встроенном режиме это свёрнутый disclosure в 36px, и код получает 580. На своей странице она остаётся раскрытой, а её summary скрыт, так что /specs не изменился: описание 220px, код 726px, как и было. 80px нижнего паддинга body существуют ради мобильного CTA и внутри рамки не значат ничего, но делали документ 778px против рамки в 698px — приложение скроллилось и открывало полосу фона под кодом, то самое пустое поле. Во встроенном режиме этого паддинга и полосы прокрутки больше нет: body 698, рамка 698, ничего не скроллится. Шелл Queen убирает тот же паддинг по той же причине. На виде SPECS в Queen правая колонка показывала intel-фид, обзор и секторы — всё это описывает доску, а не спеку на экране, — а директива лежала полосой над рамкой и отнимала ещё 200px. Теперь директива занимает колонку, а рамка забирает высоту: панель кода вырастает до 634px. На узких экранах такой колонки нет, там директива остаётся над рамкой. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e is gone The source layer is a textarea, and a textarea takes the height nothing gives it: 368px inside a 566px pane, with the file scrolling inside its own 368 and 168px of empty box sitting underneath. That was the field under the code. The swap now fills the pane and the stretch carries through to the textarea itself — stopping one level short left the editor exactly where it was. The container's inline min-height:100% had to be overridden inside the pane: on top of a swap already filling the box it made the editor a header taller than its pane and pushed 68px past it on /specs. Measured after: landing 535px of editor in a 566px pane, Queen SPECS 589 in 620, /specs 681 in 712 — one pixel of slack in each, against 168. Long files still scroll, in the editor rather than in a box that stops early. Редактор заполняет свою панель, и пустая коробка под кодом исчезла. Слой исходника — это textarea, а textarea берёт ту высоту, которую ей никто не даёт: 368px внутри панели 566px, файл скроллился внутри собственных 368, а под ним стояла пустая коробка в 168px. Это и было поле под кодом. Теперь свап заполняет панель, и растяжение доходит до самой textarea — остановка на уровень выше оставляла редактор ровно там, где он был. Инлайновый min-height:100% контейнера пришлось перекрыть внутри панели: поверх свапа, уже заполнившего коробку, он делал редактор на высоту шапки больше панели и выталкивал его на 68px за неё на /specs. Замеры после: лендинг — 535px редактора в панели 566, Queen SPECS — 589 в 620, /specs — 681 в 712; по одному пикселю зазора вместо 168. Длинные файлы по-прежнему скроллятся — в редакторе, а не в коробке, которая кончается раньше. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things were taking it. The embed locked the body's overflow as well as dropping its padding. Only the padding was ever the problem — 80px meant for the mobile CTA, meaningless inside a frame. The lock was harmless on a desktop and wrong on a phone, where the pane is deliberately overflow:visible so the page scrolls rather than trapping the gesture in a nested scroller. With the padding gone the frame does not overflow anyway, so the lock is gone too. Above the frame on a phone the standing directive ran 211px of a 581px view — 36% of the screen for a paragraph, against 197px of code. It collapses there now, behind a 44px summary that keeps the label. In the wide column it has room and stays open exactly as before. Measured at 390x844 on #/queen SPECS: the strip 211px to 45px, the frame 370px to 536px, the editor 197px to 313px. The page still does not scroll, so the one-screen contract holds. Desktop is untouched: the directive sits in the aside as a plain block, nothing above the frame. Код получает обратно экран телефона. Забирали его двое. Встраивание блокировало overflow тела вместе со снятием паддинга. Проблемой был только паддинг — 80px под мобильный CTA, бессмысленные внутри рамки. Блокировка была безобидна на десктопе и неверна на телефоне, где панель намеренно overflow:visible, чтобы скроллила страница, а не вложенный контейнер перехватывал жест. Без паддинга рамка и так не переполняется, поэтому блокировка убрана. Над рамкой на телефоне постоянная директива занимала 211px из 581px — 36% экрана под абзац против 197px кода. Теперь она там сворачивается за summary высотой 44px, сохраняющий подпись. В широкой колонке места хватает, и она остаётся раскрытой как прежде. Замерено на 390x844 в #/queen SPECS: полоса с 211px до 45px, рамка с 370px до 536px, редактор со 197px до 313px. Страница по-прежнему не скроллится — контракт одного экрана держится. Десктоп не тронут: директива в правой колонке обычным блоком, над рамкой ничего. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed is her log The right column was three panels deep and none of them could be talked to. It is now her column, on every view and in the phone drawer: the shell's bars stop at the third column so the sidebar runs the full height of the screen, the overview stays above her as a 210px band that is part of the sidebar rather than a disclosure over it, and she takes the rest — 601px of column with a 440px log, against 297px before. The intel feed is gone as a separate list because it said the same things twice. Its events are entries in her log now, in time order with the conversation, each carrying its GitHub issue link and an "ask about this" that makes it the subject of the next question. The window is the last 40 of the 120 rows; the header says how many there are. She is honest about being unanswered. checkHealth is polled and the state reads LIVE or OFFLINE from it; a failed send says so and never substitutes a sample, which is what the file's own A36 note is about. The endpoint is configurable now (VITE_QUEEN_CHAT_URL) because a hardcoded localhost is why every call from t27.ai failed — it points at a proxy that holds the model key, never at a provider, since VITE_ values are compiled into the public bundle. One round control, in the header, where the countdown already was: the command rail carried a second button with the same clock, and the phone rail a third. The details they opened moved to the header cell with the dismiss ref, so nothing is lost. .queen-chat-log is declared in the viewport contract, as a scroller inside the shell has to be. Королева отвечает из сайдбара во всю высоту, а лента стала её логом. Правая колонка была глубиной в три панели, и ни с одной нельзя было поговорить. Теперь это её колонка — на каждом виде и в телефонной шторке: полосы шелла останавливаются перед третьей колонкой, сайдбар идёт во всю высоту экрана, овервью остаётся над ней полосой в 210px как часть сайдбара, а не раскрывашкой поверх него, и ей достаётся остальное — 601px колонки с логом в 440px против 297px прежде. Отдельной ленты больше нет: она говорила то же самое дважды. Её события — записи в логе королевы, по времени вперемешку с разговором, у каждой ссылка на issue и «спросить об этом», делающее событие предметом вопроса. В окне последние 40 из 120 строк; сколько их всего, говорит шапка. Она честна, когда ей не отвечают: health опрашивается, состояние читается как LIVE или OFFLINE, неудачная отправка так и говорит и никогда не подставляет образец — ровно про это заметка A36 в самом файле. Точка подключения стала настраиваемой (VITE_QUEEN_CHAT_URL): захардкоженный localhost и был причиной, по которой все вызовы с t27.ai падали. Она указывает на прокси с ключом модели, а не на провайдера — значения VITE_ попадают в публичный бандл. Один контрол раунда, в хедере, где отсчёт и так был: командная рейка несла вторую кнопку с теми же часами, телефонная — третью. Детали, которые они открывали, переехали в плитку хедера вместе с ref для закрытия, так что ничего не потеряно. .queen-chat-log заявлен в viewport-контракте, как и положено скроллеру внутри шелла. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel was honest and silent: no backend, so OFFLINE. It has one now, and the key never touches the browser. queen-proxy is a small service in the 999 project, beside t27-github-collab where the rest of the infrastructure already lives. It speaks the website's contract (GET /health, POST /chat) on one side and OpenAI-compatible chat completions on the other, which is what OpenRouter, Groq, Together, DeepSeek and Google's compat endpoint all speak — so choosing a provider is three variables, never a patch. The model is ours rather than rented: a queen-ollama service on the same private network, with a volume, running qwen3:1.7b. On our own network no key is required and the proxy stops demanding one, which is what let it be configured at all in the setup that has no secret to hold. It repairs itself. Ollama starts empty, and a volume survives a restart but not a host that loses its disk, so the proxy asks for the model on boot and again whenever a health check finds it missing. A wiped host heals without anyone logging in — no shell step in the image, no manual pull. Health is a fact, not a flag: on our network it checks the model is actually present, because a proxy that reports LIVE and then 404s on send is the sample problem wearing a different coat. Measured end to end: 5.4s for a first answer on Railway's CPU. The website reaches it through queenModel.ts, which also speaks Ollama directly for a local Queen with no key and no hosting at all. Живая модель отвечает, и притом из нашей собственной сети. Панель была честной и молчала: бэкенда не было, отсюда OFFLINE. Теперь он есть, и ключ не попадает в браузер. queen-proxy — небольшой сервис в проекте 999, рядом с t27-github-collab, где уже живёт остальная инфраструктура. С одной стороны он говорит на контракте сайта (GET /health, POST /chat), с другой — на OpenAI-совместимых chat completions, на которых работают OpenRouter, Groq, Together, DeepSeek и совместимый эндпоинт Google. Смена провайдера — три переменные, а не патч. Модель своя, а не арендованная: сервис queen-ollama в той же приватной сети, с томом, крутит qwen3:1.7b. Во внутренней сети ключ не нужен, и прокси перестал его требовать — иначе он навсегда остался бы ненастроенным именно там, где секрета нет вовсе. Он чинит себя сам. Ollama стартует пустой, а том переживает рестарт, но не потерю диска у провайдера, поэтому прокси запрашивает модель при старте и всякий раз, когда health-проверка не находит её. Стёртый хост восстановится без чьего-либо входа — без шелл-шага в образе и без ручного pull. Health — это факт, а не флаг: во внутренней сети он проверяет, что модель действительно на месте, потому что прокси, который рапортует LIVE и падает 404 на отправке, — та же подмена образцом, только в другом костюме. Замерено насквозь: 5.4 с до первого ответа на CPU Railway. Сайт ходит туда через queenModel.ts, который умеет и напрямую в Ollama — для локальной королевы без ключа и без хостинга вообще. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three bands sat above and below the board and none of them needed the height they took. The command rail was 124px for a sector name and six icons; it is 84px for the same at the same size. The worlds bar was 59px of finger-height controls on a mouse, where a finger is not what is pointing: under pointer:fine its controls are 34px and the bar is tighter, and under a coarse pointer they keep the 44px floor because there it is the rule rather than a default. The orphan went with them. Removing the duplicate round button left its section behind — orbit rings and a logo, no data — and with the bottom bar down to two columns it wrapped into an implicit third row, which is what put a cut-off tile and a band of empty black under the rail. Measured after: the bottom bar holds exactly two children in two columns, both 124px tall at the same top, the sidebar ends on the same line, and the page still does not scroll. Полю возвращены пиксели, которые держал хром. Три полосы стояли над доской и под ней, и ни одной не нужна была занятая высота. Командная рейка была 124px под имя сектора и шесть иконок — стала 84px при том же размере. Полоса миров была 59px контролов «под палец» на мыши, где указывает не палец: при pointer:fine контролы 34px и полоса плотнее, а при грубом указателе сохраняют порог 44px, потому что там это правило, а не умолчание. Вместе с ними ушёл и сирота. После удаления дублирующей кнопки раунда её секция осталась — орбитальные кольца и логотип, данных нет, — а при нижней полосе из двух колонок она уехала в неявный третий ряд. Это и давало обрезанную плитку и полосу пустоты под рейкой. Замерено после: в нижней полосе ровно два ребёнка на две колонки, оба высотой 124px и с одного верха, сайдбар заканчивается на той же линии, страница по-прежнему не скроллится. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…field The bar above the shell ran 59px for controls a mouse does not need a finger's height to hit. On a fine pointer above 900px it is 41px now; narrow layouts and coarse pointers are untouched, because that is where the 44px floor is the rule. With the command rail's 124px already down to 84, the field is 630px. RESTORE.md is the part that was missing: the Queen was infrastructure that existed only in a dashboard. It writes down what she is made of — queen-ollama on a volume, queen-proxy beside it in project 999 — and the exact commands to rebuild both from this repository if the provider loses them. It also records the two things that are easy to get wrong: the model heals itself through the proxy rather than a shell step in the image, and the same proxy runs on any Node 20 host, so a Railway outage is a URL change rather than a rebuild. Шаблон восстановления, и полоса миров перестаёт занимать поле. Полоса над шеллом занимала 59px под контролы, до которых мышью не нужно целиться пальцем. На точном указателе шире 900px она теперь 41px; узкие раскладки и грубый указатель не тронуты — там порог 44px и есть правило. Вместе с уже сжатой со 124 до 84px командной полосой поле выросло до 630px. RESTORE.md — то, чего не хватало: королева была инфраструктурой, существующей только в дашборде. Он записывает, из чего она собрана — queen-ollama на томе, queen-proxy рядом в проекте 999 — и точные команды, чтобы поднять обоих из этого репозитория, если провайдер их потеряет. И фиксирует две вещи, которые легко упустить: модель восстанавливается сама через прокси, а не шелл-шагом в образе, и тот же прокси работает на любом хосте с Node 20, так что падение Railway — это смена URL, а не пересборка. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The star catalog was the viewport's backdrop: 830x527 behind the hive, with opaque panels either side of it. So the modules read as boxes on a page that happened to contain a scene, rather than as places on one map. Inside the shell the sky is the screen's now — fixed, full viewport, behind everything, never taking a pointer. The same HYG catalog runs under the command rail, the sidebar and the bars as under the comb, so moving between COMB, SPECS, KANBAN, MISSION MAP, FACTORY and the tech tree is movement across one sky rather than a swap of panels. The chrome floats instead of covering: --hud-panel goes from 0.86 to 0.55 and --hud-ground from #020806 to transparent. Dark enough that mono text on it stays legible, thin enough that the stars are continuous behind the whole shell. Measured at 1440x900: the canvas is 1440x900, fixed, z-index 0. Одно небо под всеми модулями, HUD плывёт над ним. Каталог звёзд был задником вьюпорта: 830x527 позади улья, а по бокам — непрозрачные панели. Из-за этого модули читались как коробки на странице, внутри которой оказалась сцена, а не как места на одной карте. Внутри шелла небо теперь принадлежит экрану: fixed, во весь вьюпорт, позади всего и не перехватывает указатель. Тот же каталог HYG идёт под командной рейкой, сайдбаром и полосами, что и под гребёнкой, поэтому переход между COMB, SPECS, KANBAN, MISSION MAP, FACTORY и деревом технологий — это перемещение по одному небу, а не подмена панелей. Хром плывёт, а не закрывает: --hud-panel с 0.86 до 0.55, --hud-ground с #020806 в прозрачный. Достаточно тёмный, чтобы моноширинный текст оставался читаемым, и достаточно тонкий, чтобы звёзды шли непрерывно под всем шеллом. Замер на 1440x900: холст 1440x900, fixed, z-index 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Making the starfield the screen's background stretched it. The component sized its buffer from the canvas's parent, and inside the shell the canvas is fixed and fills the viewport while the parent is still the comb's box: a 1660x1054 buffer scaled up into a 1440x900 element at device pixel ratio 2, where 2880x1800 was wanted. The map covered the screen and was blurred across it. It measures the canvas now, which knows where CSS actually put it, and the resize observer watches the same element. Correct in both places the field is used — embedded in the comb, or fixed behind the whole shell. Measured at 1440x900: canvas 1440x900 from (0,0), buffer 2880x1800, exactly device pixel ratio times the box. Небо рисуется в том размере, в котором показано, а не в размере старой коробки. Когда звёздное поле стало фоном экрана, оно растянулось. Компонент брал размер буфера у родителя холста, а внутри шелла холст фиксирован и занимает весь вьюпорт, тогда как родитель — по-прежнему коробка гребёнки: буфер 1660x1054 масштабировался в элемент 1440x900 при DPR 2, где нужно было 2880x1800. Карта закрывала экран и была по нему размыта. Теперь размер берётся у самого холста, который знает, куда его поставил CSS, и ResizeObserver следит за тем же элементом. Верно в обоих местах, где поле используется, — встроенным в гребёнку и фиксированным позади всего шелла. Замер на 1440x900: холст 1440x900 от (0,0), буфер 2880x1800 — ровно DPR, умноженный на коробку. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository's Dockerfile builds background-agent-api with the pinned Zig 0.15.2 — the bees' runtime — but nothing at the root said so, and Railway's auto-detection read a 1.6GB polyglot repo as a Node app and failed on a missing entry point. The config says what the build is. It does not override a builder already pinned in a service's own settings, which is a switch in the dashboard. Сборка в корне объявлена тем, чем она и является — Dockerfile. Dockerfile репозитория собирает background-agent-api на закреплённом Zig 0.15.2 — это рантайм пчёл, — но в корне об этом ничего не говорило, и автодетект Railway принял полиглотный репозиторий на 1.6 ГБ за Node-приложение и упал на отсутствующей точке входа. Конфиг называет сборку своим именем. Он не перебивает билдер, уже зафиксированный в настройках самого сервиса, — это переключатель в панели. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panels were already translucent and it changed nothing, because what showed through them was not the star map: .queen-universe painted solid black across the whole page, sitting between the fixed sky and everything above it. Transparency over an opaque sheet is just a darker opaque sheet. The ground belongs to the page now — body.queen-shell carries the black — and the wrapper is transparent inside the shell, so there is nothing between the canvas and the chrome. The sky also drops to z-index -1: at 0 it painted in DOM order alongside the panels and only ever showed inside the viewport box. Sampled over the sidebar, the command rail and the bottom bar: the only opaque layer left in each stack is the body, which is the ground beneath the sky rather than a sheet over it. Чёрный лист убран из промежутка между небом и хромом. Панели уже были полупрозрачными, и это ничего не давало: сквозь них просвечивала не звёздная карта. .queen-universe заливал сплошным чёрным всю страницу и стоял между фиксированным небом и всем, что над ним. Прозрачность поверх глухого листа — это просто более тёмный глухой лист. Подложка теперь принадлежит странице: чёрный несёт body.queen-shell, а обёртка внутри шелла прозрачна, так что между холстом и хромом не осталось ничего. Небо заодно уходит на z-index -1: на 0 оно рисовалось в порядке DOM наравне с панелями и показывалось только внутри коробки вьюпорта. Проверено пробами над сайдбаром, командной рейкой и нижней полосой: единственный непрозрачный слой в каждом стеке — body, то есть подложка под небом, а не лист поверх него. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… map Layering them over the scene was not what was asked and did not read as it: the panels were still siblings of the map in a grid that reserved a row or a column for each, so the scene was a cell among cells however far its canvas stretched behind them. They are children of the map container now. The shell is a single cell, the viewport fills it, and the status bar, the command rail, the Queen's column and the bottom bar are absolutely positioned inside that container — overlays on the scene rather than tracks around it. The scene's own content takes padding in place of the space the chrome used to occupy, so the law line and the search row stop running under the floating bar. Fullscreen still empties the container completely: every one of those four leaves, along with the worlds bar above the shell. Хедер, рейки и футер переезжают внутрь карты. Наложить их поверх сцены — не то, о чём просили, и читалось это не так: панели оставались соседями карты в сетке, резервировавшей под каждую ряд или колонку, поэтому сцена была ячейкой среди ячеек, как бы далеко её холст ни уходил за них. Теперь они дети контейнера карты. Шелл — одна ячейка, вьюпорт её заполняет, а полоса статуса, командная рейка, колонка королевы и нижняя полоса позиционированы абсолютно внутри этого контейнера: оверлеи на сцене, а не дорожки вокруг неё. Содержимое самой сцены получает отступы вместо места, которое занимал хром, поэтому строка закона и строка поиска перестают уходить под плавающую полосу. Полный экран по-прежнему опустошает контейнер целиком: уходят все четыре, а вместе с ними полоса миров над шеллом. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… they say Three things were left. The worlds bar still lived above the shell — the one strip outside the container everything else had moved into; it floats over the scene now and leaves with the rest in bare mode. The status bar kept a ten-column grid template it no longer had the width for, so its tiles squeezed until BEES, DONE, VERDICTS, RESEARCH and FOUNDRY overlapped; floating, it sizes tiles to their content and scrolls sideways instead. The third was the real one. The viewport is a grid, and an absolutely positioned grid child is placed against its own cell rather than the container. The command rail still carried grid-row 2, so top:126px meant 126px into that row: it began at 512, stood 283px tall and lost a module off the bottom. The overlays span every track now, and the offsets mean what they say. Measured: no pair of bars overlaps, the rail runs 136 to 796 and holds all six modules, all four panels are inside the map container, and the page does not scroll. Every control probed is reachable — the full-screen canvas takes no clicks from them. Полоса миров переезжает к карте, а оверлеи встают туда, куда сказано. Оставалось три вещи. Полоса миров всё ещё жила над шеллом — единственная полоска вне контейнера, куда переехало остальное; теперь она плывёт над сценой и уходит вместе со всеми в голом режиме. Полоса статуса держала шаблон из десяти колонок, на который ей уже не хватало ширины, и плитки сжимались, пока BEES, DONE, VERDICTS, RESEARCH и FOUNDRY не начали наезжать друг на друга; в плавающем виде она подгоняет плитки под содержимое и прокручивается вбок. Третья была настоящей. Вьюпорт — сетка, а абсолютно позиционированный ребёнок сетки ставится относительно своей ячейки, а не контейнера. На командной рейке оставался grid-row 2, поэтому top:126px означало 126px внутри этого ряда: она начиналась на 512, была высотой 283px и теряла модуль снизу. Теперь оверлеи охватывают все дорожки, и отступы значат то, что написано. Замерено: ни одна пара полос не пересекается, рейка идёт с 136 до 796 и держит все шесть модулей, все четыре панели внутри контейнера карты, страница не скроллится. Каждый проверенный контрол достижим — полноэкранный холст не перехватывает у них клики. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four faults stood between src/background_agent and a running container, each hiding the next. build.zig never declared it. `zig build background-agent-api` — the command the deployment Dockerfile runs — answered "no step named 'background-agent-api'", so the image could never be produced. The directory has existed unwired: the step now points at src/background_agent/main.zig under the name the Dockerfile already used. The Dockerfile never carried the binary out of the builder. Nothing referenced that stage, so Docker skipped it entirely, and the runtime stage ran `find /app/.zig-cache` against a filesystem that has neither. The artifact is resolved in the stage that holds the cache and copied across with COPY --from. The database client could not resolve a name. resolveIp parses a literal address and performs no lookup, so every managed Postgres crashed the agent on boot with InvalidIPAddressFormat: Railway addresses its database as postgres.railway.internal. tcpConnectToHost resolves names and still accepts a literal IP. The startup message was malformed twice over. @memcpy was handed a five-byte destination and the four-byte "user", which panics on non-equal lengths, and the size counted a terminator after each key that the literals already carried — two bytes the server would read as a broken parameter list. The container now boots, reads its config and listens. Two things remain, and neither is a setting: the Postgres client speaks only trust authentication and misreads the wire header — it takes the length before the type, where the protocol sends type first — so LOCAL_MODE=true is what keeps it up; and the HTTP server answers Railway's proxy with 502 while the process stays alive. Рантайм пчёл впервые собирается и стартует. Между src/background_agent и работающим контейнером стояли четыре поломки, каждая прикрывала следующую. build.zig его не объявлял. На `zig build background-agent-api` — команду из деплойного Dockerfile — ответом было «no step named», и образ не мог быть собран в принципе. Каталог всё это время не был подключён к сборке: шаг теперь указывает на src/background_agent/main.zig под тем именем, которое Dockerfile уже использовал. Dockerfile не выносил бинарник из стадии сборки. На эту стадию никто не ссылался, поэтому Docker её пропускал, а рантайм искал /app/.zig-cache там, где нет ни того, ни другого. Артефакт разрешается в стадии с кэшем и переносится через COPY --from. Клиент базы не умел доменные имена. resolveIp разбирает литеральный адрес и не делает резолва, поэтому любой управляемый Postgres ронял агента при старте ошибкой InvalidIPAddressFormat: Railway адресует базу как postgres.railway.internal. tcpConnectToHost резолвит имена и по-прежнему принимает литеральный IP. Стартовое сообщение было испорчено дважды. @memcpy получал пятибайтовый приёмник и четырёхбайтовое "user", что в безопасной сборке паникует, а размер учитывал терминатор после каждого ключа, который литералы уже несли, — два лишних байта, которые сервер прочёл бы как сломанный список параметров. Контейнер теперь стартует, читает конфиг и слушает. Остаются две вещи, и ни одна не является настройкой: клиент Postgres знает только доверительную аутентификацию и неверно читает заголовок — берёт длину раньше типа, тогда как протокол шлёт тип первым, — поэтому его держит LOCAL_MODE=true; и HTTP-сервер отвечает прокси Railway 502, хотя процесс жив. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…x changes The active sector answered the same question as the worlds selector — which repository is in hand — so it is a tile in the status bar rather than a panel under the map. The quick commands ride beside the map's own head. With both gone the footer holds nothing on a desktop, so it goes, and the Queen's column runs the whole height: 744px with a 582px log, against 601 and 440. Floating beside the head there is room for the row of commands or for a title above it, not both: at 39px with its header the five buttons had nowhere to be and none of them rendered. The icons carry the meaning and the panel keeps its label for a screen reader. The camera reframes when the container changes shape. A resize used to keep zoom and target, which is right for a window nudged by a few pixels and wrong when the box becomes another box: framing chosen for 830x527 left the hive off screen at 1440x900, and the only way back was the Whole map button. A quarter in either dimension is the line — past it the view is refitted, under it the operator's zoom is left alone. On a phone the footer stays: it carries the six module tabs, which is the only way to change view there. Футер уходит, а карта перестраивает кадр при смене формы контейнера. Активный сектор отвечал на тот же вопрос, что и селектор миров, — какой репозиторий в руках, — поэтому он плитка в полосе статуса, а не панель под картой. Быстрые команды едут рядом с заголовком самой карты. Без них обоих футер на десктопе не держит ничего, поэтому его нет, а колонка королевы идёт во всю высоту: 744px с логом 582px против 601 и 440. Рядом с заголовком есть место либо для ряда команд, либо для названия над ним, но не для обоих: на 39px вместе с шапкой пяти кнопкам было негде разместиться, и не отрисовалась ни одна. Смысл несут иконки, а подпись панель сохраняет для скринридера. Камера перестраивает кадр, когда контейнер меняет форму. Раньше resize сохранял зум и цель — это верно для окна, сдвинутого на несколько пикселей, и неверно, когда коробка становится другой коробкой: кадр, выбранный для 830x527, оставлял улей за экраном на 1440x900, и вернуться можно было только кнопкой «Вся карта». Четверть по любому измерению — граница: за ней вид перестраивается, до неё зум оператора не трогается. На телефоне футер остаётся: он несёт шесть вкладок модулей, а другого способа сменить вид там нет. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/health returns 200 {"status":"ok"} and the guarded routes return 401 instead
of the platform's 502. Three faults, none of them a setting.
The response was not HTTP. It was built from a multiline literal, which does
not process escapes: every "\r" in it was a backslash and an r, the lines ended
with a bare \n, and the blank line that closes the header block was a single
stray character. Nothing on the other end could parse that, so the proxy
answered 502 while the process sat there healthy — the shape of bug that reads
as "the service is down" and never is.
The request line was misread. It is three fields, "METHOD TARGET PROTOCOL", and
the parser skipped the target, then took the URI from a fourth field that does
not exist. path was therefore always empty and no route ever matched, health
probe included.
The listener was invisible. Railway reaches a container over IPv6 and the
server bound 0.0.0.0, so the port was open to nobody. "::" accepts both
families.
Пчёлы отвечают: валидный HTTP, верная цель запроса и IPv6.
/health отдаёт 200 {"status":"ok"}, закрытые маршруты — 401 вместо
платформенного 502. Три поломки, и ни одна не была настройкой.
Ответ не был HTTP. Он собирался из многострочного литерала, а тот не
обрабатывает экранирование: каждый "\r" в нём — обратный слэш и «r», строки
заканчивались голым \n, а пустую строку, закрывающую блок заголовков, заменял
одиночный случайный символ. Разобрать это на той стороне невозможно, поэтому
прокси отвечал 502, пока процесс был жив и здоров, — та самая поломка, которая
читается как «сервис лежит», хотя он никогда не лежал.
Строка запроса читалась неверно. В ней три поля, «METHOD TARGET PROTOCOL», а
разбор пропускал цель и брал URI из четвёртого поля, которого не существует.
Поэтому path всегда был пустым и ни один маршрут не совпадал, включая проверку
здоровья.
Слушатель был невидим. Railway ходит в контейнер по IPv6, а сервер слушал
0.0.0.0 — порт был открыт в никуда. "::" принимает оба семейства.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lace Two faults, both mine. The bar was sized to its content, so it re-laid itself every second: the countdown gains a digit, a count goes 99 to 100, the status swaps WAITING_FOR_REVIEW for something shorter, and every tile after it shifts. Tiles keep a floor now and digits are tabular, so a number changing changes nothing else. Measured over a ticking clock: no tile moves. The other was pixel offsets tuned at 1440x900 — 240 for the rail, 336 for the column, 356 for what sat beside it — repeated at each site and adapting to nothing. They come from variables now, and a breakpoint moves every panel together: 200/300 below 1280, 180 and no column below 1100 where it becomes a drawer, and none of them on a phone. What that surfaced, in order: the head and the law line stopped 280px short of the edge because a hidden column still reserved its width; the status bar overlapped the head at 768 because the offset assumed bars shorter than they are; and at 390 the head wraps to two lines, where a floating element cannot tell the body how much room it took, so it sits in the flow — its position was set twice in the file and the file's order was deciding, which is stated at the end now rather than left to chance. The sector tile was drawn for a footer slot: in the bar its mark and its two columns of facts overlapped the name, so it shows the name and the state. Verified at 1440, 1100, 768 and 390: no pair of bars overlaps, nothing scrolls in either direction, and on the phone the order reads top, head, law. Полоса перестаёт ездить, а оверлеи меряются из одного места. Две ошибки, обе мои. Полоса подгонялась под содержимое и потому перестраивалась каждую секунду: счётчик получает цифру, число идёт с 99 на 100, статус меняет WAITING_FOR_REVIEW на что-то короче — и всё, что правее, едет. Теперь у плиток есть порог, а цифры табличные, так что смена числа не меняет ничего вокруг. Замер на тикающих часах: ни одна плитка не двигается. Вторая — пиксельные отступы, подогнанные на 1440x900: 240 для рейки, 336 для колонки, 356 для того, что рядом, повторённые в каждом месте и ни подо что не подстраивавшиеся. Теперь они из переменных, и брейкпоинт двигает все панели разом: 200/300 ниже 1280, 180 и без колонки ниже 1100, где она становится шторкой, и ни одной на телефоне. Что это вскрыло по порядку: заголовок и строка закона не доходили до края на 280px, потому что скрытая колонка всё ещё резервировала ширину; на 768 полоса статуса наезжала на заголовок, потому что отступ считал полосы ниже, чем они есть; а на 390 заголовок переносится на две строки, и плавающий элемент не может сообщить телу, сколько занял, — поэтому он в потоке, а его позиция задавалась дважды, и решал порядок файла, что теперь сказано в конце, а не оставлено случаю. Плитка сектора рисовалась под слот футера: в полосе её знак и две колонки фактов наезжали на имя, поэтому она показывает имя и состояние. Проверено на 1440, 1100, 768 и 390: ни одна пара полос не пересекается, ничто не скроллится ни по одной оси, а на телефоне порядок читается как полоса, заголовок, закон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two of the six views floated on the sky and four were sheets over it. The specs frame was the worst of them: an iframe carries its own document, and that document's ground made the tab a black rectangle over the star map. Embedded, the Explorer is a pane of the game rather than a page of its own, so it is marked as embedded and its ground goes. The containers were not the end of it. Probing the centre of each tab found the content itself opaque underneath: the kanban's columns, the factory's city and foundry panels, the tech map with its nodes. Each keeps a veil now instead of a ground — enough to read a card against, thin enough to see the sky behind it. Probed at three points on all six tabs: nothing opaque is left between the map and the eye but the body, which is the ground beneath the sky. Карту видно на каждой вкладке, а не только на гребёнке. Из шести видов два плыли над небом, а четыре были листами поверх него. Хуже всех — рамка спек: iframe несёт собственный документ, и его подложка делала вкладку чёрным прямоугольником поверх звёздной карты. Встроенный Explorer — панель игры, а не отдельная страница, поэтому он помечается как встроенный и подложку теряет. Контейнерами дело не кончилось. Проба в центре каждой вкладки нашла глухим и само содержимое: колонки канбана, панели города и литейной на фабрике, карту технологий с её узлами. Теперь у каждого вуаль вместо подложки — достаточно, чтобы читалась карточка, и достаточно тонко, чтобы за ней было небо. Проверено в трёх точках на всех шести вкладках: между картой и глазом не осталось ничего непрозрачного, кроме body — подложки под небом. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… address The container logged four lines at boot and then nothing for the rest of its life. A service that was answering looked identical to one that was not, which is why the working deployment still read as broken: there was no evidence in the logs either way. Every request now logs its method, path and status. The startup line prints host and port instead of dumping the address union as a raw struct, which said nothing a reader could use. Container logs now read: info: Background agent listening on :::8080 info: GET /health -> 200 info: GET /api/agents -> 401 Пчёл стало видно: строка на запрос и читаемый адрес. Контейнер писал четыре строки при старте и молчал всю оставшуюся жизнь. Работающий сервис выглядел ровно так же, как неработающий, — поэтому рабочий деплой и читался как сломанный: в логах не было свидетельств ни за, ни против. Теперь каждый запрос пишет метод, путь и статус. Стартовая строка печатает хост и порт вместо сырого дампа union-а адреса, из которого читателю нельзя было извлечь ничего. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two strips were doing one job. The worlds bar and the status bar each took a row, 128px of map before anything was shown, and the status bar still had more tiles than width — the sector read PRODUCTI… CARD HELD SLOT: SIGN, clipped mid-word. They share a row now. The worlds keep 620px, which is what their six controls need: the select carries flex:1 and had been absorbing the shortfall down to 58px of unreadable repository name. The status takes the rest, and the five counts sit behind a summary that shows bees, done and verdicts closed and drops the five below the row when opened. Measured: the bars are on one line, the select is 180px, the counts open to five tiles at distinct offsets inside the viewport, and the page scrolls in neither direction. Один хедер, а счётчики — в выпадающем списке. Две полосы делали одну работу. Полоса миров и полоса статуса занимали по ряду, 128px карты до того, как что-то показано, и статусу всё равно не хватало ширины — сектор читался как PRODUCTI… CARD HELD SLOT: SIGN, обрезанный на полуслове. Теперь они в одной строке. Мирам отданы 620px — столько нужно их шести контролам: у селекта flex:1, и он забирал недостачу на себя, до 58px нечитаемого имени репозитория. Статус берёт остальное, а пять счётчиков ушли за summary, который показывает пчёл, сделанное и вердикты в свёрнутом виде и роняет пятёрку под строку в раскрытом. Замерено: полосы на одной линии, селект 180px, счётчики раскрываются в пять плиток на разных отступах внутри экрана, страница не скроллится ни по одной оси. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d stops shoving The state goes left and the worlds right: what is read continuously belongs where reading starts. They were two designs sharing a row. The worlds controls came from a page nav — rounded, wrapping onto a second line whenever the row narrowed — while the HUD tiles beside them are square-edged, monospaced and one line high. The worlds take the HUD's shape now: 40px, no radius, mono, uppercase, nowrap. The round tile was the thing that moved. min-width is a floor, not a width, and its text changes constantly — SINCE ROUND becomes OVERDUE, the clock gains a digit, the second line rewrites every round — so it grew and pushed everything after it along the row. It is pinned to 300px with smaller type, and what no longer fits truncates instead of widening the tile. The sector still printed its two columns of facts and clipped them mid-word at PRODUCTI… CARD HELD SLOT: SIGN. In a header it is the name and the state. Measured over three seconds of a ticking clock: no tile moves. Every worlds control is 40px on one line, and the round is exactly 300px. Половины меняются местами, хедер читается как один дизайн, плитка раунда перестаёт толкаться. Состояние уходит влево, миры вправо: то, что читают постоянно, должно быть там, где чтение начинается. Это были два дизайна в одной строке. Контролы миров пришли из страничной навигации — скруглённые, переносящиеся на вторую строку, как только строка сужается, — рядом с квадратными моноширинными плитками HUD высотой в одну строку. Теперь миры принимают форму HUD: 40px, без скруглений, моно, капс, без переносов. Двигала всё плитка раунда. min-width — это порог, а не ширина, а её текст меняется постоянно: SINCE ROUND становится OVERDUE, часы получают цифру, вторая строка переписывается каждый раунд, — поэтому она росла и толкала всё правее. Теперь она закреплена на 300px с уменьшенным шрифтом, а что не помещается, обрезается, а не расширяет плитку. Сектор всё ещё печатал две колонки фактов и обрезал их на полуслове: PRODUCTI… CARD HELD SLOT: SIGN. В хедере это имя и состояние. Замер за три секунды тиканья часов: ни одна плитка не двигается. Каждый контрол миров — 40px в одну строку, раунд — ровно 300px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The loop runs end to end against the deployed service: POST /api/sessions opens a task, GET lists the queue, GET /:id reads one, DELETE closes it after review, and a wrong verb answers 405 instead of the wrong handler. Six faults stood in the way, and all six were on the path between the Queen and a bee. Two verbs were unreachable. The GET arms carried no method guard, so /api/sessions answered POST with the list and /api/sessions/:id answered DELETE with the read — handleCreateSession and handleDeleteSession could not be called at all. Those are exactly the verbs that open a task and close it. Every authenticated request was refused. Header lines end with CRLF and the parser split on \n, so each value kept a trailing \r; inside an Authorization header that carriage return travelled into the token and base64 rejected it. Names and values are trimmed now. Every POST arrived empty. The body walk restarted its offset at zero and reused the line iterator the header loop had drained, so it never found the blank line — and one read() is not one request: TCP may deliver headers and body in separate segments. The request is read until the header block is complete and then until Content-Length bytes have followed it; the body is what follows the blank line. Sessions needed a database that cannot be reached. The Postgres client speaks only trust authentication, so the agent runs with no connection and every session call died with ConnectionFailed. Without a connection the calls keep sessions in memory: not durable, and honest about it, but the loop runs while the client learns SCRAM. Two security faults on the same path. An empty AUTH_SECRET still produces a valid HMAC, so a server running without one accepted any token a forger signed with "" — it fails closed now. The signature was compared with mem.eql, which returns at the first differing byte and leaks how much of a guess was right; the comparison is constant time. Session names went into SQL unescaped, and the Queen writes those names — a quote is rejected rather than quietly changing the statement. Королева может поставить задачу пчеле и закрыть её после ревью. Цикл проходит целиком на развёрнутом сервисе: POST /api/sessions открывает задачу, GET отдаёт очередь, GET /:id читает одну, DELETE закрывает её после ревью, а неверный метод отвечает 405 вместо чужого обработчика. На пути стояли шесть поломок, и все шесть — ровно между королевой и пчелой. Два глагола были недостижимы. У GET-веток не было проверки метода, поэтому /api/sessions отвечал на POST списком, а /api/sessions/:id на DELETE — чтением. handleCreateSession и handleDeleteSession нельзя было вызвать вовсе. Это ровно те глаголы, которыми задача открывается и закрывается. Любой запрос с токеном отвергался. Строки заголовков заканчиваются CRLF, а разбор шёл по \n, и каждое значение сохраняло хвостовой \r; внутри заголовка Authorization этот возврат каретки попадал в токен, и base64 его отвергал. Теперь имена и значения обрезаются. Каждый POST приходил пустым. Обход тела начинал смещение с нуля и переиспользовал итератор строк, уже исчерпанный разбором заголовков, поэтому пустой строки не находил, — а один read() не равен одному запросу: TCP вправе доставить заголовки и тело разными сегментами. Запрос дочитывается до конца блока заголовков, а затем до Content-Length байт после него; телом считается то, что идёт за пустой строкой. Сессиям нужна была недостижимая база. Клиент Postgres знает только доверительную аутентификацию, поэтому агент работает без соединения, и каждый вызов сессий падал с ConnectionFailed. Без соединения сессии живут в памяти: недолговечно и честно об этом, но цикл работает, пока клиент не выучит SCRAM. Две дыры в безопасности на том же пути. Пустой AUTH_SECRET всё равно даёт валидный HMAC, поэтому сервер без секрета принимал любой токен, подписанный пустой строкой, — теперь он закрывается. Подпись сравнивалась через mem.eql, который выходит на первом различии и выдаёт, сколько символов угадано; сравнение стало постоянного времени. Имена сессий попадали в SQL без экранирования, а пишет их королева — кавычка теперь отвергается, а не меняет запрос молча. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Until now the loop existed but a human drove it with curl. The Queen sees the hive and acts in it. Before each answer the proxy fetches the bees' queue and gives it to her, so a review is of the board rather than of memory — she picked the session to close out of what she had been shown, which is the part that could not be faked. Her reply may end with one directive, open_task or close_task, and only those two shapes are forwarded. Everything else in the reply is prose: the proxy never acts on what it did not recognise, and it reports the outcome rather than the intention, so a directive that failed cannot read as work that happened. Measured against the deployed hive: "put a concrete task on bee-1 for tri-net" opened sess_…2fc7b6e3 with status 201; "review it and close it" closed the same session with 200 and left the queue empty. Она сама ставит задачи и закрывает их после ревью. До сих пор цикл существовал, но крутил его человек через curl. Королева видит улей и действует в нём. Перед каждым ответом прокси забирает очередь пчёл и отдаёт ей, поэтому ревью делается по доске, а не по памяти: сессию для закрытия она выбрала из того, что ей показали, — именно эту часть подделать нельзя. Ответ может заканчиваться одной директивой, open_task или close_task, и только эти две формы исполняются. Всё остальное в ответе — проза: прокси не действует по тому, чего не распознал, и сообщает результат, а не намерение, чтобы неудавшаяся директива не читалась как выполненная работа. Замерено на развёрнутом улье: «поставь bee-1 конкретную задачу по tri-net» открыло sess_…2fc7b6e3 со статусом 201; «сделай ревью и закрой» закрыло ту же сессию с 200 и оставило очередь пустой. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rk back It had been patched a dozen times and the patches were fighting: floors set for stability overriding the caps meant to make things fit, a later rule losing to an earlier one on specificity, two halves placed by hand against widths that stopped being true the moment the text changed. Three hundred and forty-five lines of that are gone, replaced by one block. The worlds portal into a slot in the bar and both wrappers dissolve, so the controls are items of the same flex row as the tiles and the browser distributes the width instead of me guessing it. Every tile states a basis; none states a floor, because a floor is what kept overriding the caps. Numbers are tabular, so a value changing never changes a width. The mark is back — hiding it was a way of buying 104px, which is not a reason to remove a game's logo. What the row could not hold is the overview: at 1440 the logo, counts, round, sector, select, four world buttons, alerts and status already claim it, and whatever came last was squeezed to nothing — 15px in the last measurement. It is also a view of its own, so it appears above 1600px and steps out below. Measured at 1440 over three seconds of a ticking clock: the row needs exactly what it has, nothing scrolls, no tile is under 40px, and nothing moves. Хедер пересобран одним контейнером, знак возвращён. Его латали десяток раз, и заплаты дрались: пороги, поставленные ради устойчивости, перебивали ограничения, поставленные ради умещения; более позднее правило проигрывало раннему по специфичности; две половины расставлялись руками против ширин, переставших быть верными в момент смены текста. Триста сорок пять строк этого удалены и заменены одним блоком. Миры порталом попадают в слот полосы, обе обёртки растворяются, и контролы становятся элементами той же flex-строки, что и плитки: ширину распределяет браузер, а не мои догадки. У каждой плитки объявлена опора и ни у одной — порог, потому что именно порог перебивал ограничения. Цифры табличные, поэтому смена значения не меняет ширину. Знак вернулся: спрятать его было способом выгадать 104px, а это не причина убирать логотип игры. Чего строка вместить не смогла — овервью: на 1440 логотип, счётчики, раунд, сектор, селект, четыре кнопки миров, алерты и статус уже забирают её, и то, что осталось последним, сжималось до ничего — 15px в последнем замере. Это к тому же самостоятельный вид, поэтому он появляется выше 1600px и уходит ниже. Замер на 1440 за три секунды тиканья часов: строке нужно ровно столько, сколько есть, ничего не скроллится, ни одна плитка не уже 40px, ничего не двигается. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…, and a schema Sessions survive a restart now: a row written before a deploy comes back after it. Seven faults stood between this service and the database it was written against, and every one of them was silent. The client spoke no password. It understood AuthenticationOk and nothing else — the answer a server gives when it asks for nothing — so no managed database would ever let it in. SCRAM-SHA-256 is implemented against the Postgres SASL framing, including verifying the server's own signature, which is what makes the exchange mutual rather than a password thrown into the dark. It read every message backwards. A backend message is [type][length], and the reader took the length first, so it mistook the top byte of a length for a type. Nothing it received was ever what it thought. It could not read a URL. Splitting "postgres://u:p@h:5432/db" on '/' puts the empty string between the two slashes where the credentials should be, so the user was always empty and the server answered "no PostgreSQL user name specified in startup packet" — a credentials error with no bad credentials in it. Userinfo is split at the last '@' and the first ':', and percent-encoding is decoded, because a generated password routinely contains characters that only survive a URL encoded. Its queries were truncated. The Query message length must count itself; it did not, so the server saw every statement end four bytes early and answered "invalid string in message". It discarded every result. readQueryResponse took the result parameter and immediately voided it, so a SELECT that returned rows returned none — the session list could only ever be empty. RowDescription and DataRow are parsed now, and headers are read to completion rather than with bare reads that may return short and desync the stream. Its table did not exist. Nothing in the repository ever created one. The service owns its schema now and makes it on connect; a failure there is logged rather than fatal, because a container that dies writes no logs and takes the reason with it. Its timestamps were read as something else. The columns hold epoch seconds as BIGINT and the queries asked EXTRACT(EPOCH FROM ...) of them, which exists only for timestamps. Пчёлы сохраняют свою работу: SCRAM, честный разбор протокола и схема. Сессии переживают перезапуск: строка, записанная до деплоя, возвращается после него. Между этим сервисом и базой, под которую он написан, стояли семь поломок, и каждая была немой. Клиент не знал ни одного пароля. Он понимал только AuthenticationOk — ответ сервера, который ничего не спрашивает, — поэтому ни одна управляемая база его бы не впустила. SCRAM-SHA-256 реализован поверх SASL-обёртки Postgres, вместе с проверкой подписи сервера: именно она делает обмен взаимным, а не паролем, брошенным в темноту. Он читал каждое сообщение задом наперёд. Сообщение бэкенда — это [тип][длина], а читалась сперва длина, поэтому старший байт длины принимался за тип. Ничто из полученного не было тем, чем он его считал. Он не умел читать URL. Разбиение "postgres://u:p@h:5432/db" по '/' кладёт на место учётных данных пустую строку между двумя слэшами, поэтому пользователь всегда был пуст, а сервер отвечал «no PostgreSQL user name specified» — ошибка про учётные данные, в которой нет ни одной неверной. Теперь userinfo делится по последнему '@' и первому ':', а процентное кодирование раскрывается: в сгенерированном пароле обычно есть символы, которые только так и переживают URL. Его запросы приходили усечёнными. Длина Query-сообщения обязана считать саму себя; она не считала, и сервер видел конец每 оператора на четыре байта раньше. Он выбрасывал каждый результат. readQueryResponse принимал параметр результата и тут же его обнулял, поэтому SELECT со строками возвращал пусто — список сессий не мог быть непустым в принципе. Теперь RowDescription и DataRow разбираются, а заголовки дочитываются, а не берутся короткими чтениями, рассинхронизирующими поток. Его таблицы не существовало. Её никто никогда не создавал. Сервис владеет своей схемой и создаёт её при подключении; ошибка там логируется, а не убивает процесс, потому что мёртвый контейнер не пишет логов и уносит причину с собой. Его отметки времени читались не тем. Колонки хранят эпоху в BIGINT, а запросы просили у них EXTRACT(EPOCH FROM ...), который существует только для timestamp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header rebuild made it worse, so it is reverted rather than patched again: the three commits that merged the bars, restyled the worlds as HUD tiles, hid the mark and rewrote the row are undone. The mark is back where it was, and the tiles read as they did before — labels, counts and the round in the shape the HUD was drawn in. Two things stayed from that work because they were not style. Tabular digits, so a changing number never changes a width. And the floors that came with the flex row are gone: the bar is a grid again, and its minmax(0,1fr) tracks let tiles give when the row is short, which is what made it fit in the first place. The status bar also gives back the sector tile. The map's own head names the sector, so a copy in the bar was a third one — and it left eleven children against ten grid tracks, which is why every count was crushed to 27px. The worlds move onto the map, where a repository is a place and its tasks are its cells: they render into the map's control row and take that row's scale. Their strip above the shell is gone with them. The row came out 95px over, so the green action is an icon there as it already is on a phone, and the separator before a view name the line below states in full is dropped. Measured at 1440: the map's row needs exactly the 804px it has, the status bar fits, nothing overlaps, and the page scrolls in neither direction. Хедер возвращён, миры переехали на карту. Пересборка хедера сделала хуже, поэтому она откачена, а не залатана снова: три коммита, слившие полосы, перекрасившие миры под плитки HUD, спрятавшие знак и переписавшие строку, отменены. Знак на месте, плитки читаются как раньше. Две вещи из той работы остались, потому что они не про стиль. Табличные цифры, чтобы смена значения не меняла ширину. И пороги, пришедшие с flex-строкой, убраны: полоса снова сетка, и её дорожки minmax(0,1fr) дают плиткам сжиматься, чем она и умещалась изначально. Полоса статуса отдаёт и плитку сектора. Заголовок карты называет сектор сам, так что копия в полосе была третьей — и оставляла одиннадцать детей против десяти дорожек сетки, отчего каждый счётчик сжимался до 27px. Миры уходят на карту, где репозиторий — это место, а его задачи — его соты: они рендерятся в ряд управления картой и принимают его масштаб. Их полоса над шеллом исчезает вместе с ними. Ряд вышел на 95px длиннее, поэтому зелёное действие там иконка, как уже на телефоне, а разделитель перед именем вида, которое строка ниже называет полностью, снят. Замер на 1440: ряду карты нужно ровно те 804px, что у него есть, полоса статуса умещается, пересечений нет, страница не скроллится ни по одной оси. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…verview onto it The header floated 72px below the top edge: --hud-worlds-h still reserved 52px for a worlds strip that had already moved into the map's control row. Removed, the chrome keeps the same 10px margin the sides keep, and everything rose 42px. The map's control row now begins and ends where the field's own content does — its two collapsed spans were spending a gap each and leaving a ragged lead. The round tile is a button with display:contents, so its parts never matched ".queen27-hud-res > *", where the bar states its grid areas and type scale: it laid out unscaled at 410px of a 1400px bar. That is why the overview beside it read "OVERVIEW · gHashT" and three stat labels painted over their own dividers. The overview left the bar for the map, at the foot of the field it describes, and the alert tile left the bar entirely — every event it counted is in the Queen's own log, where it can be asked about. Below 1101 she is a drawer, and her opener moved to the map's controls, since that tile was the only one. Also two rows for the viewport in landscape below 901: the head is static there, so the field was an implicit row, which is where `-1` stops — the phone's module tabs floated in the middle of the map instead of at its foot. Measured at 761, 800, 860, 900, 901, 940, 1024, 1100, 1200, 1280, 1439, 1440, 1920 and 844x390: no overflow, no overlap, no page scroll, on all six tabs and with the rail collapsed. Хедер висел в 72px от верхнего края: --hud-worlds-h всё ещё резервировал 52px под полосу миров, которая уже переехала в ряд управления картой. Убрано — вся оболочка поднялась на 42px и держит те же 10px, что и по бокам. Ряд управления картой начинается и заканчивается там же, где содержимое поля. Плитка раунда — кнопка с display:contents, её части не попадали под правила бара и занимали 410px из 1400: отсюда обрезанное "OVERVIEW · gHashT" и подписи, залезающие на разделители. OVERVIEW переехал из хедера на карту, а плитка сигналов убрана совсем: все события и так в журнале королевы. Ниже 1101 её ящик открывается кнопкой в управлении картой. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…she cannot answer The panel asked localhost:8080 unless a build set VITE_QUEEN_CHAT_URL, so on any built site it reached a port nobody serves and read OFFLINE. It defaults to queen-proxy's public address now — the same shape as DEFAULT_QUEEN_API, which is how the board already finds its origin. The proxy holds the token that reaches trios-agent-server; the bundle is public and carries neither key nor token. Her /chat answers text/event-stream. The proxy called .json() on it, got null, and reported "the Queen returned no message" — so a working Queen and an absent one produced the same sentence. It reads the body once and tries an object, SSE frames, JSON lines, then plain text, and an error frame is passed on as her own words: measured 2026-09-08, "[1310] Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-09-11 23:35:07" was arriving as silence. The website prints what the proxy says instead of "The Queen did not answer." alone, and the log shows how long she has been at it while she works. Чат подключён к trios-agent-server через queen-proxy: адрес прокси теперь значение по умолчанию, токен живёт только в переменных прокси. Её /chat отдаёт SSE — прокси читал его как JSON и получал null, поэтому «королева не ответила» означало и работающую королеву, и отсутствующую. Теперь читаются объект, SSE, JSON-строки и текст, а кадр с ошибкой доходит до чата её словами: сегодня это «Weekly/Monthly Limit Exhausted, сброс 2026-09-11 23:35:07». Рядом с вопросом идёт счётчик секунд ожидания. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ery tab The overview panel drew a picture of the field on top of the field and took 280x224 of the map to do it. What it carried that the map does not is two numbers — how many cards the board holds and how they fall across the six sectors — so those move into the bar as a ninth tile, in the bar's own anatomy: glyph, name, total, and the six counts on the line beneath. The rows keep their names for a screen reader and their click, which still opens the sector. The star map is the ground under one tab, not six. index.css puts the starfield at z-index -5, below the page's own black, so the only sky anyone saw was the hive canvas's own inside the comb: the other five tabs were a black sheet with translucent panels on it, which is translucency over nothing. Zero lifts it above that ground and leaves it under every panel, which start at 3. With the sky lit, four panels turned out to be opaque and are not any more — the mission map's sectors at 0.88, and in the factory the floor (1480x642), the city canvas, the inspector and the bay cells. The specs tab's standing directive was drawn straight over the Queen's first four events: her column is two rows when it is there, and said so as one. Панель OVERVIEW убрана с карты — её цифры (всего карточек и шесть секторов) переехали в верхний хедер отдельной плиткой в общем стиле бара. Звёздная карта теперь под всеми вкладками: в index.css у неё был z-index -5, ниже чёрного фона страницы, поэтому небо было видно только в COMB — из канваса улья. Заодно сделаны прозрачными четыре непрозрачных слоя (секторы миссии, пол фабрики, канвас города, инспектор) и починена вёрстка правой колонки на вкладке спеков. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… on load Reported as "the page does not load", and it was accurate: in Safari the WebGL context was lost the moment Babylon asked for it, the scene threw "Unable to create index buffer" while building the comb, and this app had no error boundary anywhere — so React unmounted the whole tree, taking the board, the rail, the Queen's column and the header with it. React said as much in the console and was ignored. The scene has a boundary now: it is one panel of a HUD, and when it fails the panel says what the browser said and offers to draw it again, which is worth trying — a second context is sometimes granted where the first was not. Two things made a cold load look like a failure of its own. The worlds nav has no slot until the shell mounts, so it rendered in place first: a strip across the top, the layout this HUD replaced, arriving for a second on every load. It waits for its slot now. And the paragraph that says the shared map is loading carried the error class either way, so every cold load opened in failure red on a black page. Сообщение «не загружается» было точным: в Safari терялся контекст WebGL, сцена падала на "Unable to create index buffer", а границы ошибок в приложении не было — React снимал всё дерево вместе с доской, рейлом, королевой и хедером. Теперь у сцены есть граница: падает панель, а не страница, и она показывает слова браузера и кнопку перерисовать. Плюс две вещи, из-за которых холодная загрузка выглядела поломкой: старая полоса миров успевала отрисоваться до появления слота в шапке карты, а строка «загружаю карту» шла красным цветом ошибки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… never renders The map stopped because the second engine of every mount was built on a dead canvas. Babylon's dispose() calls WEBGL_lose_context on the context it made, and a canvas whose context has been lost that way never gets another: every later getContext hands back the same dead one. React 19's StrictMode mounts an effect, cleans it up and mounts it again, so with the canvas in the JSX — one element, reused — the second run asked a corpse for a context. Safari showed it plainly: two Babylon banners, "WebGL: context lost", then "Unable to create index buffer" thrown out of the scene. Chrome grants a fresh context on the same element and hid this for months. The canvas is created in the effect now and removed with the engine, so no mount ever meets the last one's context. The worlds strip was still blinking because hiding it while the atlas loaded covered only half the window. The other half is the shell's own lazy chunk: atlas resolved, nav visible again, shell not yet mounted, no slot. Where the Runtime is what will render, the nav does not render in place at all — it exists only in the map's control row. The pages with no shell keep it where it is. Карта вернулась: у Babylon dispose() гасит WebGL-контекст через WEBGL_lose_context, и канвас, у которого контекст погашен так, второго уже не получит. StrictMode в React 19 монтирует эффект дважды, а канвас в JSX один — и второй движок строился на мёртвом. Теперь канвас создаётся в эффекте и удаляется вместе с движком. Старая полоса миров мигала потому, что пряталась только на время загрузки атласа: вторая половина окна — ленивый чанк самого шелла. Там, где рендерится Runtime, полоса не рисуется на месте вовсе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… context Sharpness was set from the device ratio alone: a 2x display meant a drawing buffer of four times the CSS area — 5.2 million pixels at 1440x900 — and this scene keeps several full-size targets beside it, the glow layer's two blur passes and the image-processing pass among them. Chrome grants that. Safari answered by losing the context at creation, and "Unable to create index buffer" is what a scene says when the memory it asked for was refused. The budget is an area now, 4.2 million pixels, applied at creation and on every resize: a big display still gets sharpness up to that ceiling and never past it. Measured at 1440x900 on a 2x display, the buffer went from 2880x1800 to 2592x1620 and the comb draws. A lost context is also handled as the event it is — preventDefault, which is what lets the browser offer one back, and the render loop stops instead of throwing out of a React effect. Резкость считалась только от плотности экрана: на 2x буфер вчетверо больше CSS — 5.2 млн пикселей при 1440x900, — а рядом сцена держит ещё несколько полноразмерных целей (две размывки свечения и обработку изображения). Chrome это даёт, Safari отвечал потерей контекста при создании: "Unable to create index buffer" — это то, что говорит сцена, когда в запрошенной памяти отказали. Теперь бюджет задан площадью в 4.2 млн пикселей, при создании и при каждом ресайзе; буфер стал 2592x1620 вместо 2880x1800, соты рисуются. Потеря контекста обрабатывается как событие, а не исключение. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ne is visible React reconciles the field's children, and the canvas made in the effect was sitting among them — a node React did not create, between nodes it did. It can be moved or dropped on any re-render that changes that list, and the engine goes on drawing into a canvas nobody is showing: a blank map with nothing in the console to say why. It lives in a stage element now, which React renders and never looks inside. And the scene's chunk is the largest on the page. Until it arrived this slot was empty — a star field with nothing on it, which looks exactly like a map that failed. It says what it is doing while it loads. Канвас, создаваемый в эффекте, лежал среди детей поля, которыми управляет React: такой узел он вправе передвинуть или удалить при любом перерендере списка, а движок продолжит рисовать в канвас, которого никто не показывает — пустая карта без единой ошибки в консоли. Теперь канвас живёт в собственном контейнере, внутрь которого React не заглядывает. Плюс ожидание чанка сцены больше не молчит: пустое звёздное поле неотличимо от несработавшей карты. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he body its whole height Three things the map's chrome was getting wrong. The law and the cell search read over the map's head, first thing and in the page's body type — the one panel here still wearing it. They describe the field: where the snapshot came from, and a way into one cell. That is a legend, so they sit along the bottom edge between the rail and the Queen, level with both, in the HUD's own mono and colour. The commands were 420px in a corner, six tiles of 63px: a glyph each, with the name clipped away underneath, so the row said what the buttons do only to whoever already knew. Across the map's width they are 127px and they are named. And the body took whatever grid row it was auto-placed into, sized to its own content, while the context chip took the second. In the comb that went unseen — the field is positioned over the whole shell anyway. In the specs view the content is an iframe, and it was handed 150px of an 880px shell: the Spec Explorer drew its header and its tab bar, and the code was cut off below them. Measured at 1440x900, that iframe is 660px now and the source reads. Строка закона и поиск ячейки переехали к низу карты, к футеру, и получили тот же моношрифт и цвет, что вся панель. Ряд быстрых команд растянут на всю ширину карты — плитки стали 127px вместо 63, и подписи наконец видно. И главное: тело вьюпорта занимало одну строку сетки по своему содержимому, а во вкладке спеков содержимое — это iframe, которому доставалось 150px из 880: заголовок и вкладки рисовались, а код обрезался. Теперь тело занимает всю высоту, iframe — 660px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d this time Counted, rather than reasoned about: a patch on HTMLCanvasElement.getContext during one load of this page recorded two webgl2 contexts, 183ms apart, on two different queen-hive-scene canvases. This effect rebuilds on [signature, sceneKey] and StrictMode mounts it twice besides, and each run built its own engine. Chrome grants every request. Safari has a small per-page limit and evicts the oldest context to serve a new one — which arrives as "WebGL: context lost" on the context the scene is still building, and then "Unable to create index buffer" thrown out of that build. Two earlier attempts (a canvas per mount, a pixel budget) reduced what was asked for without changing how many times it was asked. The engine now outlives the effect: a rebuild disposes the scene and keeps the context, and disposal of the context itself is scheduled two seconds out and cancelled by the next run, which is what carries it across StrictMode's gap. Same counter after the change: one canvas, one context. Beside it, three things the user asked for. The kanban speaks the HUD's language now — mono, uppercase, the panel's own line and ground — instead of the page's 16.8px body type in white over the sky. Its cards were laid out in an implicit max-content column, 207px inside 176, so every title was cut at the column's edge; they take the column's width and wrap. And the star catalog's badge is off the map: the HYG data is CC BY-SA 4.0 and the credit is a condition of using it, so it moved into the menu, where this HUD already lists where its facts come from, rather than being deleted. Посчитано, а не выведено: перехват getContext за одну загрузку страницы записал два контекста webgl2 с разницей 183мс на двух разных канвасах. Эффект пересобирается по [signature, sceneKey], плюс StrictMode монтирует его дважды — и каждый прогон строил свой движок. Chrome выдаёт все; у Safari лимит на страницу, и он вытесняет старый контекст ради нового — это и приходит как "context lost" с последующим "Unable to create index buffer". Теперь движок живёт дольше эффекта: пересборка меняет сцену, а не контекст. Плюс канбан в стиле HUD над звёздным небом, карточки по ширине колонки, и подпись каталога звёзд убрана с карты в меню (лицензия CC BY-SA требует указания авторства). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The kanban took the HUD's line and colour in the last commit and still read as a sheet where the other tabs read as glass. Two reasons, both measured: the ground was doubled — a column on --hud-panel at 0.42 and every card another 0.5 on top, about 0.71 where the cards are, while every other panel in this shell is a single 0.42 veil over the sky. And the card titles were still the page's proportional face at 16.8px, the last of it left in this view. The card keeps its outline and lets the column's one veil be the ground; the titles are the same mono as everything else. Measured after: card background rgba(0,0,0,0) over a column at 0.42, and the stars read through the board. Канбан уже получил линию и цвет HUD, но всё ещё читался как лист, а не как стекло. Причины две: основание было двойным — колонка 0.42 и поверх неё каждая карточка ещё 0.5, около 0.71 там, где карточки, тогда как любая другая панель здесь — одна вуаль 0.42 поверх неба; и заголовки карточек оставались наборным шрифтом страницы в 16.8px. Теперь карточка держит только контур, а фоном служит одна вуаль колонки, и заголовки — тот же моношрифт. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…abs, and none of the map's controls The SPECS tab was a black rectangle over the star map, which is the one thing every other panel here had stopped being. Measured inside the frame: three solid layers — the embedded document's own root, the Explorer's root, and the source pane, whose ground is an inline style and so beat any rule from outside. The first two go transparent; the third is chosen where that style object is built, so embedded it is the shell's own veil (one 0.42 layer, the value every panel out there floats on) and standalone the Explorer keeps its opaque panel, because there it is a page and not a pane. Its tab strip scrolled sideways, so past Zig the backends existed only for whoever thought to drag a row that gives no sign it can be dragged — Verilog and the HIR beside it were off the end of an 804px pane. It wraps: one more line, every name legible at once. And the map's controls are gone from this view. Copy-to-agent, fit view, the zoom pair, fullscreen — every one addresses a scene that is not on this screen, and the Explorer is given back the 66px they stood in. The standing directive goes with them: it counted clean, flagged and rejected, which the Explorer's own filter strip counts on the same screen, and it took the head of the Queen's column to say it. Вкладка спеков была чёрным прямоугольником поверх звёздной карты. Замер внутри фрейма: три сплошных слоя — корень встроенного документа, корень Explorer и панель исходника, чей фон задан inline-стилем и потому не перебивался снаружи. Первые два прозрачны, третий выбирается там, где этот объект собирается: во встроенном виде — вуаль шелла 0.42, отдельной страницей — прежняя панель. Полоса вкладок больше не скроллится вбок, а переносится: видны все имена. И убраны элементы управления картой (они действуют на сцену, которой здесь нет) вместе с директивой, чьи цифры Explorer показывает сам. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clicking a cell opens its spec over the map, and that panel was the last opaque thing left in the view: a near-solid ground with a blur behind it and a frame painted black, so a spec reached from the star map arrived as a black card over the sky the map is made of. It takes the same veil as every other panel here and lets the Explorer inside it be transparent — which it already is, embedded. The detail card a cell opens first gets the same treatment. Спека, открытая по соте, была последним непрозрачным элементом: почти сплошной фон с размытием и чёрный iframe поверх звёздной карты. Теперь та же вуаль, что у остальных панелей, и прозрачный фрейм — код читается, соты видно сквозь него. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re readable The map was mounted only for the comb, so every other tab was the flat starfield with panels on it — asked five times, in the end as "где 3D карта с Babylon.js?". The field is already a fixed, full-shell layer, so mounting the scene always puts it behind whatever the tab draws. Off the comb it keeps drawing and stops listening: its labels, its law line and the cards a cell opens belong to the map's own view, and a scene taking clicks from under the board would be a scene in the way. Its holder became an overlay too — as a flow child it had been pushing each tab's content down by its own height, which put the specs pane two thirds of the way down the screen. With a hive under everything, one 0.42 veil was no longer enough to read through: a cell's outline running through a line of source is a line read twice. The veil is 0.62 now and every text panel is frosted, so the map stays as depth and its detail leaves the words. The embedded Explorer takes the same treatment in two layers — a thin one under its tab strip and its file list, which have no panel of their own, and a darker one under the code. And two smaller things asked for along the way: one loader for the whole shell, carrying the counts the wait is actually for rather than the word "loading"; and the category select's chevron, which the platform draws hard against the border, replaced by the one the worlds select uses with room around it. Улей теперь рисуется под всеми вкладками, а не только под COMB: сцена монтируется всегда, а вне карты перестаёт слушать клики. Её контейнер стал оверлеем — как элемент потока он сдвигал содержимое каждой вкладки вниз на свою высоту. Под ульем одной вуали 0.42 стало мало, чтобы читать: теперь 0.62 плюс матовость на всех панелях с текстом, а у встроенного Explorer два слоя — тонкий под вкладками и списком файлов, плотный под кодом. Плюс единый лоадер с настоящими цифрами и стрелка селекта, отодвинутая от края. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d the cell card speaks green The floating row of six was in the way of the field, and five of the six were already somewhere: fit view and fullscreen in the map's own head, the language in the menu. What it held that nothing else does — the agent packet, the repository, the picked issue — joins that head as icons at its own size, each keeping its name in a tooltip and for a screen reader. The language goes with them, since it was asked for back. And the card a cell opens came from the world pages with their palette: white body type, teal borders, 40px buttons — over the map, a different program's dialog. It takes the shell's green, its mono and its hairline, and under a mouse its controls are 26px like the rest; under a coarse pointer the 44px floor stays, because there it is the rule. Плавающий ряд из шести кнопок убран: пять из них уже были в других местах, а три уникальные — пакет агенту, репозиторий, выбранная задача — переехали иконками в строку управления картой, вместе с переключателем языка. Карточка соты получила цвета и шрифт HUD вместо палитры страниц миров. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ddressable The landing showed the map and the corpus and stopped. The other four modules of the same one screen — the board, the mission map, the factory, the technology tree — were reachable only by opening the shell and finding them. All six are presented now: the module's own glyph, the number that opens it, the same one-line hint the rail uses, and a paragraph saying what that view actually shows, written from the view rather than from the name. Each card opens its own tab, which is why the shell now reads `?tab=` once on mount. Not `view=`: the universe around that page already owns that name for its atlas and its shared core. The grid is auto-fit/minmax, so the column count comes from the width available rather than from a list of breakpoints. Three smaller things asked for alongside: the TRI PHONE / TRI CLAW product strip is off the homepage; the atlas loader no longer prints "Loading GitHub world" — it is the mark and the bar, with the name kept for a screen reader; and the shared loader takes an optional title for exactly that reason. На главной теперь представлены все шесть модулей шелла: глиф, цифра, которой он открывается, та же строка-подсказка, что в рейле, и абзац о том, что вкладка показывает на самом деле. Каждая карточка открывает свою вкладку — шелл читает `?tab=` при монтировании (не `view=`: это имя уже занято атласом и общим ядром). Сетка auto-fit, поэтому число колонок определяется шириной, а не списком брейкпоинтов. Плюс убран блок TRI PHONE / TRI CLAW и текст «Loading GitHub world» у загрузчика атласа. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Continues #890. Saves the user-approved continuous Babylon hive and its shared spec catalog integration. Each canonical spec has one shared-core placement and a source placement inside each contributing repository; real issues surround those gold centers. Matching placements connect on selection without doubling canonical counts. Preserves black space, transparent honey contours, point-down original mark, RU/EN, canonical embedded Spec Explorer and one-window navigation. Includes Node 20-compatible catalog indexing for the existing apex publisher. Local contract suites, actual Node 20 production build, type/API ratchets, ARIA, silicon and independent read-only review pass. Final interactive verification is in progress; publication must use the existing ghashtag.github.io publish-website workflow after review and source integration. No worker, DNS, secret or hosting changes.