Handoff

Цель: новая сессия должна за 5 минут понять где остановились и продолжить. Читай в порядке: этот файл → Rules.mdImplementation_Workflow.md → конкретная Phase.

⚠️ Repo переехал в org: Arnomake/arno (был vadimpianov/arno; старые URL ниже редиректят, не битые). Remote = github.com/arnomake/arno. gh команды с --repo vadimpianov/arno всё ещё работают через redirect, но canonical — Arnomake/arno.


⚠️ CROSS-SESSION (2026-09-07): feat/sorg-layout is now landed into through a MERGE QUEUE — direct push is rejected (ruleset shared-trunk-merge-queue, ADR 0089). Several sessions share this trunk because they share one review surface. Measured before the change: 17 direct pushes in a day, deploys serialised behind a per-branch FIFO (7-11 min, 15-17.6 min when overlapping), and four consecutive red runs froze the stand for ~30 minutes for everyone. New flow: work on land/<topic> → PR into feat/sorg-layoutgh pr merge --auto --squash. Nothing else; the queue orders, batches (up to 5), runs stand-gate on the speculative commit, and ejects a failing PR while merging the rest. A PR touching tools/migrate/drizzle/** rides alonestand-gate fails a mixed batch on purpose, because an applied migration on the shared test Neon cannot be un-batched the way code can. deploy-test.yml is now gatefreshnessdeploy: the gate is per-SHA and never cancelled (and skips itself when stand-gate already passed that SHA), a superseded commit is not deployed, and the deploy is cancellable — the stand shows the newest commit instead of replaying intermediate ones. Every other feat/** / fix/** branch keeps its own test-<topic> stand and direct pushes.

⚠️ PROD-MIGRATION GOTCHA (Punta spacing SSOT, commit 4987fb3). Container spacing moved from CSS classes into the tree (node.gap); the .slc__* group-layout classes no longer carry gap, and .slc__group lost its margin. Existing saved Sorgente compositions (rows in sorgente.composition_instance) predate node.gap, so on load they render with collapsed gaps. The test project was re-seeded by DELETEing its 5 sub-tab compositions (Studio reverts to the new defaultTree). When this ships to prod, prod's saved Sorgente trees must be re-seeded the same way (delete the composition rows for /sorgente/tokens/{colors,typography,spacing,radii,effects} via DELETE /api/v1/sorgente/composition?routePath=…, maintainer-auth), or the catalog spacing collapses. No schema-guard auto-migration was added.

⚠️ CROSS-SESSION (2026-07-09, feat/sorg-layout): single-leaf MERGE is DISABLED. User rule: "all folders behave the same — a container with ≥1 child is a folder; delete its last child and it's pruned". mergedLeafChild now always returns null (layers/single-child.ts), so a box with one leaf child is a normal expandable folder, NOT a merged one-row leaf. This supersedes the mergedLeafChild-based leaf-detection a parallel session built: Dito (action-registry.ts isLeafLike, Dito.tsx:170) and Punta (BoxSidebar.tsx hideTracks) now treat any single-leaf box as a FOLDER — full Dito toolbar + TRACKS shown. Specimens are exempt because they are no longer box-wrapped: a load-time migration unwrapSpecimenBoxes (types.ts, in the useStudioTree chain) promotes any container whose only child is a specimen (ColorSpecimen/Typography/Primitive/Spacing/Radius/Shadow) to a bare leaf — the ~34 drifted swatch cards self-heal to clean leaf rows. So: specimen = leaf; every other container = folder. Do NOT re-enable merge to "fix" swatches. Commits 286407d (Cmd+G group + Shift-range-select), 2690813 (merge disable), 4407579 (specimen unwrap).

⚠️ CROSS-SESSION (2026-07-09, drop-tracker.ts): drag-COMBINE added + draggedParentId helper completed. New feature: dragging a row onto another and pushing right combines them into a new folder (combineLeafId on the drop target → shell calls tree-ops combineIntoFolder). While wiring it, a parallel session's UNCOMMITTED edit to mergeInto referenced draggedParentId(tree, activeId) with no definition — the whole app failed typecheck. I added the obvious 1-line helper (findNode(tree,id)?.parent?.id ?? null) to unblock; if that session also defines it, drop the dup. Their Reel.tsx / PuntaIcons.tsx WIP was left untouched (not committed by me).

Session 2026-08-20 — Instant sidebar tab switch: Workflow ⇄ Design system keep-alive (branch feat/sorg-layout)

Sidebar rail switch Design system ⇄ Workflow was ~1–3.5 s. Diagnosed live (browser-driven, JS-instrumented) and fixed in three layers, all on feat/sorg-layout, deployed to test-sorg-layout.arnomake.com:

  1. eff38782 — per-project cache in use-workflow-artboards.ts / use-workflow-artboard-links.ts: state survives the route unmount, no refetch on re-entry (killed workflow-artboards + entity-links, ~690 ms each).
  2. 5977496eexperimental.staleTimes in next.config.mjs: Next 14.2 client Router Cache keeps the RSC payload, so the tab switch stops re-fetching the destination RSC (~0.5 s warm, up to 5 s cold through the preview proxy).
  3. 84d2496b + 15df644c + c98f3aaf — new apps/web/src/features/studio-workspace/ (StudioWorkspace): keep-alive host — both surfaces mount once, a tab switch flips pane visibility (not display, so the canvas fit camera stays valid) + history.pushState (URL mirrors the tab, ?project preserved), no route navigation, no remount. popstate syncs back/forward. The two heavy surface elements are useMemo'd so the flip does NOT re-reconcile them (that ~1 s reconciliation was the last floor). Idle surface pre-warmed on requestIdleCallback. Rail: Workflow/Design system are in-place <button>s (useWorkspaceTab); editor/docs/backend stay <Link> routes.

Both /app/workflow and /app/library boot StudioWorkspace with their own initialTab — deep-links + refresh land on the right tab. Full doc comment in StudioWorkspace.tsx is the source of truth.

Measured live (switch = MutationObserver on pane data-active):

  • Seed (~800 boxes): 20–40 ms both directions (was 1.7–3.5 s / ~1.0 s).
  • Stress test — 100 real artboards (11.5k boxes, 24 985 DOM nodes, tokens + DS components; seeded via API PUT then deleted, seed restored): switch 60–84 ms — still under 100 ms.

24/7 concern (huge files open all year) — verified safe:

  • Parked surface is fully inert: 817-box AND 11 497-box parked Workflow → 0 DOM mutations over 6–8 s idle, heap Δ ~1–2 KB. No continuous rAF loop / setInterval / infinite CSS animation anywhere in the canvas stack (tela/compasso/quadro/albero/punta/layers/artboards-canvas) — canvas is event-driven, idle = ~0 CPU regardless of file size. Hidden pane also has pointer-events: none.
  • Memory: both surfaces resident (seed ~33 MB; 100 artboards ~134 MB), no leak (parked tree static). Scales with file, one-time not continuous.
  • Initial load of a 100-artboard file ~7.7 s — inherent big-file render cost, not keep-alive (old route-remount would pay it on every switch instead of once).

Cross-talk bug + fix (e6a24ae4). Keep-alive broke the studio-bus invariant "one mounted surface = one publisher": the parked surface still called registerActiveTree, so the visible surface's Layers/Punta rendered the parked tree (after refresh + idle pre-warm the Workflow tree bled into the Design system / Component Builder view; the shared selection outline dropped). Fix: module store features/studio-workspace/active-surface.ts (setActiveWorkspaceSurface + useIsWorkspaceSurfaceActive, via useSyncExternalStore so the gate doesn't re-render the memoised tree). Both tree publishers now gate on their surface being the visible tab: workflow-composition-artboard.tsx (!active || !surfaceActive) and the Library ComponentBuilder visible (&& libraryActive). Mirrors the pre-existing component-builder/PageArtboard.tsx visible gate. Verified live (both triggers: manual switch, and refresh+pre-warm). Rule: any new keep-alive surface / registerActiveTree publisher MUST gate on useIsWorkspaceSurfaceActive. See user-memory reference_studio_bus_single_active_surface.

Open follow-ups: if 100-artboard files become the norm and load/memory bite, the real lever is artboard virtualization/culling (render only viewport artboards) — helps initial render + memory + switch together. Not built. Verified: pnpm --filter @arno/web typecheck, next build (29/29 static), workspace-tabs L1 test, workflow/page.invariants, live L8 smoke.

Session 2026-07-16 — Workflow/Screen edits stopped resetting; Sorgente gets a real project row (branch feat/sorg-layout)

Reported as "switching sidebar tabs resets my edits" on test-sorg-layout.arnomake.com/app/workflow/?project=prj-arno-sorgente-seed. Three stacked causes, all fixed + verified live via the maintainer's own Chrome session (not the sandboxed preview — auth required, no credentials entered).

Root cause 1 — two useWorkflowArtboards instances. Workflow (ArtboardsCanvas) and Screen (ProjectScreenCanvas) each called the hook independently; switching tabs unmounted one, cancelling its in-flight 600ms-debounced saveTree before the other's fetch landed. Fixed by lifting the hook to WorkflowShell, passed down as props to both (contract W7, S7).

Root cause 2 — Punta Typography never rendered. size/weight/line/ tracking staged correctly into node.props (Punta write path fine) but TextFactory (registry.tsx) never read them back — edits were invisible and, for heading/subheading variants, not even attempted past family. Fixed in render/text-style.ts (textTypographyOverride, unit-tested).

Root cause 3 (the actual "still resets" repro) — prj-arno-sorgente-seed had no project-table row, so every GET/PUT of its workflow_artboard 404'd ("Project not found") — nothing ever reached the DB regardless of fix 1. A parallel session hit the same 404 mid-session and shipped the opposite fix (c32c959/5fc906d: permanently skip persistence for Sorgente) — reverted, since it directly blocks the stated need (Sorgente used as a real project by Arno's own interface developers, not just a token showroom). ADR 0071 gives it a real row instead (one-shot seed-sorgente-project-row.yml, run

  • removed same session, PRs #96/#97 on main). Verified: edit → Workflow ↔ Screen → full reload survives; /api/v1/me/projects + the projects-list pinned-card dedup (ADR 0054 §5) confirmed no duplicate card.

Gotcha for future sessions: if workflow-artboards 404s again for Sorgente, the row got dropped (e.g. a test-DB rotation restored from a dump older than 2026-07-16) — re-run the pattern in ADR 0071 §Decision, don't reach for the skip-persistence special case again.

Session 2026-07-15 — Tela: Adaptive removed + per-project default; Workflow artboard editing (branch feat/sorg-layout)

All shipped to the test-sorg-layout stand and browser-verified. Commits c86a91fa14373c (interleaved with other sessions on the shared branch).

Tela — Adaptive removed → configurable, persisted Default device:

  • Adaptive (the size===null no-frame mode) is gone from the strip; a device frame is always active. DEFAULT_TELA_SIZE = 1440×900 seeds the active + a new defaultSize state (getDefaultSize/setDefaultSize/subscribeDefaultSize/ isDefaultSize). The Default device (radio in Tela settings) is the base-editing context for per-width overrides — replaces Adaptive's role (ADR 0065 amendment). node-bridge (gap) + StudioRenderer (columns) gate on isDefaultSize, not size===null.
  • The default is a persisted per-project setting — new column project.default_viewport jsonb (migration 20260714140000, applied on live test-Neon via migrate push; DB-default backfills existing rows). API: create assigns 1440×900, /me/projects returns it, PATCH /projects/:id accepts it (validator apps/api/src/viewport.ts + test). Client: ProjectMeta.defaultViewport, updateProjectViewport, useProjectViewport; the Workflow page seeds Tela on open + PATCHes the Default change back. ADR 0070.

Workflow tab — composition artboards are editable device frames:

  • Artboards size to the active Tela preset; the ROOT fills the frame via a new StudioRenderer fillHeight prop (.studio--fill-height, rules in StudioRenderer.css, guarded by fill-height-css.test.ts) — so selecting the artboard edits its own fill/stroke/radius/padding in Punta (clicking the frame picks the root; ⌘-click picks a box). Frame chain transparent → no default border, radius rounds the artboard. Seed = Stack ("Row") primitive + concrete values (#ffffff, 24px, 8px). Selection matches the DS/Inspector Tweak look (gradient ring + hollow gradient corner diamonds), counter-scaled by 1/zoom. Library rail + zoom pill temporarily hidden behind flags.
  • Docs: docs/workflow/architecture.md §Workflow-tab artboard editing.

Gotchas hit: committing page.tsx by pathspec swept a parallel session's uncommitted ProjectScreenCanvas WIP (untracked file → CI TS2307); reverted the committed side to SorgenteLibraryCatalog, left their WIP unstaged (0410fb6). The test-sorg-layout stand persists Workflow artboards in localStorage (Sorgente is synthetic, no DB row) — reset by clearing arno.workflow.* keys. Stand assets carry a contradictory Cache-Control (no-store + immutable); server serves fresh, but browsers need ⌘⇧R.

Session 2026-07-14 — feat/sorg-layout → dev + DEV_DATABASE_URL retirement

Landed the Studio subsystem on dev. Merged feat/sorg-layout → main via PR #92 (opens in a new tab) (288 commits: full Sorgente Studio + @arno/{ponte,tela,punta,layers,dito}, ADRs 0055/0063/0064/0066). Gate: full pnpm --filter @arno/web exec tsc --noEmit clean → PR opened → main-CI green (verify 5m15s; only the advisory suggest bot flaked). ci.yml auto-deployed backend arno-api-dev + FE dev.arnomake.com. Prod untouched (main→prod is manual since 2026-07-03). Merge also pulled the proxy pin-fix #91 into the branch (clean, no conflicts).

Verified on dev (drove the maintainer's authed Chrome). dev.arnomake.com/app/library renders the seed composition identical to test-sorg-layout; all API calls hit arno-api-dev with GET /api/v1/sorgente/state200. No data copy was needed: dev + test both resolve the Sorgente DB to the rotating arno-test-active by Neon project name (ci.yml reseeds the dev worker's DATABASE_URL + SORGENTE_DATABASE_URL each push) — the ADR 0052 "separate dev Neon" is dead. Bundle parity confirmed (core Studio chunks byte-identical dev vs test).

Retired DEV_DATABASE_URL (PR #93 (opens in a new tab)). It was read by nothing live (ci/release/deploy-test resolve by project name), only by 6 vestigial manual one-shots against a now-deleted separate dev Neon: deleted apply-sorgente-{schema-all,component-entry,layout-override}, debug-dev-sorgente, debug-db-secrets, seed-sorgente-secret-test. Corrected the stale HANDOFF warning that claimed the prod release path was broken (release.yml was already fixed to resolve arno-test-active by name). Then deleted the orphaned GH secret DEV_DATABASE_URL itself. User-memory reference_arno_release_sorgente_step_broken updated to "resolved".

Still open (unchanged by this session): feat/sorg-layout branch stays (Studio work continues on it, worktree active). Temp GET /api/v1/sorgente/_debug/conn still in sorgente.ts. 6-secret rotation. Eventual full prod release ships the Studio work with it.

Session 2026-07-12 — Full-repo review + remediation (batches 1-5) + security hotfix to prod

Full-repo code review (6 dimensions via subagents: frontend/backend quality, tests, security, performance, infra) → triaged map → fixed and shipped batches 1-5. Every finding was re-verified against origin/main before acting — the agents were pointed at the divergent local ~/Desktop/Claude/Arno main, which carries a superseded architecture (old per-env arno-sorgente.ts model, a different workflow set, red port-v9-synth-tokens typecheck, a 2s sorgente-overlay poll), so ~40% of the raw findings were local-main phantoms that do not exist on canonical (e.g. a destructive wipe-non-allowed-users.yml, a migration-journal desync). Lesson recorded in user-memory project_arno_local_main_divergent: audit Arno from an origin/main worktree, not local main.

Merged to main (3 PRs, all CI green):

  • #87 (opens in a new tab) fix(security)P0 OAuth open redirect → JWT theft → account takeover: auth.ts used the caller's return_to verbatim as the JWT-bearing redirect with no allowlist. Added cors.safeReturnTo() (reuses the origin allowlist via new isAllowedOrigin()), validated at login + callback. P1 stored XSS: captured SVG rendered via dangerouslySetInnerHTML in the arno origin → now rendered through an <img> data URI (browser never executes script in image-loaded SVG). +6 cors.test.ts cases.
  • #88 (opens in a new tab) cici.yml never ran @arno/foundation (104) + @arno/shared (17) tests on the main gate; added the steps (121 tests).
  • #89 (opens in a new tab) fix(db) — hot-path indexes on project.owner_id / staged_component(user_id,last_activity_at) / component_md_versions(project_id,file_path,saved_at) (were seq-scans; migration 20260712120000_hot_path_indexes, additive); stop leaking raw neon() error text to clients (url-imports/captured-pages/fonts → let app.onError return opaque 500); url-imports POST now gates project_id ownership; CompositionRenderer wrapped in React.memo. +5 regression tests.

Security hotfix shipped to prod (isolated). Prod (arnomake.com) was last released 2026-06-09 (83f9815); main is ~445 commits ahead (a month of unreleased studio/sorgente/fonts work, some unverified). Per maintainer decision, did not full-release main — instead a surgical hotfix: branch from the prod SHA + cherry-pick only the security commit (applied cleanly — the vulnerable lines were unchanged since June) → gh workflow run release.yml --ref hotfix/.... OAuth + SVG fixes are live on prod; the month of features is not. Hotfix branch deleted after. #87/#88/#89 are on main + dev, will ship with the eventual full release.

✅ Release path fixed (was: broken 28P01 on globals.css). release.yml step "Regenerate globals.css from sorgente schema" no longer reads the stale DEV_DATABASE_URL secret — it resolves the Sorgente DB by Neon project name (arno-test-active) via NEON_API_KEY, DATABASE_URL_TEST fallback (same resolver as ci.yml / deploy-test.yml). DEV_DATABASE_URL is now referenced by nothing (2026-07-14): the vestigial DEV_DATABASE_URL-based one-shots (apply-sorgente-*, debug-dev-sorgente, debug-db-secrets, seed-sorgente-secret-test) were deleted and the GH secret retired. Sorgente schema is live on arno-test-active (dev + test share it, resolved by name).

Deferred follow-ups (out of the batch-1-5 scope, noted for a future session):

  • N+1 serial GitHub calls in sync.ts (createBlob loop) and webhooks.ts (getContent loop) — parallelize with bounded concurrency (GitHub secondary rate limits); deploy.ts already shows the Promise.all pattern.
  • Coverage for the highest-consequence untested backend paths (sync.ts merge/ conflict, gdpr.ts delete-scoping, api-client.ts 401-retry) — needs an integration harness (real Postgres) / jsdom; mock-only tests would be tautological.
  • God-file splits (composition-renderer.tsx 1352, api-client.ts 1298, index.ts 525 business logic) + the feature-folder migration — the review's "batch 6", intentionally left as trigger-driven per the Rules migration policy.
  • Fix the broken release sorgente step (above) before the next full prod release.

Session 2026-07-08 — Token-optimization sweep (docs/memory/hooks, no app code)

Session-context cost reduction, no product code touched. Three doc PRs to main: #82 (opens in a new tab) (archive sessions ≤2026-06-07 → HANDOFF-archive.md), #83 (opens in a new tab) (Rules: read HANDOFF via offset), #84 (opens in a new tab) (archive DONE-milestone logs + Rules: delegate code investigation to subagents, Dito naming invariant relocated from user-memory). Net: HANDOFF.md 1683 → 975 lines (−42%); completed-milestone + old-session detail lives in HANDOFF-archive.md (read only when old history / an evergreen section is actually needed).

Non-repo changes (recorded here so they're not invisible): removed the duplicate local Figma MCP server (figma-mcp @127.0.0.1, Dev-Mode-only) from ~/.claude.json; added Tools/Hooks/handoff_size_check.sh (SessionStart reminder to re-archive when HANDOFF > 1400 lines, reminder-only — never mutates the file, since worktree sessions edit it concurrently); consolidated global user-memory 52 → 40 notes.

⚠️ Local checkout caveat. The ~/Desktop/Claude/Arno main is an intentionally divergent branch — it carries a local commit adapting the /build multi-agent TDD workflow (.claude/) to run at the Arno repo root (apps//packages/), whereas origin/main holds the generic project/-subdir form. Do not ff / merge / reset local main to origin (it would clobber the adaptation and break /build-in-Arno). When origin doc files move ahead, sync surgically: git checkout origin/main -- HANDOFF.md HANDOFF-archive.md Rules.md _index.md. There is also unrelated uncommitted WIP (CVD/daltonism filter) in the working tree — don't sweep it into commits. Full detail in user-memory project_arno_local_main_divergent.

Session 2026-07-12 — Phase 17: Studio tools → reusable @arno/* packages (branch feat/sorg-layout)

Done + browser-verified on the stand. The Studio kernel and the four live tools were extracted into workspace packages so an edit propagates to every importer via workspace:* with no copies (ADR 0063, Implementation_Workflow.md Phase 17):

  • @arno/ponte (packages/ponte/) — shared kernel: studio-bus, types, tree-ops, adaptive/primitives, render/even-size, single-child, telemetry (studio-events, transport injected via configureStudioEvents, wired app-side in studio/configure-ponte.ts).
  • @arno/{tela,punta,layers,dito} — the tools; imported by name (import { Punta } from "@arno/punta"), NOT from studio/<tool>/.
  • useStudioTree stayed app-side (binds persistence). single-child (mergedLeafChild) was promoted from layers/ into @arno/ponte — so the 2026-07-09 cross-session note above about mergedLeafChild now lives at packages/ponte/src/single-child.ts, imported as @arno/ponte/single-child (behavior unchanged: always returns null). Tool DAG is acyclic ponte ← punta ← {layers, dito}; tela standalone.
  • Kernel shims removed; app-side studio code (render/, sections/, seeds/, inspector/tweak+ruler, canvas catalog) imports @arno/ponte directly.
  • Chord-debt: @arno/layers is a generic name — rename to ordito/strati in a later ADR (ADR 0059).

Commits (all on feat/sorg-layout): 2fb922d ponte · 71133be tela · c034108 punta+single-child · 3120f10 layers · 83ff95a dito · 06d3763 layers-scaffold repair + shim removal · bf33be1 browser-verified. Note: 3120f10/83ff95a are internally incomplete (a concurrent session sharing this worktree disturbed the index between add and commit; layers scaffold landed in 06d3763) — harmless under squash-merge; HEAD is complete + green.

Verified: 595 package tests + @arno/web typecheck/tests + pre-commit monorepo typecheck/lint. Browser regression on test-sorg-layout.arnomake.com /app/library under auth: pick (layers→ponte-bus→punta), tela preset→device frame, punta instant-write→canvas, dito hover panel — all work across package boundaries; studio-bus singleton deduped; zero console errors.

Pending: proof-of-reuse (import a tool in a SECOND part of Arno — capability ready, not yet demonstrated); the @arno/layers chord rename.

Session 2026-07-04 → 07-07 — Punta/Layers polish + ship to dev (branch feat/sorg-layout; PR #79 to main)

All on feat/sorg-layout. feat/sorg-layout was MERGED to main (PR #79) → dev pair (dev.arnomake.com + arno-api-dev) now carries the Sorgente Library surface. Later Layers/Punta commits are on the branch, NOT yet re-merged to dev (maintainer hasn't re-approved a merge).

Shipped & verified live (test-sorg-layout.arnomake.com):

  • Layers container glyphs = the node's assigned Punta primitive (shared registry adaptive/primitives/primitive-meta; Layers + Punta can't drift).
  • Box-gap |·| even-snap on write (node-bridge.setGap) — kills odd-px on canvas + the migrate round-trip "jump".
  • useStudioTree dirtyRef — a stale Sorgente poll no longer clobbers an actively-edited tree (the "gap jumps around the 3rd arrow click" bug).
  • Sorgente egress −99%: /api/v1/sorgente/version change-token poll (4s, hidden-tab pause, debounced refresh) replaces the 2s full-state loop.
  • Collapse-all / expand-all toggle in the Layers header (pinned to the rail edge, symmetric to the root icon; expandableNodeIds in collapse-all.ts).
  • Leaf-row cleanup: a leaf (and a single-leaf box, which IS a leaf to the designer) shows ONLY its Component/Text icon — no primitive, no padding glyph. Padding badge lives on REAL folders (multi-child) that own padding, shown only when padding > 0 (ownPaddingSides/hasOwnPadding). Punta TRACKS lists container children only (leaf children removed — can't take a primitive).
  • Logo short-form + pinned "Arno Sorgente" card resolve the seed name/gate statically (survive test-DB reset/rotation).

Infra fixes this window:

  • feat/sorg-layout was 4 commits behind main → the CI "merge origin/main" preview step conflicted and failed EVERY deploy. Fixed by merging origin/main into the branch (clean). Keep the branch current or deploys break for all sessions on it.
  • deploy-proxy: dev.arnomake.com resolved via the Pages canonical deployment, but CI ships dev as a BRANCH preview → froze on an old build. Now resolves <branch>.<project-subdomain> alias (PR #80).
  • ci.yml dev pair rides the rotating test DB by Neon project name (arno-test-active) — the dead DEV_DATABASE_URL had been failing every main push. Rotation reseeds the dev worker too.
  • ADR 0052 amended (PR #78): Sorgente storage = the rotating test DB, not "dev Neon". release.yml Sorgente export re-pointed to the resolved test DB (DEV_DATABASE_URL was a deleted endpoint — next prod release would 've failed).

Neon one-time setup DONE: NEON_API_KEY repo secret set; test project renamed arno-test-active. Nightly backup + hourly quota-lock auto-rotation live on main (verified by a forced rotation: user id + JWT survived).

Pending: temp GET /api/v1/sorgente/_debug/conn still in sorgente.ts (remove after a week clean). Sorgente historical rows on the Arno project's production branch aren't read by any worker — import decision open.

Session 2026-07-03 — Test-DB survival infra + Sorgente rewiring (branch feat/sorg-layout + main PRs #75-77)

Free-tier Neon quota (HTTP 402) had been killing the test stand. The stand is now self-healing, and the session surfaced + fixed a wrong assumption about where Sorgente data lives.

Infra shipped (workflows live on main — cron/dispatch require default branch):

  • test-db-backup.yml — nightly 03:00 UTC pg_dump of the active test DB → test-db-dump artifact (30 days). The dump carries ALL schemas incl. sorgente.*.
  • test-db-rotate.yml — hourly /ready probe; on quota lock: create fresh Neon project → restore latest dump → top up migrations → rename old → arno-test-retired-<run> / new → arno-test-active → reseed BOTH worker secrets (DATABASE_URL + SORGENTE_DATABASE_URL). Snapshot-forward only, никаких merge. Проверено боевым force=true прогоном: user id, JWT и Sorgente-данные переживают ротацию.
  • deploy-test.yml — резолвит живую тест-БД ПО ИМЕНИ Neon-проекта arno-test-active через NEON_API_KEY (fallback: DATABASE_URL_TEST secret). Сеет оба секрета воркера из одного resolved URL.
  • Neon аккаунт — organization-scoped: GET /projects без org_id пуст; резолверы обходят personal + все org scopes.
  • tools/migrate: db:export / db:import; push.ts нормализует URL через URL API (drop channel_binding, force sslmode=require для neon.tech).

Ключевое открытие: DEV_DATABASE_URL secret вопреки имени указывал на ТЕСТОВЫЙ проект — Sorgente тест-стенда живёт в той же ротируемой тест-БД, а не в "dev Neon" из ADR 0052. После удаления retired-проекта Sorgente падал с password-auth — починено переводом сида на resolved URL. ⚠️ ADR 0052 при этом НЕ обновлён — нужно решение юзера (amendment или отдельный ADR).

UI-фиксы (класс «зависимость от строки в БД» убит):

  • Логотип short-form на Sorgente-странице: имя seed-проекта резолвится в коде (isSorgenteProject → "Arno Sorgente"), не из projects-таблицы.
  • Закреплённая карточка "Arno Sorgente" на странице проектов рендерится по live maintainer-пробе, без projects-row.

Egress (2026-06-19, тот же спринт): /sorgente/state poll 2s → version-token 4s + пауза скрытых вкладок + debounce refresh — ~99% срезано; квота больше не сгорает за день.

Секреты: NEON_API_KEY добавлен (rotation/backup/deploy резолв); DATABASE_URL_TEST теперь только fallback; DEV_DATABASE_URL для arno-api-test больше не используется (остался у arno-api-dev).

Pending:

  • Удалить temp GET /api/v1/sorgente/_debug/conn из sorgente.ts после недели без рецидивов.
  • Решить судьбу ADR 0052 (test-Sorgente = rotating test DB, не dev Neon).
  • Данные Sorgente на production-ветке Neon-проекта Arno — юзер подтверждал их наличие; сейчас стенд их не читает. Открытый вопрос: нужны ли они.

Session 2026-06-15 — Layers panel UX polish (branch feat/sorg-layout)

Layers tool got a deep UX pass on top of the v1 tool. All landed on feat/sorg-layout (shared worktree with the Punta session — see collision note below). Verified live via Claude-in-Chrome with realistic timing (background-tab throttling caveat — see docs/layers/architecture.md §Resize note).

Shipped & verified:

  • Multi-drag — Shift-pick set drags together; moveNodesAt (tree-ops) preserves DFS order, strips root, dedups descendants, cycle-guards every picked container. Tests D10–D21.
  • Drag-hover auto-expand (spring-loaded folder, 100ms) + auto-scroll hot-zone during drag.
  • Keyboard nav — Arrow up/down (+auto-pick), Arrow left/right collapse/expand-or-move, Delete/Backspace, Cmd+Z; Enter pick.
  • ARIA — role=tree/treeitem, aria-level/expanded/selected, polite live region for drop target.
  • Resize fade — non-root rows fade opacity to 0 between FADE_THRESHOLD_PX (36) and MIN_WIDTH (32); pick + hover gated on expanded width.
  • Root-icon click — single=collapse, double=fit-to-tree (DBL_WINDOW 300ms); collapse→expand restores the PRE-collapse width; collapse from <40 restores to 100. 5-branch state machine — see architecture.md.
  • Width sizing rewrite — physical width + min-inline-size:0, rAF tween 120ms ease-out with document.hidden snap-guard. (Root cause of the long "fit locks to a value / reverse / abrupt" saga: CSS transition on a flex item's width doesn't reflow; logical inline-size ignored by flex-basis. Full writeup in architecture.md §Resize.)
  • Deselect — click a selected row clears it on BOTH layers + canvas (new bus requestDeselectAll → Tweak closeInspector). Shift+click drops one id.
  • Esc — clears selection both surfaces + blurs focused row (kills the :focus-visible ring that lingered on root).
  • Layer taxonomy documented — box/text/component canon in docs/layers/glossary.md §"Layer taxonomy" + docs/punta/contract.md.

Deploy note: GitHub Actions was billing-blocked mid-session (org billing); once cleared, normal push deploys. Local wrangler pages deploy can't reach the CF account (403, no scoped token) — rely on Actions.

⚠️ Cross-session collision (2026-06-15) — d6249b0 bundled Punta's staged work

Layers and Punta share ONE worktree on feat/sorg-layout. Commit d6249b0 (titled as a Layers width fix) ALSO contains Punta's then-staged changes — Punta.tsx, Punta.css, docs/punta/contract.md, docs/punta/invariants.md, Rules.md — because git commit picked up everything in the shared index, not just the Layers paths. Nothing is lost; the tree was green. Punta session: do NOT re-commit those edits, they are already in d6249b0. History was left as-is (no force-push — a live session shares this worktree). Lesson for both sessions: commit with an explicit pathspec (git commit -- <paths>) so a shared staged index doesn't get swept into the other's commit.

Cross-session note → Punta (2026-06-15, from the Layers session)

Layers and Punta currently share branch feat/sorg-layout. Three things the Punta work-stream should know:

  1. Shared CI gate. deploy-test runs pnpm -r typecheck + pnpm -r test across the whole repo. A mid-flight Punta refactor that leaves any punta test uncompilable blocks the Layers deploy too, not just Punta. (Happened today: the registry split BOX/TEXT/COMPONENT left section-registry.test.ts / validators.test.ts red for a window.) Resolved by commit d0e2c46 — tsc clean, 91 punta tests green. Keep the branch green or flag here before pushing a breaking intermediate.
  2. Layer taxonomy is documented and canonical. docs/layers/glossary.md §"Layer taxonomy" (box = row|column; text/component = node.type "component" split by ref). Consumer mapping in docs/punta/contract.md §"Applicable sections by layer type". If you change how sections map to layer types, sync both docs.
  3. Tree shape is shared kernel. LayoutNode (row/column/component) lives in studio/types.ts — single source. Punta added gap on containers (good). Any further new node field is a contract change — note it here so the Layers side (widthForTree, drop-tracker, renderer) doesn't drift.

Session 2026-06-11 / 2026-06-12 (latest) — Layers tool complete (branch feat/sorg-layout)

Sidebar panel + drag-drop + magnet pick поднят до strategic-tier инструмента (30k MAU horizon). Все три bug-репорта от пользователя закрыты на корне через gap+depth модель + auto-prune + аудит.

Что зашло:

  • Bug fixes (committed early in session):
    • root icon выравнен с depth-1 column, edges 8/8 left+right
    • drop-to-bottom больше не уходит "предпоследним" (surface-aware over rect)
    • auto-fit width учитывает что deepest visible row — leaf vs container
    • ban empty layers — pruneEmptyContainers строго дропает blank Text (включая undefined / non-string / whitespace) + cascading empty containers
  • gap+depth drop model — заменил dnd-kit row-zone collision своим pointer-driven tracker'ом. Курсор Y → gap между двумя visible rows, cursor X → depth (snapped к indent grid). Один drop-line indicator, один parent-row tint, нет ambiguity "before/after which row". Modules: drop-tracker.ts, tree-ops.moveNodeAt, tree-ops.insertFromPaletteAt.
  • Instrumentation pipeline. Новая таблица sorgente.studio_event (migration 20260611120000_sorgente_studio_event.sql). POST/GET endpoints в apps/api/src/sorgente.ts. Client emitter (studio-events.ts) собирает drag_end / drag_cancel / prune / undo / reset события батчами 1s × 10. drag_end payload включает intent + divergence flag (post-rAF DOM check vs tracker promise) — этот класс багов "показывает в одно место, ложится в другое" теперь ловится автоматически в digest.
  • Capability flags scaffoldSurfaceCapabilities тип на шине, 5 флагов (allowDrag/Delete/PaletteInsert/ReorderRoot/EditNodeProps), defaults all-true. Gates вшиты в StudioDndShell + LayersSidebar context menu + useStudioTree сигнатуре. Surface'ы opt-in без изменения кода Layers.
  • Test harness. 55 vitest unit-сценариев (tree-ops + drop-tracker pure exports + types + studio-bus). Coverage gate активен — vitest-config scope: tree-ops.ts + types.ts. Thresholds: 100% lines / 100% functions / 92% branches (over 90% gate). .github/workflows/layers-tests.yml блокирует merge при падениях / drift coverage. Каждый сценарий sourced из chat-репорта или явного spec rule — никакой фантазии.
  • /app/sorgente/layers-digest — страница digest с window-selector (7/14/30/90 дней). Цветовая индикация divergence rate vs threshold. Samples list для triage. Notifier 14-day click-through landing.
  • Notifier pipeline. Local launchd plist (com.vadimpianof.arno-layers-digest, fires every 14d via StartInterval=1209600s) + shell script (~/.local/bin/arno-layers- digest.sh) добавляет Notifier card с click-through на digest URL. Загружен через launchctl load в текущей сессии. Plist + script не в git (лежат в ~/Library/LaunchAgents/ + ~/.local/bin/).
  • Документация. Полный 11-файл canonical set в docs/layers/ (README / _index / architecture / contract / invariants / integration / api-reference / telemetry / testing / migration / scenarios / glossary). apps/web/src/dev-tools/studio/_index.md + studio/README.md обновлены как umbrella manifest (studio != layers; layers — один tool внутри). Code-side _index.md + README.md рядом с кодом — pointers на docs/. Rules.md §Studio tools — documentation pattern (parallel session) codifies the 11-file pattern repo-wide.

Параллельная работа в этой ветке (не моя, в коммитах честно помечено):

  • docs/nav-bar/ — nav-bar tool scaffold, 11-файловая структура по тому же шаблону. Стартовая работа над следующим tool'ом.
  • Rules.md §Studio tools — codification 11-file pattern as binding rule.
  • apps/web/src/app/sidebar/... rail width — отдельная UI работа, не Layers.

Открытые / parked items (зафиксированы explicit в docs):

  • Component tests (Layer 2) + E2E (Layer 3) — нужны при первой регрессии вне pure logic (DOM, focus, scroll, keyboard).
  • 10 planned сценариев K01–K10 в scenarios.md — требуют Layer 2/3 инфраструктуры.
  • Multi-select drag, extended shortcuts (Cmd+D/G/C/V), locked nodes, i18n UI labels, visual regression Playwright — в README.md Roadmap.
  • /app/sorgente/layers-digest page визуально не проверен (deploy in flight; страница смонтирована, fetcher + thresholds готовы).

Ключевые commits (chronological):

SHAЧто
380f527root alignment + edge padding restore
57bfbdcedges 8/8
4095acbsurface-aware over-rect (drop-to-bottom fix)
d6ddd29auto-fit width per-row
675600dban empty layers — strict prune
db05ee8gap+depth drop model
b7620d7instrumentation + 22 unit tests + Notifier pipeline
d32af5cdocs(layers): full 11-file documentation suite
ff819ebCI gate + digest page + capability flags
ea98d9aparallel-session docs/nav-bar + Rules.md doc pattern codification
9323b59coverage gate active (95% lines / 90% branches / 100% functions)

Sign-off 4 оси:

  • Компактно: 1 контракт (studio-bus), 1 source-of-truth для типов, без forks
  • Документировано: docs/layers/ 11 файлов + _index/README рядом с кодом
  • Наблюдаемо: studio_event table + digest endpoint + страница + Notifier таймер
  • Тестируемо: 55/55 unit pass, CI gate активен, coverage 100%/92%/100%

Layers tool готов. Next session — другие studio tools (nav-bar в скаффолдинге; renderer / palette / inspector в roadmap).


Session 2026-06-08 — Sorgente editor surface complete

Built on top of ADR 0052 architecture (see entry below). /app/sorgente is now a full CRUD + audit editor for the shared design system:

  • Tokens — 98 token overrides (ported from globals.css :root via tools/port-v9-synth-tokens). Each row: label + color swatch + value input + Apply / Reset / × Delete. AddTokenForm at top for new rows. Live optimistic preview via SorgenteOverlay polling.
  • Brand → Seeds — BrandSeedField per slot (picker + hex + label) + AddSeedForm. Editing hex regenerates dependent ramp downstream.
  • Brand → Bindings — BrandBindingField (semantic id → slot + step dropdowns) + AddBindingForm. Graceful empty-state when no seeds yet.
  • History — apply_log feed from server (when / table / row / env) + Revert button per row that re-plays prev_value through the matching endpoint (UPDATE → restore, CREATE → delete, DELETE → recreate). Refresh button. Every revert creates its own apply_log row so chains walk the audit history.

Architecture:

  • dev-tools/sorgente-draft/use-sorgente-draft.tsx — SorgenteStateProvider (single owner of /sorgente/state 2s polling) + useSorgenteDraft&lt;T>(&#123; kind, id &#125;) generic hook. Optimistic overlay-merge so drafts stage instantly across all consumers of the same target before Apply lands.
  • dev-tools/sorgente-draft/fields/&#123;TokenField,BrandSeedField, BrandBindingField,AddForms&#125;.tsx — composable presentational components, all wired through the same hook. No registry / spec file — new fields = new component import + JSX usage.

Tests:

  • 75 new vitest cases in apps/api/src/sorgente.test.ts covering TOKEN_ID_RE / SLOT_RE / SEMANTIC_ID_RE / STEP_VALUES / HEX_RE validation regex (positive + negative + boundary) and gating smoke tests (unauthenticated → 401, no SORGENTE_DATABASE_URL → 503).
  • Validation regex exported from sorgente.ts so the server-side contract is pinned for tests; AddForms / useSorgenteDraft mirror the same shape on the client.
  • Total api tests: 140/140 green. Repo typecheck 17/17 green.

Live on test stand:

What's NOT built yet (next round candidates):

  • LayoutInspector / SpacingInspector / BrandPanel rewrite onto useSorgenteDraft. Hook + schema ready; rewrite is gated on a product UX decision (per-element CSS override vs editing the token the element consumes). Maintainer iterates on /app/sorgente directly today.
  • Composite token support (typography / shadow). Scalar-only path covers everything ported from globals.css; composite values are silently skipped in the export.
  • Group Apply transaction (ADR 0052 §3 "Apply all pending in this panel"). Per-row Apply is the only path today.
  • Variant editor (ADR 0044) — schema sorgente.variant_override ready, no edit UI.
  • Prod release. Squash-merge feat → main + release (prod) runs the globals.css regenerator and ships the static CSS bake.

Session 2026-06-08 (late) — ADR 0052 supersedes per-env Sorgente (branch feat/foundation-v1-tokens)

Earlier today this branch carried per-env Sorgente (Phase A+B + Rounds 1-6, see prior entry below). After live trial that architecture turned out wrong for the actual maintainer flow — every edit required a Publish click and per-env sync workflows, but the maintainer needs ONE design system surfaced everywhere, edited instantly from any project. ADR 0052 (docs/adr/0052-sorgente-shared-db.md) replaces the per-env DB model with shared-DB. The product intent of ADR 0049 stays; only the storage layout changes.

New architecture (см. ADR 0052 §1-§4):

  • sorgente schema in dev Neon = single source of truth for brand/variant/composition state
  • All test + dev workers read+write via SORGENTE_DATABASE_URL secret (= dev Neon URL)
  • Prod has no such secret; live Sorgente endpoints fail closed (503). Prod ships only the static CSS bake from the last release (prod)
  • Apply gate at LayoutInspector / SpacingInspector / BrandPanel — local React draft, explicit Apply commits to DB, Reset reverts to last applied
  • Propagation = 2s polling loop in SorgenteOverlay (mounted in /app/layout)
  • No Publish/Pull buttons. No promote/sync workflows. State is already shared.

Landed in this session:

  • ADR 0052 written (docs/adr/0052-sorgente-shared-db.md)
  • Migration 20260608110000_sorgente_schema.sqlCREATE SCHEMA sorgente + 9 tables (brand_seed/binding/font/icon/token_override/wcag_override + variant_override + composition_instance + apply_log). All keyed by user_id.
  • Migration 20260608120000_drop_legacy_sorgente.sql — drops project.is_arno_sorgente column + partial unique index + arno_sorgente_publish_log table. Per-env Sorgente concept gone.
  • packages/db/src/schema.ts — added 9 sorgente.* exports via pgSchema. Dropped per-env legacy schema.
  • apps/api/src/sorgente.ts — new endpoints: GET /sorgente/state, PUT/DELETE per-table Apply endpoints (token-override, brand-seed, brand-binding), GET /sorgente/apply-log. Maintainer gate via auth-allowlist.isLoginAllowed(env, requestUserLogin) + presence of SORGENTE_DATABASE_URL.
  • apps/web/src/components/sorgente-overlay.tsx — rewrote on /sorgente/state polling.
  • apps/web/src/app/app/sorgente/page.tsx — strip UI (no project_id binding); read-only tokens + brand inventory. Inspector wiring lands as follow-up.
  • apps/web/src/lib/api-client.ts — new fetchSorgenteState / applySorgenteTokenOverride / applySorgenteBrandSeed / applySorgenteBrandBinding / fetchSorgenteApplyLog + delete helpers. Old publishArnoSorgente / syncArnoSorgenteFromUpstream deleted.
  • tools/port-v9-synth-tokens/ — one-shot port: parses apps/web/src/app/globals.css :root { ... } block, upserts each var into sorgente.token_override keyed by maintainer's user_id.
  • tools/export-arno-sorgente/run.ts — rewritten as globals.css regenerator (reads sorgente.token_override for maintainer, replaces :root block in-place between /* ===== sorgente:auto-generated:begin ===== */ markers).
  • release.yml — export step now uses SORGENTE_DATABASE_URL (= dev Neon) + MAINTAINER_LOGIN=vadimpianov. Commits the regenerated globals.css if changed.
  • deploy-test.yml + ci.yml — per-env Sorgente export steps removed. Test/dev rely on SorgenteOverlay polling at runtime, no rebuild needed for chrome to reflect Sorgente edits.
  • apps/web/src/app/app/library/page.tsx — removed PublishSorgenteButton + isArnoSorgente lookup.
  • apps/api/src/index.ts/api/v1/me/projects response no longer carries isArnoSorgente.
  • New workflow .github/workflows/apply-sorgente-schema.yml — one-shot applies both new migrations (20260608110000_sorgente_schema + 20260608120000_drop_legacy_sorgente) to dev Neon.
  • New workflow .github/workflows/port-v9-synth-tokens.yml — one-shot runs the port script.

Removed (in same commit, per ADR 0052 §8):

  • .github/workflows/{arno-sorgente-promote,arno-sorgente-publish,seed-arno-sorgente-from-prod,seed-gh-dispatch-token,apply-arno-sorgente-migrations}.yml
  • apps/api/src/arno-sorgente.ts + arno-sorgente.test.ts
  • apps/web/src/components/publish-sorgente-button.tsx

Tests: 65/65 api tests + 17/17 typecheck зелёные.

Pickup sequence next session (in order):

  1. Squash-merge this branch to main (no Publish/Pull/per-env infra is left to migrate; this is a coherent replacement).
  2. Run apply-sorgente-schema.yml against dev Neon to create the new schema + drop legacy columns. After success: git rm it.
  3. Sign in once on dev.arnomake.com so the vadimpianov user row exists on dev Neon (likely already does from prior sessions).
  4. Run port-v9-synth-tokens.yml with login=vadimpianov. After success: git rm it. dev Neon now has every globals.css :root var as sorgente.token_override rows.
  5. Set SORGENTE_DATABASE_URL = $DEV_DATABASE_URL on the prod worker only if you want the maintainer Sorgente UI accessible there — by ADR 0052 §2 prod should NOT have it. Skip.
  6. Set SORGENTE_DATABASE_URL = $DEV_DATABASE_URL on the test + dev workers. Pattern: copy seed-pulse-read-token.yml as a one-shot or wrangler secret put SORGENTE_DATABASE_URL -c wrangler.dev.toml / .test.toml locally.
  7. Push to feat (or merge to main) → next deploy of test/dev workers reads sorgente.* live.
  8. Verify on dev.arnomake.com/app/sorgente + test-foundation-v1-tokens.arnomake.com/app/sorgente: strip UI shows current sorgente.token_override inventory. SorgenteOverlay paints :root overrides over globals.css. Edit a token via Apply (when inspector wiring lands) → all open editor tabs in both envs show within 2s.
  9. Follow-up PRs (not in this commit):
    • LayoutInspector + SpacingInspector + BrandPanel — Apply / Reset gate wired to the new endpoints. Schema is ready; this is pure UI work.
    • Snapshot chain rebuild for drizzle-kit generate (the pre-existing collision is still there; hand-authored migrations remain the active pattern).
    • Rebuild arno_sorgente_publish_log references in HANDOFF if any external scripts still query it.

Known caveats:

  • Test stand frontend will keep showing the static globals.css for non-/app/sorgente chrome until a maintainer Apply via inspector overrides individual tokens. Initial paint = whatever globals.css ships; SorgenteOverlay paints over within 2s.
  • ADR 0049 Rounds 1-6 infrastructure has been historically preserved in git log (commits 663cdd7fdbd55b); no rollback needed since the new architecture supersedes cleanly.

Session 2026-06-08 — Arno Sorgente Phase A + B (branch feat/foundation-v1-tokens)

ADR 0049 phases A and B landed together on this branch. The v9-synth design direction is renamed to Arno Sorgente in every non-historical surface (globals.css header, design-system-legacy.html title/chips, mocks/_index, foundation-catalog-v9.css → foundation-catalog-arno-sorgente.css; ADR 0030 left untouched per Rules §Language ADR carve-out; mock HTML filenames kept for chronology with a rename note in mocks/_index.md). The reserved project id prj-arno-sorgente was DROPPED — the live Sorgente row on prod is the existing prj-by3zajnv flipped to is_arno_sorgente = true, which preserves every FK across captured_page / brand_seed / brand_binding / token_override. Routes resolve by flag, not by id.

Schema (packages/db/src/schema.ts):

  • project.is_arno_sorgente boolean NOT NULL DEFAULT false + partial unique index project_arno_sorgente_singleton ON (is_arno_sorgente) WHERE is_arno_sorgente = true — at most one Sorgente row per env.
  • arno_sorgente_publish_log table — audit row per Publish press (status, source_snapshot_hash, workflow_run_id, deploy_commit_sha, error_message, completed_at).

Migrations: 20260608090000_arno_sorgente_init.sql and 20260608090100_arno_sorgente_publish_log.sql hand-authored (drizzle-kit generate is broken on this branch — two existing snapshots share the same prevId, pre-existing not introduced here). Journal entries idx 29 and 30 added; no snapshot files written — apply via the one-shot workflow path, not via migrate push on test/dev.

Static export (tools/export-arno-sorgente/): new workspace package @arno/export-arno-sorgente. tsx run.ts reads the Sorgente project's brand_seed / brand_binding / brand_font / brand_icon / token_override / wcag_override rows, resolves bindings through generateRamp from @arno/foundation/brand/hct-ramp (new export entry; bypasses the React components barrel so the node script doesn't try to import .css files), emits packages/foundation/dist/arno-sorgente.{css,json,meta.json}. Idempotent: skips writes when the resolved content sha256 matches the prior meta.json hash. Composite tokens (typography, shadow) recorded in JSON but NOT emitted to CSS in Phase A — scalar-only. Gitignore for packages/foundation/dist/ keeps blanket dist/ ignore + explicit un-ignore for the three Sorgente files (release.yml diff-and-commit expects them visible).

Static layer wired: apps/web/src/app/layout.tsx now imports @arno/foundation/tokens.css then @arno/foundation/arno-sorgente.css at root, before ./globals.css. Order matters — Sorgente overrides Foundation. Initial dist/arno-sorgente.css committed as the empty placeholder (:root {}) until the first prod release regenerates it.

Publish endpoint (apps/api/src/arno-sorgente.ts): two routes mounted under requireAuth, both maintainer-gated by ownerId === userId on the Sorgente row (no separate role table — flipping is_arno_sorgente is itself maintainer-only):

  • POST /api/v1/projects/arno-sorgente/publish — snapshots state to sha256, inserts pending row in arno_sorgente_publish_log, dispatches arno-sorgente-publish.yml via GitHub workflow_dispatch using GH_DISPATCH_TOKEN wrangler secret. Returns publishLogId for client polling.
  • GET /api/v1/projects/arno-sorgente/publish-log?limit=N — history pane source. Used by the button below for status polling and by the future revert UI.

Pure helpers canonicalSnapshot + snapshotHashFromCanonical exported for tests (6 new vitest cases — empty stability, order invariance, revoked WCAG drops, audit column dropping). Workflow updates the log row directly via DATABASE_URL rather than calling back over HTTP — fewer auth surfaces, fewer round-trips.

UI: apps/web/src/components/publish-sorgente-button.tsx — auto-polling button in the Library navbar that only renders when the loaded project has isArnoSorgente === true. Status surfaces: Publish to sourcePublishing…Published <sha7> / No changes / Publish failed. Project list endpoint (/api/v1/me/projects) now exposes isArnoSorgente so the Library page hydrates the flag from the same fetch as dsMode.

Route /app/sorgente: client-side resolver — fetches projects, finds the isArnoSorgente === true row, redirects to /app/library?project=<id>. Auth-gated via the /app/** layout's existing AuthGate. Spec §2 route preserved; reserved-id assumption dropped.

Workflows:

  • .github/workflows/release.yml — added permissions: contents:write
    • checkout token + two new steps (Export Arno Sorgente snapshot and Commit Arno Sorgente snapshot if changed) BEFORE the Storybook + web build (the build embeds arno-sorgente.css; running export after the build would ship a stale skin).
  • .github/workflows/arno-sorgente-publish.ymlworkflow_dispatch target for the Publish endpoint. Inputs: publish_log_id. Steps: mark row as in-flight → export → commit-if-diff → mark row terminal (status + commit sha) via node --input-type=module inline scripts. concurrency: arno-sorgente-publish serialises operator double-clicks.
  • .github/workflows/apply-arno-sorgente-migrations.yml — one-shot for test + dev, mirrors apply-pulse-event-migration.yml shape. Applies the two new migrations directly and records them in drizzle.__drizzle_migrations so a future migrate push skips them.
  • .github/workflows/seed-arno-sorgente-from-prod.yml — one-shot. Reads brand state from prod's prj-by3zajnv via PROD_DATABASE_URL, upserts to prj-arno-sorgente-seed on the target env, flips is_arno_sorgente = true. Owner resolved by the target_owner_login input lookup against the target env's user table. Re-runnable: each invocation DELETEs + re-INSERTs the brand rows on the target.

Pickup sequence next session:

  1. Merge ordering — this branch (feat/foundation-v1-tokens) carries schema.ts changes; rebase on main as soon as anti-spy lands so the conflict surface stays small.
  2. Run apply-arno-sorgente-migrations.yml against dev + test (both).
  3. Set GH_DISPATCH_TOKEN on dev + test workers (wrangler secret put GH_DISPATCH_TOKEN -c wrangler.dev.toml and .test.toml; fine-scoped PAT with repo:actions:write on Arnomake/arno). Prod gets the same secret when this branch ships.
  4. Run seed-arno-sorgente-from-prod.yml with target=test, target_owner_login=vadimpianov. Sign in once on test-foundation-v1-tokens.arnomake.com first so the user row exists.
  5. Verify on test-foundation-v1-tokens.arnomake.com/app/sorgente → should redirect to the seeded project's library page, with the "Publish to source" button visible in the navbar.
  6. Squash-merge → main. Release pipeline picks up:
    • the two migrations (pnpm migrate push)
    • the export step (regenerates arno-sorgente.css on every prod release)
    • the commit-if-diff step (any new prod brand-seed edits land in git automatically).
  7. Set GH_DISPATCH_TOKEN on the prod worker. Flip prod's prj-by3zajnv row: UPDATE project SET is_arno_sorgente = true WHERE id = 'prj-by3zajnv'; (one-shot SQL via a temporary workflow, per the established no-direct-prod pattern).
  8. After two clean releases without drift surprises → drop the two one-shot workflow files (apply-arno-sorgente-migrations.yml, seed-arno-sorgente-from-prod.yml).

Known caveats:

  • Drizzle-kit generate is broken on this branch (snapshot prevId collision). Hand-author all new migrations until the chain is rebuilt (a one-shot drizzle snapshot rebuild lands as a separate small PR after this branch merges).
  • The 5 files CONFLICT with PR #45 listed in the prior session are STILL conflicted by my schema.ts edits — they need to be aligned before merge. The migrations themselves are additive (new column, new table), so the SQL files do not conflict.
  • packages/foundation/dist/arno-sorgente.css is committed initially as empty (:root {}). First prod release with is_arno_sorgente flipped regenerates it from prod's brand state. Until then the static layer is a no-op cascade entry (the runtime overlay in Phase D will bring live values for the maintainer).

📦 Старые сессии (2026-06-07 и раньше) вынесены в HANDOFF-archive.md чтобы не грузить контекст каждой новой сессии. Открывай архив только если нужна история до 2026-06-08.


TL;DR

Phase 0-14 🟢 done. MVP scope §IV почти полностью покрыт. Live + работает. Layout Grid Phase 1-3 🟢 done (session 2026-05-25..27, см section ниже). Composition zones unification 🟢 done (session 2026-05-27 late — ADR 0027/0028, см ниже). Backend prod deploy + auto-migrate в CI 🟢 done (ADR 0029 — solo-dev deviation от master spec §III.2.5). Требуется: один раз set GH Actions secret PROD_DATABASE_URL (см "One-time CI setup checklist" ниже). Phase 15-16 осталось: Onboarding → Production readiness (~8 дней).

🎯 2026-06-02 — Mantine capture milestone (live verify, test-capture-v2.arnomake.com project 2):

  • Captured https://mantine.dev/core/button/411 leaves / 263 containers / depth 14, single capture
  • DS grid 48 components — Button/Icon/Text семантика чистая (включая весь sidebar Mantine: AppShell, AspectRatio, Center, Container, Flex, Grid, Group, SimpleGrid, Space, Stack, AlphaSlider..MaskInput — visual эффект «забрал все subpages» за счёт того что Mantine выкладывает sidebar как plain <a>, DOM-walk их подбирает)
  • Workflow reassembly: единый srcdoc 1.36 MB → visual fidelity ≈ 9/10 (vs Stripe 2-3/10, GitHub 6-7/10 в measurement pass). Header + sub-nav + sidebar + main content (huge Button heading, meta-row, 3 tabs) — пиксель-в-пиксель. Остаточный косяк: 2 пары item'ов в сайдбаре налезают на ~5px (position-context, добиваем в P1 если потребуется)
  • Fonts: Outfit (Google, weights 100/100 900/900) — верно классифицировано
  • Tokens: 8 extracted. Fidelity deviation: 1 (off dominant scale). Снап-к-pattern toggle = ON
  • Vindicates capture-v2 P1 stack: animation pause+seek 5eb1a8c + opacity:0 drop ae5d988 + layout cap raise 402e8e5 + positioning context dbf59c7 + step 1-9D pipeline (real text/SVG/data-URI images, container rects+paint order, force-show animation targets, nested DOM render с parent-relative coords, @font-face injection, ::before/::after) — все шаги видны в результате
  • fiber_tree = null на этом capture ИСПРАВЛЕНО (re-verify 2026-06-02, та же ссылка): capture перезахвачен с capture-v3fiber_tree присутствует: React 19.2.6, 2921 компонентов / 208 уникальных / 122 типа / depth 87; 2330 инстансов несут реальные @mantine/core/* имена (Box 818, UnstyledButton 152, Code 98, Text 84, Transition 71, Tooltip 55, ScrollAreaScrollbar 90…), шум только 222 Anonymous + минифицированные u/v. Composition diag-бар: captured 2921 / visible 2353 / with-rect 1739 / rectless 614 / framework-filtered 568. Tokens (актуальные): 424 CSS vars + 20 colors, deviations=0. Полный measurement log → docs/capture_v2_measurement.md §«Actual capture #7». Прежняя заметка «null» относилась к до-v3 снимку (single DOM-walk); B2 tag+role heuristic остаётся валидным fallback для не-React сайтов
  • Investor demo proof-by-example: Mantine = clean DS docs class → one-click → recognizable component catalog + faithful reassembly. До этого момента это был «должно сработать», теперь — рабочая ссылка

Capture onboarding + page reconstruction (ADR 0030) — A + B1 + B2 + B4-basic + B5 (full font subsystem) 🟢 done. Canonical = feat/layout-grid. Шрифты (B5) закрыты: B5a Google-link, B5b-1 detect, B5b-2/3 BYOF upload+embed, Font-Phase-A local-first (device fonts via local()), A2 replace-picker. Deploy-gate (бывш. «B») осознанно отложен (нет публичной редистрибуции — см ниже). Text-edit gating (C) — позже. Verified live: до B5b-1 (2026-05-28); B5b-2/3+A+A2 задеплоены, в браузере под юзером пока не прогнаны (pending).

DONE (всё в каноне, верифицировано вживую на preview):

  • Convergence: оба трека (capture/extension + layout-grid) сведены на feat/layout-grid, дрейф с main = 0, CI зелёный (починены 4 бага пайплайна). См "MERGE 2026-05-28" ниже.
  • A — Onboarding: create → "Browser capture" → entry_mode='capture' → bridge-binding (arno_capture_project + arno-set-project event = мгновенное обновление) → entry_mode-aware sidebar (Library + Workflow + + меню). Extension env-aware (prod + *.arno-1hu.pages.dev) via SET_ENV.
  • B1 capture: lib/find-layouts.js buildLayoutTree() — рекурсивное дерево (container|leaf + layoutCss + запечённые computed-стили в leaf html + node tokens). Verified: 396 узлов на liveblocks.
  • B2-backend: captured_page таблица + POST/GET /api/v1/captured-pages (ownership) + extension UPLOAD_PAGE_TREE.
  • B2-frontend: Design System рендерит реальные leaf-компоненты (captured-library.tsx), fallback на legacy-мок если нет captured-страниц.
  • B4-basic: Workflow рендерит пересобранную страницу из дерева (reconstructed-page.tsx, sandboxed iframe), fallback на граф. + <base> для ассетов.
  • B5a fonts: Google-шрифты линкуются по имени в reassembly; Apple/системные — через system stack.
  • B5b-1 fonts: detectFonts() классифицирует used-шрифты → captured_page.fonts (google|byof, фильтр фолбэков, реклассификация популярных Google). Verified: liveblocks → Suisse[byof], Inter/JetBrains[google].

B5b-2/3 — BYOF upload + embed — КОД ГОТОВ (session 2026-05-28), pending deploy+verify.

  • DB: project_font (base64-байты в Postgres — R2-биндинга нет; MVP-блобстор) + миграция 20260528143232_project_font_byof.sql. Миграция на test-Neon ещё НЕ применена (нужен manual node+@neondatabase/serverless — journal divergent).
  • API apps/api/src/fonts.ts: POST/GET /api/v1/projects/:id/fonts, GET .../fonts/faces (data-URI для embed), DELETE /api/v1/fonts/:id. license_attested обязателен; size-guard ~3MB.
  • web: вкладка Fonts в /app/library (fonts-panel.tsx) — группы google/system/byof, drag-drop, license-чекбокс, OpenType auto-map (lib/opentype-name.ts: name-таблица для ttf/otf — verified 8/8 на системных шрифтах; woff/woff2 → эвристика по имени, brotli в браузере не распаковать).
  • embed: @font-face data-URI в srcdoc reassembly (reconstructed-page.tsx, все faces) + grid (captured-library.tsx, только faces семейства листа). Хелпер lib/font-faces.ts.
  • typecheck зелёный (web/api/db), web-lint чист, docs (rest.md/schema.md) регенерированы.
  • Задеплоено + проверено (2026-05-28): commit 56c5213 запушен в канон, CI deploy-test зелёный. Миграция project_font применена на test-Neon (11 колонок verified, через drizzle getDB+sql.raw). Fonts-роуты смонтированы (/fonts → 401 на unauth = auth-gate до DB). Frontend-бандл содержит Fonts-таб (verified в чанке page-eab09c22).
  • ⚠️ Тест-стенды (trunk-based, 2026-05-29): dev.arnomake.com (последний main), test-<topic>.arnomake.com (превью ветки через proxy-worker). Старые feat-layout-grid*.pages.dev URL мертвы — ветка удалена.
  • Capture-v3 — code extraction pipeline (2026-06-01, ветка feat/capture-v2): strategic pivot от visual DOM-walk reassembly (capture-v2 P1+P2 patches) к React fiber introspection + TSX code generation. Не регрессировать этот flow — это база под investor demo за 5 дней.
    • Phase 1 (commit 4670680) — fiber probe runs in page MAIN world via chrome.scripting.executeScript({world:"MAIN"}). Walks __reactFiber*/__reactInternalInstance*/__reactContainer* from any DOM node, emits flat list {id, name, typeId, parentId, props, depth} with stable per-type identity. Stored as captured_page.fiber_tree jsonb (migration 20260601000000_captured_page_fiber_tree.sql, applied на test-Neon).
    • Phase 2 (commit d403300)apps/web/src/lib/tsx-generator.ts pure-functional pipeline. clusterByType группирует instances по fiber.type identity; inferPropsType unions observed types в TS interface (optional marker если prop не на каждом instance); generateComponentTsx emit functional shell. Sentinels из probe (<children>/<fn>/<array[N]>/<object>) map to React.ReactNode/function/unknown[]. Visible в Code tab новый раздел "Generated TSX (top 20)".
    • Phase 3 (commit 5c73d0e)generatePageComposition walks parent/child graph top-down, emits single Page.tsx file: import block per distinct component + JSX assembly. Sentinels render as prop={/* placeholder */} comments. Caps PAGE_NODE_CAP=2000 / PAGE_DEPTH_CAP=80.
    • Phase 3 hotfixes (ff458b2, 584e62b) — skip Context.Provider/Consumer/forwardRef/memo/lazy/fragment $$typeof noise + name-shape heuristic isFrameworkNoise (length ≤ 3, all-caps ≤ 4 chars, "Anonymous"/"Special"/"Component" fallbacks). Flatten through them: их children promote to parent's level. Empty-string filter + self-close when all kids were noise. Stripe: 1557 instances → 78 user nodes, 527 framework filtered.
    • Phase 4 (commit 188b0fd) — Workflow Composition view default mode когда fiber_tree present. components/composition-renderer.tsx renders nested labelled boxes (legacy fallback) + right side panel с TSX skeleton on click. Mode toggle Composition / Reassembly в header — old DOM-walk reassembly остаётся как legacy fallback.
    • Phase 5 (commit b431c92) — spatial composition: extension extractFiberInPage для каждого fiber находит first host descendant (stateNode.nodeType===1 или recurse children with budget=200), samples getBoundingClientRect + computed {bg, color, br, fz, fw}. Frontend LayoutBox рендерит absolute-positioned box на page-coords canvas с real surface CSS. Hover/selected → label tag floats above. Pre-Phase-5 captures (без rect) fall back на nested view.
    • Ceiling honest: spatial layout работает для видимых компонентов; animation-hidden / portals / fragments / shadow-DOM teleport → null rect → не появятся на canvas. Animation-heavy SPA (Stripe-class) показывают только небольшой видимый slice. Visual fidelity для animated SPA fundamentally limited without runtime compile + real component bodies — это Phase 6+ territory (Sandpack / esbuild-wasm).
    • Demo-ready state: capture on Mantine/docs/admin sites → fiber tree clean (95% real names), composition view useful. Stripe-class → partial. Side-panel TSX inspector работает на любых captures. Generated TSX и Page.tsx уже копируется и компилируется как самостоятельный React project (placeholder bodies).
  • Extension capture-onboarding bind flow (восстановлен 2026-05-31, серия фиксов 1ef4f65..abfe9a5): после trunk-based cutover bridge на новых стендах не привязывал проекты — capture лил в legacy /app/imports/. Сейчас полный поток работает; не регрессировать при изменении packages/url-import-extension/:
    • manifest.json content_scripts.matches покрывает: arnomake.com/app/*, *.arnomake.com/app/* (включая dev.* и test-<topic>.*), *.arno-1hu.pages.dev/app/* (legacy). Сужать без проверки списка живых хостов — гарантировано сломать bind.
    • manifest.json permission webNavigation обязателен — без него background не ловит SPA pushState из Next.js App Router → переключение проекта в тех же tab не обновляет bind.
    • arno-bridge.js env-detection (isProd): dev.arnomake.com помечен как prod (frontend ходит на prod api.arnomake.com, проверено в bundle chunks). Только test-<topic>.arnomake.com + legacy CF Pages → arno-api-test. Несогласованность frontend↔bridge → JWT issued одним backend, validated другим → "Token invalid".
    • arno-bridge.js bind sources (три слоя): (1) localStorage.arno_capture_project flag — для свежесозданных через "+ New project" → "Browser capture"; (2) ?project=prj-XXX в URL — ловит открытие существующего проекта; (3) arno-set-project custom event от React — real-time push без reload. Все три обязательны: убрать (2) → bind не работает для existing projects; убрать (3) → real-time switching не работает.
    • SPA navigation re-bind: chrome.webNavigation.onHistoryStateUpdated в background.js пингует bridge через CHECK_URL message при изменении URL без полной перезагрузки. Plus bridge сам слушает focus + visibilitychange как cheap fallback. Content-script history.pushState monkey-patch не работает (isolated world), не пытайся.
    • Project name resolution race: SET_PROJECT_ID от bridge может прийти раньше AUTH_TOKEN_HANDOFF (JWT ещё не в storage). Handler resolveAndBindProject сразу пишет id-as-name fallback и ретраит fetch /api/v1/me/projects до 5 раз с 1s spacing. Убрать retry → popup застрянет на prj-XXX вместо имени.
    • "Extension context invalidated" silenced: при reload extension stale content-script на открытых страницах теряет связь с SW. Любой sendMessage throws этот error — bridge молча его глотает (один narrow if в setProject). Это dev-loop noise, не баг; не возвращай warn без причины.
    • Dogfood-loop требует ручной reload: изменения в packages/url-import-extension/** → юзеру chrome://extensions reload + Cmd+R на открытых табах ARNO. Без второго шага старый bridge будет сыпать "context invalidated" в Errors. См spawn'нутую задачу про build:dev script (HANDOFF chip 2026-05-31) — long-term: грузить из dist/ без _index.md-блокера + watch-mode.
  • Осталось только интерактивно (нужна авторизованная сессия юзера): sign-in → capture страницы с кастомным шрифтом (Suisse) → вкладка Fonts в /app/library → upload .otf/.ttf (auto-map family/weight) + license-чекбокс → проверить @font-face в reassembly (Workflow) и grid (Design System).

МОДЕЛЬ ПЕРЕСМОТРЕНА на Figma-style local-first (session 2026-05-29, ADR 0030 §6 update). Юзер: «не грузить наперёд — подтягивать установленные шрифты, лицензия только перед деплоем».

  • Phase A done (commit 1d04c40, в каноне): lib/local-fonts.ts — кроссбраузерный probe (FontFace+local(), без permission) + queryLocalFonts() enumerate (Chromium-only) + localFaceCss(). Панель Fonts: кастомные шрифты → статус On your computer / Provided for deploy / Missing; upload переименован в «provide for publishing» (не upfront). Reassembly+grid рендерят установленные шрифты через local(), если нет upload. Render-приоритет: uploaded data-URI → local() → Google link → system.
  • Phase A2 done (commit a496ed9): replace-picker. captured_page.font_overrides jsonb (миграция применена на test-Neon) + PATCH /api/v1/captured-pages/:id. Панель: Missing → дропдаун Replace (free-шрифты ∪ локальные через queryLocalFonts по клику-жесту) + Clear; статус Replaced · → X. Рендер переписывает font-family в токенах И в запечённом leaf-html (overrideStack/overrideHtmlFonts) → замена реально грузится. FREE_FONTS список в fonts-panel.tsx.
  • Phase B (deploy-gate) — ОТЛОЖЕН (решение 2026-05-29, ADR 0030 §6). Аудит: публичной редистрибуции шрифтов нигде нет (embed только в редакторе у владельца; share-viewer рендерит только workflow-канвас; deploy пушит layout-JSON без рендерера — ADR 0028). Лицензия уже собирается на upload. Механизм гейта готов (project_font + /fonts/faces). Re-open триггер: публично-рендерящийся артефакт начнёт встраивать шрифты (share-viewer покажет reassembly ИЛИ deploy-JSON получит рендерер по ADR 0028).
  • Phase C (later): текст-редактор captured-страниц + блокировка правки при отсутствии шрифта. Архитектура держится совместимой.
  • Ключевое (браузерные ограничения): рендер установленным шрифтом + детект «есть/нет» = все браузеры (local()/FontFace); полный enumerated-список = только Chrome/Edge desktop (queryLocalFonts, WICG, не стандарт). Figma делает кроссбраузерный список через helper-агент — нам для MVP не нужен.

NEXT (всё спроектировано в ADR 0030 §6/§7, атомизация B5/B6):

  • Capture v2 measurement pass — done (4 сайта: mantine/github/react.dev/stripe + vercel). Полный трейл → docs/capture_v2_measurement.md §«Actual capture results» + §«Target A/B». Итоги:
    • Target A (fiber-проба брала только 1 корень → 0 на island/RSC) — ЗАКРЫТ, верифицирован. Фикс findRoots (multi-root) в main: github 0→1061, vercel →1136, react.dev без регресса. См §«Target A — RESOLVED».
    • Target B (fidelity под анимацией) — в работе. Reassembly ломается по 2 осям: media (canvas/lazy-img пустые) и structure/flow (горизонталь + наезды).
      • B-white canvas — done+verified (2417fb0 find-layouts.js rasterizeCanvas): vercel hero рендерится. B-white pass 2 (img→drawImage→data-URI) — pending (8 next/image боксов белые).
      • B-structure — ДОМИНИРУЮЩИЙ, не начат. Горизонтальная раскладка: container-detection хранит только flex/grid, дропает display:block-обёртки → теряется вертикальный document-flow. Правка find-layouts.js, рискованная → нужен diagnostic (top-level layoutCss) + no-regress на 4 верифицированных сайтах. Следующий заход начинать отсюда.
    • B3 (inferential token detection) — подтверждённо low-priority (deviations=0 на всех 4, токены solved где есть CSS vars). B6 — legal floor, не блокирует (runtime fiber ≠ source code).
    • ⚙️ Дев-цикл расширения: правка packages/url-import-extension/**pnpm --filter @arno/url-import-extension watch:dev (вотчит source → пересобирает dist/dev-reloader авто-reload). MV3 SW может спать → если не подхватил, chrome://extensions ↻ вручную.
  • B3 — pattern detection → Fidelity deviation-list в Design System + snap выбросов на паттерн в reassembly. Возможно отменится после measurement: если ground-truth источники (framework introspection / CSS custom properties / Tailwind classes) покрывают tokens детерминированно, inferential pattern detection (B3) становится strictly tier-4 fallback.
  • B6 — ownership verification (§7): account-token + extension-checked <meta>/well-known + DNS TXT для коммерческих доменов. Двигается перед capture v2 если v2 начнёт читать чужой source (sourcemaps, vendor lib paths) — legal floor сначала.
  • мелочь: Google-шрифты в DS-гриде (тривиально), Storybook in-app stories, имя проекта в library/workflow UI.

ГДЕ РАБОТАТЬ (trunk-based — cutover 2026-05-29, см Rules.md): main = единственный trunk. Все долгоживущие ветки (feat/layout-grid, feat/capture-onboarding, ui-claude2) УДАЛЕНЫ. Новая работа: git worktree add -b feat/<topic> ../Arno-<topic> origin/main → PR → squash-merge → удалить ветку+worktree. Бэкап старой capture-ветки: origin/backup/capture-onboarding (контент уже в main).

ТЕСТ-СТЕНД (trunk-based): dev.arnomake.com = последний main; test-<topic>.arnomake.com = превью ветки (привязать хост: infra/preview-proxy/attach-branch.sh <topic>). Backend arno-api-test, тест-Neon ветка feat-layout-grid (host ep-holy-resonance-al2n98xv; миграции вручную через node+@neondatabase/serverless). Расширение: load unpacked из packages/url-import-extension, env auto via bridge.

Спека: docs/adr/0030-page-reconstruction.md. API/schema: docs/api/rest.md (opens in a new tab), docs/api/schema.md (opens in a new tab).

🚨 До любых действий в новой сессии: прочитать секцию "Security debt" ниже — production secrets (CF-токен arno-test-deploy, prod DB-пароль) утекли через chat, требуется ротация.

Side feature: URL-import (master spec v1.3 §V unparked) — architecturally complete, parked для V1 launch decision:

  • Pipeline (Phase 1+5+6+10): packages/url-import-extractor — CLI works on real sites (~70-80% coverage для small-biz target)
  • Backend: 5 API endpoints, 3 DB migrations (0007-0009)
  • Frontend: /app/imports list + detail + edit + preview
  • 95 tests passing
  • 13 ADRs (0007-0019) documenting all design decisions
  • См docs/url_import_spec.md, docs/runbooks/url_import_deployment.md
  • Pending: legal review (lawyer ~$500-1000), production DB migration, deploy

Extension MVP (capture для auth-gated sites) — DONE этой сессии:

  • Package: packages/url-import-extension — Chrome/Edge MV3, popup + background SW + content script + overlay state machine + lib (decompose, sanitize, uuidv7, extract-core)
  • Auth: temporary manual paste JWT (юзер копирует токен из /app/settings → paste в popup). Web Store + auto-handoff — Phase 15c/15d.
  • Destination: existing stagedComponent inbox (Camp 1 persistence — ADR 0019)
  • Backend: CORS expanded для chrome-extension://* origins (apps/api/src/index.ts)
  • Frontend: settings page +section с token copy widget (apps/web/src/app/app/settings/page.tsx)
  • См docs/extractor_mvp_spec.md (canonical), docs/adr/0019-extension-capture-path.md (why)
  • Dogfood ready: load unpacked → paste JWT → capture на real auth SPA. Validation cycle открыт.

Live infrastructure

ServiceURLNotes
Frontend (CF Pages)https://arnomake.com (opens in a new tab) (canonical) + https://arno-1hu.pages.dev (opens in a new tab) (CF default)static export Next.js, auto-deploy on push. arno-ijr.pages.dev устарел и не резолвится
Backend (CF Workers Paid)https://arno-api.vadimpianof.workers.dev (opens in a new tab) (+ custom api.arnomake.com)Hono + Drizzle Neon HTTP driver. Auto-deploy через .github/workflows/ci.yml step "Deploy backend → Cloudflare Workers (prod)" на push в main + healthcheck. Manual fallback: cd apps/api && pnpm exec wrangler deploy --config wrangler.toml (без --config wrangler 3.x валится).
DB (Neon Postgres 17)ep-dry-block-al36bkvg.c-3.eu-central-1.aws.neon.tech/neondbFrankfurt, free tier
ARNO repo (GitHub)https://github.com/Arnomake/arno (opens in a new tab) (был vadimpianov/arno — redirect жив)main → arnomake.com via CI deploy step (CF git auto-integration не работает — см ci.yml)
Connected DS repohttps://github.com/vadimpianov/- (opens in a new tab)renamed а-. Generic connected DS slot для render bundle.
Connected DS content repohttps://github.com/vadimpianov/arno-design (opens in a new tab)NEW (ADR 0023). main branch = prod target (Arno-dev), staging branch = test target (Arno-test). Deploy button писует pages/{id}.layout.json сюда.
Render adapter gh-pageshttps://vadimpianov.github.io/-/arno-bundle.js (opens in a new tab)render bundle, GH Action rebuild on push к - repo
Liveblocks dashboardhttps://liveblocks.io (opens in a new tab)prod project (originally stitch, переименован в arno), test project arno-test
Test env frontendhttps://feat-layout-grid-sl4j.arno-1hu.pages.dev (opens in a new tab)CF Pages alias of latest feat/layout-grid deploy. NB: bare feat-layout-grid.arno-1hu.pages.dev (no -sl4j) is a stale alias serving an old build — CF appends a hash suffix to long branch names. Use the -sl4j alias or the immutable <hash>.arno-1hu.pages.dev from CI's "Deploy frontend" log.
Test env backendhttps://arno-api-test.vadimpianof.workers.dev/health (opens in a new tab)Hono на Workers, отдельный Worker от prod. Auto-deploy через .github/workflows/deploy-test.yml на push в feat/** или fix/** + healthcheck.
Test env Neon branchep-holy-resonance-al2n98xv-pooler.c-3.eu-central-1.aws.neon.tech/neondbSeparate branch от prod. Migration journal divergent — initial seed was manual SQL before Drizzle journal tracked, so pnpm migrate push against test re-attempts 0007 and fails on existing tables. New test migrations applied directly via node + @neondatabase/serverless (see ADR 0029 "Test env" section). Prod is clean.

Secrets / credentials map

Wrangler secrets (на CF Workers, arno-api):

  • DATABASE_URL — Neon connection string
  • GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET — для user OAuth (Phase 9)
  • JWT_SECRET — 256-bit hex, HS256 signing
  • LIVEBLOCKS_SECRET_KEY — для room auth (Phase 11)

Local .dev.vars (apps/api/.dev.vars, gitignored) — те же ключи для wrangler dev.

Frontend env vars: нет. Liveblocks public key не используется (authEndpoint flow).

CF Pages dashboard: project arno, env vars: NODE_VERSION=20, PNPM_VERSION=11.1.3.

GitHub OAuth Apps (verified 2026-06-02 — three Apps mirror three env pairs per master spec §III.2.6):

App name (GitHub UI)Client IDCallback URLPair
Arno-prodOv23liPTOBHJwGiboeochttps://api.arnomake.com/auth/github/callbackarnomake.comarno-api (custom domain)
Arno-devOv23liiM4f1napOYRepOhttps://arno-api-dev.vadimpianof.workers.dev/auth/github/callbackdev.arnomake.comarno-api-dev
Arno-test(check via app's edit page in GitHub Settings → Developer settings)https://arno-api-test.vadimpianof.workers.dev/auth/github/callbacktest-*.arnomake.comarno-api-test

History footnote: до 2026-06-02 в GitHub UI существовал один App, формально названный «Arno-dev», но callback указывал на arno-api (прод). Этот рассинхрон HANDOFF vs реальности привёл к одному инциденту: при попытке настроить настоящий dev-pair я случайно отредактировал callback прод-App'а. Сейчас имена соответствуют ролям: Arno-prod (renamed) хранит прод, новый Arno-dev — настоящий dev. При любой OAuth-операции верифицируй callback URL ДО редактирования. Device Flow остаётся включённым только на Arno-prod (для Phase 13 CLI).

ARNO GitHub App (Phase 13 sync, separate entity!):

  • Name: arno-dev-vadimpianov
  • App ID: 3821481 (corrected 2026-05-29 from ground truth — sync PRs #4/#5/#6 on vadimpianov/- authored by app/arno-dev-vadimpianov; deployed GITHUB_APP_ID mints через него. Prior value 3810588 was stale = ID мёртвого pre-rebrand app Stitch dev vadimpianov, который до сих пор installed на -, но не используется — кандидат на удаление после verify test-backend/arno-design)
  • Client ID: Iv23li8Ime5iRYpMDc4U (⚠️ не сверено после исправления App ID — re-verify на странице app перед использованием)
  • Installation ID: 134630026 (на vadimpianov/- repo)
  • Webhook URL: https://arno-api.vadimpianof.workers.dev/webhooks/github
  • Permissions: Contents R/W, Pull requests W, Webhooks R/W, Workflows R/W, Packages R/W, Metadata R
  • Events: push, pull_request
  • Private key + webhook secret в Workers secrets (GITHUB_APP_PRIVATE_KEY, GITHUB_APP_WEBHOOK_SECRET)

Account ownership (SPOF — see master spec §III.2.9)

  • GitHub username: vadimpianov (был <old-github-user>, переименован)
  • CF account: vadimpianof@gmail.com
  • Neon account: GitHub OAuth (vadimpianov)
  • Liveblocks: GitHub OAuth (vadimpianov)
  • Single-owner risk для всех — Phase 16 launch checklist требует multi-owner CF + secrets backup в 2 password managers.

Architecture decisions (ADRs)

Все в docs/adr/:

  • 0001 — Static export Pages сейчас, edge adapter когда Auth/API
  • 0002 — Module-level stores + useSyncExternalStore (composition/library/workflow)
  • 0003 — Design-system source: GitHub Contents API + raw URLs (не local snapshot)
  • 0004 — Render adapter postMessage protocol v1 (iframe sandbox)
  • 0005 — Dedicated Worker arno-api, не Pages Functions
  • 0006 — Backend-driven OAuth (deviation от master spec §III.2.6) — Auth.js не работает со static export
  • 0007-0019 — URL-import / Extension MVP series
  • 0020 — Layout Grid engine (recursive Container/Content, 5 zones)
  • 0021 — Scale ceiling 30K MAU
  • 0022 — Multi-env setup (test + prod isolated)
  • 0023 — Deploy pipeline (JSON push в connected repo, не codegen)
  • 0024 — Storybook design system (replace handcrafted HTML)
  • 0025 — Adaptive overrides + Tokens publish (CSS vars hot-swap)
  • 0026 — Navigation perf paradigm (module cache + optimistic render)
  • 0027 — Composition zones unification + deploy push_lock mutex
  • 0028 — Deploy JSON consumer resolved = defer until demand (3 options + triggers)
  • 0029 — Auto-apply prod migrations в CI (solo-dev deviation от §III.2.5, re-eval triggers)

Monorepo layout (стабильный)

apps/
  web/     — Next.js 14 App Router (CF Pages, static export)
  api/     — Hono + Drizzle (CF Workers Paid)
packages/
  shared/         — domain types + pure functions (no React)
  editor/         — md-parser + store classes (composition/library)
  render-adapter/ — typed postMessage protocol
  db/             — Drizzle schema + Neon HTTP driver
tools/
  tsconfig/       — base, nextjs, node
  eslint-config/  — base, next
  migrate/        — Drizzle Pool driver runner (CI/local)
docs/
  adr/            — 0001-0006 architecture decisions
  development.md, cloudflare-pages.md

CI: .github/workflows/ci.yml (typecheck + lint + drift check + tests + Storybook build + worker bundle gate + Playwright + deploy main). Test env CI: .github/workflows/deploy-test.yml (на feat/** push — Storybook + web + backend test deploy + healthcheck). Connected DS bundle: .github/workflows/arno-build.yml в vadimpianov/- (bundle → gh-pages).


Что 🟢 в DB

Schema (11 migrations applied — последняя 0011_low_dorian_gray.sql ADR 0027):

  • user (Phase 9, GitHub OAuth)
  • project + project.owner_id FK
  • workflow (nodes/edges JSONB — Postgres mirror, primary в Liveblocks Yjs)
  • screen_composition (instances JSONB legacy + zones JSONB nullable canonical — ADR 0027 expand-contract; drop instances после 30d soak)
  • project_share_link (Phase 10)
  • component_md_raw + component_md_versions + trigger purge_old_md_versions() AFTER INSERT OFFSET 20 (Phase 12)
  • connected_repo + session_branch_state (Phase 13)
  • component_drift (Phase 14)
  • staged_component (URL-import V1, parked)
  • deleted_users (Phase 16 GDPR)

Что задеплоено backend (arno-api)

EndpointAuthPurpose
GET /healthpublicliveness
GET/POST /auth/github/*, /auth/me, /auth/logoutmixedOAuth flow + Bearer JWT issue/revoke
GET /api/v1/me/projectsBearerper-user list
POST /api/v1/projectsBearercreate
DELETE /api/v1/projects/:idBearer + ownershipdelete
POST /api/v1/projects/:id/shareBearer + ownershipgenerate share link
GET /api/v1/projects/:id/sharesBearer + ownershiplist
DELETE /api/v1/shares/:idBearer + ownershiprevoke
GET /api/v1/public/share/:tokenpublicviewer snapshot
GET/PUT /api/v1/workflow/:idBearer + ownershipPostgres mirror (primary в Liveblocks)
GET /api/v1/compositions/:idBearer + ownershiplist — returns { zones, instances } per screen (ADR 0027 dual-shape)
PUT /api/v1/compositions/:id/:screenIdBearer + ownershipaccept { zones } (canonical) OR { instances } (legacy) — server dual-writes (ADR 0027)
POST /api/v1/projects/:id/deployBearer + ownershipADR 0023 — push zones to connected_repo. v2.0 JSON, KV push_lock mutex (ADR 0027)
POST /api/v1/liveblocks/authBearer + ownershiproom auth, returns session token
GET/PUT /api/v1/projects/:id/md?path=Bearer + ownershipMD load + save с 409 conflict
GET /api/v1/projects/:id/md-versions?path=Bearer + ownershiplast 20
GET/POST /api/v1/projects/:id/syncBearer + ownershipPhase 13 — push pipeline + status
POST /webhooks/githubHMAC-verifiedGitHub webhook handler (push to main resync)
GET/POST /debug/github/*, /debug/projects/:id/syncENVIRONMENT=developmentPhase 13 acceptance helpers (ping, deliveries, redeliver, merge-pr)

Что в frontend routes

/                        — landing (sign-in CTA если no JWT)
/app                     — projects list (после sign-in)
/app/workflow?project=   — workflow editor (Liveblocks room)
/app/screen?project=&id= — composition editor
/app/screen/preview?...  — preview iframe (connected DS render bundle)
/app/component?project=&path= — MD editor (Phase 12)
/share?token=            — public viewer (read-only canvas)
/share/preview?token=&id= — public prototype walk

✅ Завершённые milestone-логи (Phase 13 sync, Phase 14 drift, Layout Grid, Composition zones, Security debt rotation) вынесены в HANDOFF-archive.md. Здесь остаётся только актуальное состояние + evergreen-справка (infra / secrets / ADR / routes / checklists).


🛠 One-time CI setup checklist

To make the prod auto-deploy pipeline work end-to-end (ADR 0029), set these GitHub Actions repo secrets (Settings → Secrets and variables → Actions):

SecretValue sourceWhyStatus
CLOUDFLARE_API_TOKENCF dash → API tokens → scoped to Workers:Edit + Pages:EditUsed by both web Pages deploy and Workers deploy steps🟡 verify set
CLOUDFLARE_ACCOUNT_IDCF dash → account home → right sidebarSame🟡 verify set
PROD_DATABASE_URLNeon prod connection string (ep-dry-block-al36bkvg...)Used by "Apply DB migrations (prod)" step before backend deploy. Missing this = CI fails fast at the migration step (intentional — better than silent skip)🟢 set 2026-05-27 via gh secret set
CF_API_TOKEN_TEST, CF_ACCOUNT_ID, CF_WORKERS_SUBDOMAINSame as prod but scoped to testUsed by deploy-test.yml on feat/** push🟡 already set per recap

Test env intentionally has no auto-migrate step (test Neon journal divergent, see ADR 0029 Test env note). Apply ad-hoc:

DATABASE_URL="postgresql://...test-neon..." \
  pnpm --filter @arno/migrate apply-one 20260531050728_perpetual_calypso

apply-one (tools/migrate/src/apply-one.ts) runs a single migration by tag and records it in drizzle.__drizzle_migrations so a future migrate push skips it — bypasses the journal-divergence problem without requiring a full reconciliation.


🛠 Dev env one-time checklist (ADR 0022 v2.1 — 2026-06-02)

Invariant: every env is a frontend↔backend pair, deployed atomically by one workflow. arnomake.com ↔ arno-api (release.yml). dev.arnomake.com ↔ arno-api-dev (ci.yml on main push). test-*.arnomake.com ↔ arno-api-test (deploy-test.yml on feat/**). Web build is identical across envs — host-based switch in api-client.ts:HOST_API decides backend.

Owner-side steps (one-time, ~10 min in UIs):

  1. Neon — branch dev from production. Console → Project → Branches → Create from production → copy pooler connection string. Add as GH Actions secret:
    gh secret set DEV_DATABASE_URL --repo vadimpianov/arno
    # paste the pooler URL when prompted
  2. Cloudflare KV — dev namespaces. Locally with CF API token authed:
    cd apps/api && pnpm exec wrangler kv namespace create AUTH-DEV
    pnpm exec wrangler kv namespace create SYNC-DEV
    Replace REPLACE_WITH_DEV_AUTH_KV_ID / REPLACE_WITH_DEV_SYNC_KV_ID in apps/api/wrangler.dev.toml with the returned IDs → commit.
  3. GitHub OAuth App — Arno-dev. GitHub → Settings → Developer settings → OAuth Apps → New:
    • Name: Arno-dev
    • Homepage: https://dev.arnomake.com
    • Callback: https://arno-api-dev.<cf-subdomain>.workers.dev/auth/github/callback
    • → save → copy Client ID + new Client Secret.
  4. Liveblocks — new project arno-dev. Dashboard → New project → name arno-dev → copy Secret key.
  5. Wrangler secrets on arno-api-dev (run locally, requires CF API token authed):
    cd apps/api
    pnpm exec wrangler secret put DATABASE_URL -c wrangler.dev.toml
    pnpm exec wrangler secret put JWT_SECRET -c wrangler.dev.toml          # openssl rand -hex 32
    pnpm exec wrangler secret put GITHUB_CLIENT_ID -c wrangler.dev.toml
    pnpm exec wrangler secret put GITHUB_CLIENT_SECRET -c wrangler.dev.toml
    pnpm exec wrangler secret put LIVEBLOCKS_SECRET_KEY -c wrangler.dev.toml
    # Phase 13 sync — reuse arno-dev-vadimpianov GitHub App (3821481):
    pnpm exec wrangler secret put GITHUB_APP_ID -c wrangler.dev.toml       # 3821481
    pnpm exec wrangler secret put GITHUB_APP_PRIVATE_KEY -c wrangler.dev.toml
    pnpm exec wrangler secret put GITHUB_APP_WEBHOOK_SECRET -c wrangler.dev.toml
  6. GH Actions secret CF_WORKERS_SUBDOMAIN — already set per test-env recap. Verify:
    gh secret list --repo vadimpianov/arno | grep CF_WORKERS_SUBDOMAIN
  7. First merge into mainci.yml runs the new dev-pair block. Watch the run; healthcheck step proves arno-api-dev is reachable before web flips.

Reuse decisions (long-term):

  • GitHub App arno-dev-vadimpianov (3821481) shared with prod — webhook URL stays on prod, dev does not receive push events (no Phase 13 sync on dev; defer until needed).
  • DNS api-dev.arnomake.com deferred — arno-api-dev.<sub>.workers.dev is sufficient (backend is internal per ADR 0022 §5).

⬇️ Phase 15 — Onboarding flow + Extension full UX — NEXT

Goal: Full first-run UX от sign-up до unblocked editing. Подразделяется на 4 фазы (см ADR 0019 + extractor_mvp_spec.md §16):

PhaseЧтоEffortЧто разблокирует
15a — Auth foundationGoogle + Apple + Email magic link backend + multi-provider user upsert + 0010_user_provider.sql + auth tab page18-22hMulti-provider sign-in (target audience ~95% coverage vs ~30% GitHub-only)
15b — Wizard + onboarding4-card project chooser + browser detection + onboarding screens + /app/settings extension status badge + auto-create default project12-15hШаги 1-4 §2 spec'a (signup → wizard → onboarding)
15c — Web Store publishmanifest.key pinned + real branding icons + $5 Google dev account + submission1-2h + 1-3 days reviewОдин-клик install из стора (replaces load-unpacked dogfood)
15d — Auto-handoffexternally_connectable manifest entry + arno.app/auth/extension-flow handshake page + popup multi-provider buttons + replace token copy widget со status badge4-6hManual paste убирается — финальный UX (Шаг 5 §2 spec'a)

Total Phase 15: ~50-60h focused work + 1 неделя Google review = 2-3 недели до full UX.


Phase 16 launch checklist key items (для context)

Per master spec §VII.2 — нужно перед public launch:

  • All MVP scope §IV implemented (Phase 0-15 cover)
  • Test coverage 90% critical paths
  • Lighthouse a11y >90, bundle <4MB
  • Multi-owner CF account + DNS TTL 300s + secrets в 2 password managers
  • All PAGE alert runbooks написаны
  • DPAs signed с vendors (CF, Neon, Liveblocks, Sentry, Grafana, Resend)
  • GDPR data export/delete endpoints
  • ToS + Privacy Policy published

Quick test plan (verify state в новой сессии)

# Prod liveness
curl https://arno-api.vadimpianof.workers.dev/health   # → {"ok":true,"ts":...}
curl -I https://arnomake.com/                          # → HTTP/2 200
 
# Test env liveness
curl https://arno-api-test.vadimpianof.workers.dev/health
curl -I https://feat-layout-grid-sl4j.arno-1hu.pages.dev/   # NB: -sl4j, голый alias stale
 
# Connected DS render bundle
curl -I https://vadimpianov.github.io/-/arno-bundle.js  # → HTTP/2 200, ~395KB
 
# DB schema drift check (no DATABASE_URL hardcode — use shell env / ~/.env)
cd tools/migrate && DATABASE_URL='postgresql://ci:ci@ci.local/ci' \
  pnpm exec drizzle-kit generate
# → "No schema changes" если код + migrations синхронны

Если всё 🟢 — стек целый.


Если что-то сломалось

  1. CF deploy failedgh run list --repo vadimpianov/arno --limit 3
  2. API 500wrangler tail --config apps/api/wrangler.toml (live logs)
  3. DB unavailable — Neon dashboard https://console.neon.tech (opens in a new tab) (auto-suspend free tier — open проект чтобы wake up)
  4. Liveblocks broken — dashboard → твой project → connection status
  5. Bundle outdated — push в connected DS repo (vadimpianov/-) → GH Action перебилдит за ~1 min

Last updated: session 2026-05-29 — font subsystem (B5) closeout. См полный backlog: docs/BACKLOG.md.

Сделано за сессию 2026-05-29 (Claude session, commits 56c5213..56e8bf2 в каноне feat/layout-grid):

  • B5b-2/3 (56c5213) — BYOF: таблица project_font (base64 в Postgres, нет R2) + миграция; apps/api/src/fonts.ts (POST/GET /projects/:id/fonts, GET .../fonts/faces, DELETE /fonts/:id, license обязателен, ~3MB guard); вкладка Fonts в /app/library (drag-drop + license + OpenType auto-map lib/opentype-name.ts, verified 8/8 на системных ttf/otf); @font-face data-URI embed в reassembly+grid (lib/font-faces.ts). Миграция применена на test-Neon; deploy-test зелёный.
  • Font Phase A (1d04c40) — Figma local-first: lib/local-fonts.ts (кроссбраузерный probe FontFace+local() без permission; queryLocalFonts enumerate — Chromium; localFaceCss). Панель: статусы On your computer / Provided for deploy / Missing; upload переименован в «provide for publishing». Reassembly+grid рендерят установленные шрифты через local(). ADR 0030 §6 обновлён (render: uploaded→local()→Google→system).
  • A2 replace-picker (a496ed9) — captured_page.font_overrides jsonb (миграция на test-Neon) + PATCH /captured-pages/:id. Missing → дропдаун Replace (free ∪ локальные) + Clear, статус Replaced · → X; рендер переписывает font-family в токенах И в leaf-html (overrideStack/overrideHtmlFonts).
  • Deploy-gate (B) — ОТЛОЖЕН (8069fd0, ADR 0030 §6). Аудит: публичной редистрибуции шрифтов нет (embed только в редакторе у владельца; share-viewer = только workflow-канвас; deploy = layout-JSON без рендерера, ADR 0028). Re-open триггер: публично-рендерящийся артефакт начнёт встраивать шрифты.
  • Фиксы: cdb69f6 попап расширения показывает активный env-хост (не захардкоженный arnomake.com); 3619abf панель Fonts защитно реклассифицирует устаревшие captures (Inter/JetBrains → google, прячет *Fallback); 46b14df env-aware JWT TTL (тест 90д / prod 7д) — manual-paste токен на тесте перестаёт протухать каждые 7д + поправлен устаревший FRONTEND_ORIGIN→sl4j + копи попапа; fe7c957 deploy-кнопка вынесена на уровень workflow-страницы (была вложена в graph-fallback ReconstructedPage → у capture-проектов с reassembly пропадала).
  • CLAUDE.md — правило: AskUserQuestion не работает в Claude Desktop agent mode (SDK-сабпроцесс без TTY, пустой ответ — known issue), спрашивать текстом.
  • 🚀 MERGE feat/layout-grid → main (PROD) по явной директиве юзера — merge commit 8007c13 (чистый 3-way, 0 конфликтов; на main до этого было 24 chore(brand)-коммита, только в brand/, миграций не было). Пуш в main запустил ci.yml: typecheck/lint/tests/drift/Playwright → прод-миграции (ADR 0029, аддитивные) → деплой backend arno-api → деплой web arnomake.com. Прод-Neon мигрируется автоматически (project_font, font_overrides, zones, entry_mode, captured_page). После этого канон (feat/layout-grid) и main сходятся.
  • Pending (юзер): browser-verify B5b-2/3+A+A2 на стенде feat-layout-grid-sl4j.arno-1hu.pages.dev (sign-in → Fonts → установленный кастом рендерится; Missing → Replace работает). Гард-правило «decision-of-record (ADR/scope/архитектура) — показать выбор и ждать явного да перед фиксацией; авто-commit только для обратимого кода» — добавлено в CLAUDE.md (2026-05-29).

Сделано за сессию 2026-05-28 (Claude session, commits 2c60708..0978c7d):

  • ADR 0027 — composition zones unification + deploy push_lock mutex. Migration 0011 (screen_composition.zones) applied на test, backfill 3/3 rows. Backend dual-shape (zones canonical + instances legacy soak 30d). Deploy serializes full zones (v2.0 JSON). User-verified UI: sidebar persists, deploy JSON has zones.
  • ADR 0028 — JSON deploy consumer resolved = defer until demand (not pending work).
  • ADR 0029 — auto-apply prod migrations в CI (solo-dev deviation от §III.2.5). PROD_DATABASE_URL secret set. Manual approval gate removed.
  • CI — prod backend deploy automated on push to main (was manual wrangler deploy).
  • Perf — subscription leak fix (cross-project corruption) + Liveblocks defer (−65KB First Load на editor/workflow) + dead bootSnapshot cleanup.
  • Phase 14 — verified end-to-end (correction: было «not verified», но drift workflow ran 2026-05-23, component_drift populated).
  • Runbookdocs/runbooks/composition_zones_backfill.md (prod backfill procedure для soak-end).
  • BACKLOG.md — consolidated not-done list created.

Параллельная сессия (same worktree) сделала Phase 15b part A — entry_mode capture flow (bb5eb1e, 70c0ac5). Migration 0012 (project.entry_mode) applied на test. Histories reconciled, git clean.

Pending (требует юзера):

  • 🔴 6 секретов rotation (через chat прошли)
  • Merge feat/layout-grid → main (prod migration авто через CI после merge)
  • Master spec — auto-reconciles, no manual work. feat _index.md = v1.4, main = v1.5 (scale ceiling), но feat не трогала этот файл после ветвления (git log merge-base..feat -- _index.md пусто) → git 3-way merge возьмёт main's v1.5 автоматически, без конфликта. Changelog bump для ADR 0027-0029 сделать на main после merge (одна строка). Merge origin/main → feat ВЫПОЛНЕН (2026-05-28): разрешены конфликты — migration collision (main 0011_faithful_slapstick staged_component.project_id + feat zones/entry_mode → перегенерированы в чистую линейную 0012_free_amphibian), CORS (взят main's unit-tested ./cors модуль), projects-list (feat's refactored entry_mode modal), ADR index (regenerated; удалён дубликат 0021-scale-ceiling-30k-mau.md в пользу main's 0021-scale-target.md). feat теперь содержит main's историю → финальный feat→main merge будет чистый.

Parallel on main (2026-05-28): Extension v1 + v2 layout mode полностью реализованы (v1 тег v1.0-components). Filter chips, Escape modal, ghost filter, thumbnail fix, pseudo-element bg fix — документированы + проверены. AutoTests manual checklist в packages/url-import-extension/AutoTests/manual.md. (Пришло в feat через merge.)

Earlier 2026-05-27: Layout Grid Phase 1-3 DONE + 4-commit perf series (e0a20ba..c544df2) на feat/layout-grid. ADR 0020-0026 written. Test env live на feat-layout-grid.arno-1hu.pages.dev.

Previous (2026-05-24): Extension MVP DONE (7 commits, packages/url-import-extension live). Capture pipeline на dogfood-ready load-unpacked install. URL-import V1 (CLI) still architecturally complete + parked. Phase 14 backend + frontend committed (26cba92); end-to-end drift verified (см Phase 14 section — workflow ran 2026-05-23, component_drift populated). Phase 15 reframed на 4 sub-phases.