Goal: sequence of ARNO development phases where every iteration produces a visible artifact — something you can open, interact with, demo.
Principles
- Vertical slicing > horizontal layers. Each phase = full-stack vertical through the current feature.
- Demo-driven. Each phase ends with something you can open in a browser and show.
- Throwaway acceptable in spike phases (1-7). Mocks → real services incrementally.
- Cumulative. Each phase builds on the previous one.
- Connected design-system as first integration target. Real MD + TSX already exist in
Desktop/Claude/<connected-repo>/. Dogfood from day one. - Master spec (
_index.md) — canonical reference for tech decisions. The workflow here is the sequencing; it does not duplicate architectural decisions.
Phase overview
| # | Phase | Visible result | Days | Status |
|---|---|---|---|---|
| 0 | Foundation skeleton | Blank Next.js on *.pages.dev | 1 | 🟢 done — https://arno-ijr.pages.dev (opens in a new tab) |
| 1 | Static workflow canvas | Mock graph of 5 screens with edges | 2 | 🟢 done — https://arno-ijr.pages.dev/app (opens in a new tab) |
| 2 | Editable canvas (local) | Create/move screens and edges | 2 | 🟢 done — https://arno-ijr.pages.dev/app (opens in a new tab) |
| 3 | Screen editor (local) | Drop components from mock library, configure props | 3 | 🟢 done — https://arno-ijr.pages.dev/app/screen (opens in a new tab) |
| 4 | Local persistence | IndexedDB — work survives reload | 1 | 🟢 done — auto-saves to arno-local DB |
| 5 | Real DS from connected repo | Library shows real components from MD | 3 | 🟢 done — GitHub Contents API → raw MD → parse (repo <connected-repo>) |
| 6 | Preview mode = live render | iframe with real React render of the connected Button | 4 | 🟢 done — bundle on <connected-DS-owner>.github.io + sandboxed iframe |
| 7 | Interactivity (clickable prototype) 🎯 | Clickable prototype with real components | 2 | 🟢 done — + picker + navigations + history stack |
| — | Spike→steady refactor | Cleanup throwaway, prep prod-stack | 2 | 🟢 done — 3 packages, 18 unit tests, CI, 4 ADRs |
| 8 | Cloud persistence | Neon + Hono + Drizzle, work cloud-backed | 3 | 🟢 done — https://arno-api.vadimpianof.workers.dev (opens in a new tab) |
| 9 | Auth + projects | GitHub OAuth, projects scoped to user | 2 | 🟢 done — backend OAuth + per-user projects (ADR-0006) |
| 10 | Sharing | Share-link, anonymous viewer | 2 | 🟢 done — /share + /share/preview public read-only |
| 11 | Real-time collab | Liveblocks + Yjs, multi-user live | 3 | 🟢 done — workflow in Yjs Storage, multi-tab sync |
| 12 | MD smart editor | Fenced blocks, versions, conflict UI | 5 | 🟢 done — /app/component editor + versions trigger-purged |
| 13 | Sync with repo | Session branches, auto-PR, GitHub App | 4 | |
| 14 | Drift detection + CI | arno-check.yml, 5 statuses in UI | 3 | |
| 15 | Onboarding flow | Full first-run UX | 3 | |
| 16 | Production readiness | Observability, a11y, probes, launch checklist | 5 | |
| 17 | Studio tools → reusable packages | Same tools on /app/library, but imported from @arno/*; one edit propagates everywhere, no copies | 4 | 🟢 done — ADR 0063; @arno/{ponte,tela,punta,layers,dito}, shims removed |
Total ≈ 50 working days (~10 weeks solo dev).
Demo milestones
- End of Phase 2: you can play with the canvas, the UI starts to feel real
- End of Phase 7: 🎯 clickable prototype with components from the connected DS — first WOW
- End of Phase 10: you can share a link with anyone
- End of Phase 11: multi-user real-time
- End of Phase 13: full git-loop with your repos
- End of Phase 16: launch-ready product
Phase 0 — Foundation skeleton
Goal: infrastructure baseline that everything else builds on. No product logic.
Visible result: open https://arno-XXX.pages.dev — see a blank page with "ARNO — coming soon".
Technical scope:
- Monorepo init:
pnpm+ Turborepo + base configs (TS, ESLint, Prettier) apps/web/— Next.js 14 App Router skeleton (single pageHome)tools/eslint-config/,tools/tsconfig/baseline- Cloudflare Pages deploy via
wrangleror git integration - GitHub repository created + first commit + remote push
Dependencies: none.
Throwaway-or-keep: keep — this is steady-state foundation.
Acceptance criteria:
-
pnpm devstarts Next.js locally on :3000 (verified viapnpm buildstatic export —out/index.htmlcontains "ARNO", "Cloud design editor", "Coming soon";pnpm lint/pnpm typecheckclean) - Push to main → auto-deploy to Cloudflare Pages (commit b94d8e9, production deployment success)
- Public URL opens and shows "Hello ARNO" — https://arno-ijr.pages.dev (opens in a new tab)
Implementation notes (2026-05-20):
- Static export (
output: "export") picked as the simplest path for Phase 0. Migration to edge adapter (@cloudflare/next-on-pagesor Workers Static Assets + OpenNext) lands in Phase 8-9 once Auth/API arrive. - Cloudflare flow: the new unified "Workers & Pages" UI creates projects through a Worker-like UI but still requires a Pages-style
wrangler.jsoncwith thepages_build_output_dirfield. Documented indocs/cloudflare-pages.md.
Phase 1 — Static workflow canvas
Goal: see what we are building. Give everyone (including yourself) a sense of the product look-and-feel.
Visible result: on the /app route you see a mock workflow — 5 hardcoded screens (Login, Onboarding, Home, Profile, Settings) connected by arrows. Static SVG/canvas.
Technical scope:
apps/web/app/app/page.tsx— editor route- Install react-flow (or a similar lib) for workflow visualization
- Hardcoded data:
const screens = [ { id: 'login', name: 'Login', position: {x: 0, y: 0} }, { id: 'onboarding', name: 'Onboarding', position: {x: 200, y: 0} }, ... ] const edges = [ { from: 'login', to: 'onboarding' }, ... ] - Sidebar layout (Components / Workflow tabs — static)
- No interaction — visual only for now
Dependencies: Phase 0.
Throwaway-or-keep: mock data throwaway, react-flow integration keep.
Acceptance criteria:
- Open
/app→ see the workflow graph (https://arno-ijr.pages.dev/app/ (opens in a new tab), HTTP 200) - Screens laid out and readable (5 nodes: Login, Onboarding, Home, Profile, Settings; verified in SSR HTML)
- Edges connect screens (4 edges, Login→Onboarding→Home→{Profile, Settings}, animated on the first two)
- Sidebar present with tabs (Components / Workflow, interactive switch)
Implementation note (2026-05-20): lib = @xyflow/react v12.10.2 (react-flow successor, supports Next.js App Router + SSR). Custom screen node, dark theme via CSS vars, dots background, nodesDraggable=false for read-only Phase 1. Bundle: /app 55kB, First Load 142kB (well under 4MB CF budget).
Phase 2 — Editable canvas (local state)
Goal: turn viewing into editing. Play with the canvas, get a feel for the UX.
Visible result: the "+ Screen" button adds a new screen. Drag-and-drop moves screens. Drag from a screen edge → new edge. Right-click → delete. State lives in React; a page reload wipes it.
Technical scope:
- React-flow customization for add/delete/move
- Local React state (
useStateoruseReducer) for screens + edges - UI controls: + screen button, delete, edit name (inline)
- Edge creation by dragging from screen handles
Dependencies: Phase 1.
Throwaway-or-keep: local state — throwaway (replaced in Phase 4). UI controls keep.
Acceptance criteria:
- Create a new screen via the button — "+ Screen" in floating toolbar (top-left)
- Drag screens with the mouse — default xyflow drag
- Draw an edge between screens — source/target Handles (purple dots)
- Delete a screen and its edges (cleanup) — right-click context menu OR Backspace/Delete; onNodesDelete filters related edges
- Rename a screen inline — double-click on title → input → Enter/blur save, Esc cancel
Implementation notes (2026-05-20):
- State:
useNodesState+useEdgesStatefrom @xyflow/react (local React state, throwaway → IndexedDB in Phase 4). - The
onRenamecallback is injected via anodesWithHandlersmemo (xyflow requires immutable data refs). onKeyDownCapture stopPropagationon the rename input is critical, otherwise Backspace during editing deletes the node through ReactFlow'sdeleteKeyCode.ReactFlowProviderwrap — so we can useuseReactFlowinside (needed in Phase 3+).
Phase 3 — Screen editor (local)
Goal: introduce the concept of composition. Drill into a screen, add components, configure props.
Visible result: click a screen in the workflow → drill in → see "screen edit mode". Sidebar with a mock library: Button, Input, BgPlate. Drag-and-drop into screen body. Click a component → right panel with props (label, variant, size). Edit a prop and the visual updates.
Technical scope:
- Routing:
/app/screen/:idfor drill-in - Mock component library:
const COMPONENTS = [ { id: 'button', name: 'Button', props: [ { name: 'label', type: 'string', default: 'Click' }, { name: 'variant', type: 'enum', values: ['primary', 'secondary'] } ]}, { id: 'input', name: 'Input', props: [...] }, { id: 'bgplate', name: 'BgPlate', props: [...], slots: ['header', 'body'] }, ] - Drop zone component → composition tree
- Right panel: dynamic form from props schema
- Render: schematic (labeled boxes), not real React
Dependencies: Phase 2.
Throwaway-or-keep: mock library throwaway (replaced in Phase 5). Drop UX, props panel UX, composition tree model — keep.
Acceptance criteria:
- Drill-in to a screen works — double-click on screen body in
/app→/app/screen?id=<id>&name=<name> - Sidebar mock library shows 3+ components — 4: Button, Input, Title, BgPlate
- Drag a component into the screen body → it appears — HTML5 DnD, mime
application/x-arno-component - Click an instance → right panel with props —
PropsPanelwith dynamic form from schema - Editing a prop reflects on the canvas (labeled box updates) —
useSyncExternalStoresubscribed, instant update - Nested composition: BgPlate → Button inside works —
Slotaccepts drops intochildren
Implementation notes (2026-05-20):
- Route
/app/screen— single static file, screenId+name from query (useSearchParams+Suspenseboundary, required for Next.js static export). Equivalent to an[id]dynamic param without a backend. - State:
composition-store.ts— module-level singleton + React 18useSyncExternalStore. Persists across/app↔/app/screennavigation (same module instance), lost on reload. Phase 4 replaces this with IndexedDB. - Workflow rename UX vs drill-in: double-click on a node title → rename (via
stopPropagationinScreenNode), double-click on node body → drill-in (viaonNodeDoubleClickinReactFlow). - DnD: native HTML5 (
draggable,onDragStart,onDragOver,onDrop). No libraries. - Workflow and composition are still two independent singletons. The workflow store lands in Phase 4 together with IndexedDB.
Phase 4 — Local persistence
Goal: work must not be lost on reload. You can come back to a project.
Visible result: make changes → close the tab → open it → everything is still there.
Technical scope:
- IndexedDB through a library (idb, dexie, or native)
- Auto-save on every change (debounce 500ms)
- Load on mount
- "Reset project" debug button (needed for testing)
Dependencies: Phase 3.
Throwaway-or-keep: IndexedDB — throwaway (replaced by cloud persistence in Phase 8). Auto-save pattern — keep.
Acceptance criteria:
- Changes save automatically —
subscribeon both stores,debounce 500ms→saveWorkflow/saveComposition - Reload → state restores —
PersistenceLoaderloads from IDB on mount, hydrates stores before first render - Closing/opening browser preserves work — IndexedDB
arno-localpersists between sessions (browser-controlled, not sessionStorage) - Reset button clears state — Sidebar footer → confirm →
resetAll()→window.location.reload()
Implementation notes (2026-05-20):
- Lib:
idb@8(~7KB, type-safe IndexedDB wrapper). - IDB schema: DB
arno-localv1, 2 object stores —workflow(keyPathid, single recordid="default"),compositions(keyPathscreenId). - Per-screen save delivery:
compositionStore.subscribeChanges()compares prev/next snapshots and delivers only changedScreenCompositions — avoids writing the entire dataset on a single screen change. - Workflow store extracted from WorkflowCanvas useNodesState/useEdgesState → module singleton
workflow-store.ts(analogous to composition-store). This also fixes an implicit Phase 3 issue: state would not be lost between workflow ↔ screen navigation in theory, but adding IDB load on mount would cause a flash. onRenamecallback (non-serializable) is stripped before IDB write vianodes.map(n => ({...n, data: {label, description}})).- PersistenceLoader renders a loading shell until hydration — no FOUC with initialScreens before user data loads.
Phase 5 — Real DS from connected repo
Goal: replace the mock library with real components from an existing design system. Prove that ARNO works with real repositories.
Visible result: library shows real connected DS components (TitleView, ButtonsGroup, etc.) — with real names, props from their MD specs. Drop into a screen → labeled boxes with real names.
Technical scope:
- connected repo used as a public GitHub repo (or local file fetch for MVP)
- Fetch MD files via the GitHub API public endpoint (no auth for public repos):
https://api.github.com/repos/{owner}/{repo}/contents/{path} - Parse frontmatter (gray-matter library)
- Parse fenced structural blocks v1 (
<!-- arno:props v1 -->) - Component spec extraction: id, name, props, events
- Replace mock COMPONENTS array with real fetched data
- Cache in IndexedDB (offline + skip rate limit)
Dependencies: Phase 4. connected DS Design_system/ MD files must be in the correct format (if not yet — add id to frontmatter, wrap props in fenced v1 markers).
Throwaway-or-keep: fetch logic — partially throwaway (real backend in Phase 8). MD parser — keep, exported to packages/editor/ later.
Pre-work: add at least 3 MD files in the correct format to the connected Design_system:
---
id: cmp-btn-001
name: Button
---
<!-- arno:props v1 -->
- id: p_label
name: label
type: string
textEditable: true
- id: p_variant
name: variant
type: enum
values: [primary, secondary]
<!-- /arno:props v1 -->Acceptance criteria:
- ARNO fetches MD files — via
/design-system/*.md(snapshot, not GitHub API; see notes) - Frontmatter parsed correctly (id, name) —
md-parser.tscustom YAML - Fenced structural blocks parsed (props, types, values) —
<!-- arno:props v1 -->+<!-- arno:slots v1 --> - Library sidebar shows 4+ real components — ButtonDesktop, InputDesktop, TitleView, BackgroundPlate
- Drop connected Button into screen → labeled box with the correct name
- Props panel shows real props from MD (view, size, label, placeholder, type, block, position, heading, subtitle...)
- Cache works (offline mode shows last fetched) — IDB
librarystore, cache-first → network update
Implementation notes (2026-05-20):
- GitHub Contents API + raw URLs — connected DS published as public repo
<connected-repo>(normalized GitHub name from the original Cyrillic "а"). The loader makes 1 request to/repos/.../contents/Design_systemfor listing → fetches MD in parallel viaraw.githubusercontent.com(not counted against the API rate limit). CORS*on both endpoints. A local snapshot atpublic/design-system/existed previously — removed after the switch. - CF Pages edge cache gotcha: removed
public/assets live on the edge for 7 more days (cache-control: public, s-maxage=604800default). The URL/design-system/Buttons.mdreturns 200 until the cache expires, but the ARNO loader does not use it. Purge via CF API if needed. - MD augmentation in the connected repo: 4 files (Buttons, Input, TitleView, BackgroundPlate) got
---id: cmp-..._name: ...---frontmatter + a fenced<!-- arno:props v1 -->block with props from existing "## Props" tables. Human-readable docs remained untouched below. Committed to the connected repo. - Custom YAML parser (md-parser.ts) — no gray-matter (~30KB saved). Supports frontmatter key:value, fenced
- id: ... name: ... values: [...]lists, quoted strings, inline arrays. - Cache-first loading in design-system-loader: instant hydrate from IDB (if present), parallel network fetch → update + persist. Network failure → cache remains valid. Cold start without cache → fallback library (5 mock components from Phase 3).
- Library source badge in the panel footer (
connected design-system/IndexedDB cache/fallback (mock)) — visible signal of what is actually loaded. - Phase 13 will replace the local fetch with the GitHub API (once the connected DS is on GitHub). Architecture is ready:
loadDesignSystemencapsulates the source, the rest of the code goes through theuseLibrary()hook.
Phase 6 — Preview mode = live render
Goal: killer feature. A real React Button with real styles appears in preview. The main wow-effect.
Visible result: in screen edit mode — a "Preview" button. Click → modal/route opens → you see the screen rendered with real components from the connected DS and real CSS. The Button looks like a real button, Input works.
Technical scope:
Pre-work in the connected repo:
- Create
arno.entry.tsxat the connected repo root:// Listens to postMessage events from ARNO iframe // Renders component by id with the passed props window.addEventListener('message', (e) => { if (e.data.type === 'render') { const { component, props } = e.data ReactDOM.render(<Components[component] {...props} />, root) } }) - GitHub Action
.github/workflows/arno-build.yml— builds the bundle, publishes to gh-pages - Enable GitHub Pages on the connected repo (settings)
- Verify bundle accessible:
https://{owner}.github.io/connected DS/arno-bundle.js
In ARNO:
- Preview route
/app/screen/:id/preview - Sandboxed iframe loads render adapter bundle URL
- postMessage protocol: ARNO → iframe
{type: 'render', component, props} - Composition tree → recursive postMessage rendering
- CSP headers for iframe sandbox
Dependencies: Phase 5.
Throwaway-or-keep: keep — this is the core render adapter protocol, used forever.
Acceptance criteria:
- render adapter bundle built and published to GitHub Pages — https://<owner>.github.io/<repo>/arno-bundle.js (726KB raw / 193KB gzip, IIFE, CSS injected via JS)
- ARNO loads the bundle in a sandboxed iframe —
<iframe sandbox="allow-scripts">with inlinesrcdoc(no allow-same-origin → no access to parent cookies/localStorage) - Preview route renders the real connected Button with real styling — ButtonDesktop / InputDesktop from
<connected-DS-npm-package>+ corp.css theme - Editing a prop in ARNO → iframe re-renders —
useEffectsubscribed to the composition store, posts a new tree on every change - Nested composition (BgPlate → Button) renders correctly —
renderNoderecursive, slotchildren - Iframe CSP does not leak —
sandbox="allow-scripts"blocks DOM access to ARNO origin
Implementation notes (2026-05-20):
- Bundle hosting: GitHub Pages with
actions/deploy-pages@v4(build_type: workflow), not a legacy gh-pages branch. Trigger: push tomainwith a paths filter on the bundle source. Enable:gh api repos/<connected-repo>/pages -X POST --inputJSON{"build_type":"workflow"}. - postMessage protocol:
- ARNO → iframe:
{ type: 'arno:render', tree: RenderTreeNode[] } - iframe → ARNO:
{ type: 'arno:ready' }(initial signal after first paint) toRenderTree()strips React callbacks (onRenameetc.) — only plain props/children are sent
- ARNO → iframe:
- Render adapter (arno.entry.tsx): maps
componentId→ React component.ButtonDesktop,InputDesktop— real<connected-DS-npm-package>.TitleView— stub from<connected-DS-npm-package>/typography/title(local TitleView.tsx depends on@local/devpanel, not self-contained).BackgroundPlate— inline div with props styling. - Prop converters:
size(string from enum) →Number()for connected DS components.block(string"true"/"false") → boolean. - Edit/Preview toggle in the top-center of each page — Link-based, preserves id/name in the query.
- Bundle URL hardcoded in
preview-frame.tsx(BUNDLE_URL). Phase 13 will make it per-project via.arno/config.json→bundleHosting.
Phase 7 — Interactivity (clickable prototype) 🎯
Goal: the main demo phase. The prototype works — you can walk between screens by clicking real components.
Visible result: in screen edit mode, click a Button in the composition → "+" icon appears → click → screen picker → pick a target → an edge is created. Preview mode: click that same Button in the live render → navigation to the target screen. A clickable prototype with components from the connected DS is ready.
Technical scope:
- Edge data model:
{from: {screenId, instanceId, eventId}, to: {screenId}, action: 'navigate'} - UI: "+" button on the selected component instance → modal → pick event (onClick) + target screen
- Workflow canvas automatically displays edges (react-flow)
- Preview mode: iframe-bridge injects
onClick={() => arnoNavigate(targetScreenId)}via postMessage arnoNavigate(id)function in preview changes the current screen → re-render with the new composition- "Back" / breadcrumb in preview UI
Dependencies: Phase 6.
Throwaway-or-keep: keep — this is the core interactivity model.
Acceptance criteria:
- "+" UI on a component instance works — badge only on
is-selected, opens the EdgePicker popover - Edge is created with the correct semantics —
{instanceId, eventId: "onClick", action: "navigate"}inWorkflowEdge.data, id =nav-${instanceId}-${eventId} - Edge visible on the workflow canvas — react-flow renders edges from
workflowStore(including nav edges, animated=true) - Preview mode: click a Button → navigate to the next screen — onClick wraps
emitNavigate, postMessage{type:'arno:navigate', toScreenId}→ preview page pushes history - Chain navigation works (Login → Home → Profile) — history stack with no depth limit, breadcrumb shows depth
- Back button in preview —
← Back+⌂ Start(reset to initial) in top-right toolbar; disabled at depth=1
Implementation notes (2026-05-20):
- One instance × one event = one target.
setInteractiveEdgefilters an existing edge with the same anchor and recreates it (replace semantics). Does not accumulate. - Edge ID stable:
nav-${instanceId}-${eventId}— survives workflow re-saves, IDB persists correctly. - Preview history: local React state in
/app/screen/preview/page.tsx, not URL.?id=in the query = initial screen for the back toggle. Browser back NOT wired to the history stack (Phase 8+ may add it once Auth/projects exist). - Click wrapping in the connected repo entry:
ButtonDesktop/InputDesktopreceiveonClickvia a native prop (valid API).Title/BgPlate— wrap with a div,cursor: pointerwhenonActivateis present. - Visual nav indicator:
→ TargetNamebadge always,+badge only when selected,has-navigationCSS class for border accent. - Cross-iframe security: sandbox
allow-scriptsonly (no same-origin), postMessage validated by type discriminator. The iframe cannot trigger navigation to an arbitrary URL — only within the boundaries of what ARNO sends.
Spike → Steady-state refactor
Goal: before going into the production stack — clean up the throwaway, prepare the foundation.
Visible result: nothing new user-facing. The codebase is ready for Phase 8 without legacy.
Technical scope:
- Move the composition model, MD parser, and render adapter into
packages/(extractable):packages/editor/— composition tree, MD parser, fenced blocks v1packages/render-adapter/— postMessage protocolpackages/shared/— domain types, Zod schemas
- IndexedDB — pull out into an interface (prepares the swap for cloud)
- Add testing infra: Vitest, Playwright (skeleton)
- Add CI: lint + type check + test on PR
- Bundle size measurement in CI
- Document accumulated decisions in ADRs
Dependencies: Phase 7.
Acceptance criteria:
- Code organized into a packages structure —
@arno/shared(domain types + immutable mutations),@arno/editor(md-parser + store classes without React),@arno/render-adapter(typed postMessage protocol) - CI pipeline works —
.github/workflows/ci.yml: pnpm install (frozen) → typecheck → lint → test → build → bundle size report. Triggers: push to main + PRs. - No regression in the Phase 7 demo — bundle sizes identical (/app 2.34KB/149KB, /app/screen 2.32KB/154KB, /app/screen/preview 1.85KB/154KB), all Phase 7 acceptance criteria continue to work
- ADRs written for major spike decisions —
docs/adr/0001-0004:- Static export → edge adapter timing (Phase 0 → 8)
- Module stores + useSyncExternalStore (Phase 2 → 7)
- Design-system source: local → GitHub API (Phase 5)
- Render adapter postMessage protocol v1 (Phase 6 → 7)
- Bonus: 18 unit tests passing (md-parser × 7, composition × 7, library-store × 4)
Implementation notes (2026-05-20):
apps/web/src/lib/*.tsare now thin React wrappers: they import classes from@arno/editorand wrap them inuseSyncExternalStore. Each file shrank from ~150 lines to ~20.workflow-store.tsstays in apps/web — it is xyflow-specific (applyNodeChanges/applyEdgeChangesfrom@xyflow/react). Extracting it into packages makes no sense while xyflow is the only canvas backend.preview-frame.tsximportsRenderNode/ARNOIncomingMessagefrom@arno/render-adapter— typed payload, not magic object literals.- IndexedDB interface abstraction deferred to Phase 8 — the swap to cloud happens there, and the switch is cleaner when done at the same time.
- Playwright skeleton deferred to Phase 8 — E2E tests on a static landing with no backend = low value. We will introduce them together with the Auth flow.
- Bundle size measurement in CI is soft for now: it prints chunks > 100KB in the console output. The hard limit (fail build at 4MB) will be added before the Phase 16 launch checklist.
Phase 8 — Cloud persistence
Goal: replace IndexedDB with a real cloud backend. Work accessible from any device.
Visible result: changes save to Postgres. Open ARNO on another device → see your work. (Still a single project shared by everyone — auth lands in Phase 9.)
Technical scope:
- Neon Postgres provisioned (free tier)
- Drizzle schema:
project,screens,composition,edgestables apps/api/— Hono on Cloudflare Workers- tRPC procedures:
workflow.get,workflow.update,screen.composition.get,screen.composition.update - REST API alternative endpoints for simplicity
- Frontend swap IndexedDB → API calls with debounced auto-save
- Optimistic UI updates
Dependencies: spike→steady refactor.
Throwaway-or-keep: keep — production stack.
Pre-work:
- Sign up for Neon free tier
- Sign up for Cloudflare (Workers Paid $5/mo activation)
Acceptance criteria:
- Neon DB provisioned, Drizzle migrations applied —
arnoproject in Frankfurt, 3 tables (project, workflow, screen_composition) - Hono backend deployed to Workers — https://arno-api.vadimpianof.workers.dev (opens in a new tab), Workers Paid
- Workflow + composition persist to Postgres — REST PUT verified end-to-end (GET → PUT → GET with new data)
- Cross-device sync works — single DEFAULT_PROJECT_ID="local" shared; reload pulls state from Postgres instead of IDB
- No data loss on network glitches —
saveWorkflow/saveCompositiongraceful catch + console.warn; PUT idempotent (upsert viaonConflictDoUpdate), safe to retry
Implementation notes (2026-05-21):
- Dedicated Worker, not Pages Functions — see ADR-0005. Pages Functions do not give us Durable Objects / Queues / Cron (needed in Phase 11/13/16). Pay-once architectural decision.
- JSONB columns for
workflow.nodes,workflow.edges,screen_composition.instances— let the tree evolve without migrations. Phase 12 normalizes intocomponent_md_versionsonce diff/conflict resolution per master spec §I.3.2 is needed. - REST instead of tRPC — Phase 8 acceptance is easier to debug via curl. tRPC migration happens with Phase 9 (Auth), once type safety FE↔BE becomes load-bearing.
- Single project ("local") — Phase 9 will replace this with per-user via Auth.js +
project.owner_idFK. - Strip non-serializable before PUT — workflow nodes have a
data.onRenamecallback (runtime-only). The persistence loader does a{ id, type, position, data: { label, description } }projection. - HTTP driver, not Pool —
@neondatabase/serverlessviadrizzle-orm/neon-http. Every request is a separate HTTP call to Neon, no connection pool (works in Workers edge runtime). The Pool driver is used only intools/migrate/. - Idempotent ensure project — every PUT first does
INSERT ... ON CONFLICT DO NOTHINGintoproject, then upserts the specific table. Safe for first-write from any client. - Explicit CORS allowlist —
https://arno-ijr.pages.dev+http://localhost:3000. No*— not even on dev.
Phase 9 — Auth + projects
Goal: real users. Projects scoped to owner.
Visible result: "Sign in with GitHub" button. After sign-in you see your projects. Create a new project. Each user sees only their own projects.
Technical scope:
- GitHub OAuth App created (dev environment)
- Auth.js v5 setup on Next.js (Pages)
- JWT mode session (per master spec §III.2.6)
- Drizzle adapter tables (users, accounts, sessions, verificationTokens)
- Backend Hono middleware validates JWT (versioned secrets)
user,project,project_membertables- "Create project" UI flow
- Projects list page
- Logout
Dependencies: Phase 8.
Throwaway-or-keep: keep.
Week-1 prototyping validation: this is where Auth.js Edge Runtime compatibility is critical (per master spec §VI). If it breaks, pivot to Lucia.
Acceptance criteria:
- Sign-in with GitHub works —
/auth/github/login302 → github.com/login/oauth/authorize with correct client_id/scope/state; verified in prod - User session persists across reload — JWT in localStorage, AuthGate hydrates on mount, /auth/me returns user
- Projects list shows my projects —
GET /api/v1/me/projectsscoped byWHERE owner_id = userId - Create project flow works —
POST /api/v1/projectswith auto-generated id (prj-<rand8>), owner_id = JWT.sub - Logout clears session —
POST /auth/logoutrevokes jti in KV TTL=remaining_exp, frontend clears localStorage - Backend rejects requests without a valid JWT —
requireAuthmiddleware on/api/v1/*; manually verified401 unauthorized - Token revocation (logout) works via KV —
revoked:{jti}lookup inverifyJwt; expired naturally via KV TTL
Implementation notes (2026-05-21):
- Deviation from §III.2.6 → ADR-0006. Auth.js on static export is not possible (no API routes). Decision: OAuth lives entirely in Workers (apps/api), Bearer JWT in the Authorization header. Static export Pages stays; ADR-0001/0005 are not broken.
- JWT lib:
jose(works in the Workers runtime, no Node deps). - JWT algorithm: HS256 per master spec §0.5. JWT_SECRET = 256-bit random hex.
- Token storage: localStorage (cross-origin pages.dev ↔ workers.dev → httpOnly cookie requires a shared parent domain, post-MVP).
- Token return: OAuth callback redirects to
frontend_origin/app#token=.... URL fragment does not appear in Referer headers or server logs (RFC 6749 §1.3.2 recommendation for implicit grant style). - CSRF protection: anti-CSRF state nonce stored in KV with TTL 10min, consumed atomically on callback (
get+delete). - DB schema: new
usertable (UNIQUE(provider, provider_user_id)) +project.owner_idFK ON DELETE CASCADE. Migration0001_rapid_infant_terrible.sqlapplied. - Ownership check:
requireOwnedProject()helper on every /api/v1/workflow and /api/v1/compositions endpoint — 404 if it does not exist, 403 if another owner. Safe. - Frontend route restructure:
/appis now the projects list (was the workflow),/app/workflow?project=<id>is the workflow editor. Screen routes accept&project=<id>in the query. - Project switching:
PersistenceLoadertakesprojectIdas a prop and resets stores on change (avoids cross-project state leaks). - GitHub account rename: <connected-DS-owner> → vadimpianov. All URLs updated: design-system-loader (Contents API), preview-frame (gh-pages bundle), git remotes of both repos. GitHub raw redirects work, but gh-pages does not, so the linter was correct to update them.
Week-1 prototyping validation (master spec §VI #1):
- We did not test Auth.js v5 Edge — we chose a more resilient backend-driven path (ADR-0006). Master spec §VI #1 assumed we would validate Auth.js or fall back to Lucia; we took a third path (custom Hono OAuth), which is architecturally equivalent to the "Lucia fallback".
- JWT + Edge runtime + Drizzle adapter pattern works — that is what is being validated. JWT.verify works in Workers, Drizzle Neon HTTP works (Phase 8 already).
Phase 10 — Sharing
Goal: show the product to a customer or stakeholder without requiring sign-up.
Visible result: in project settings — "Generate share link". You get a URL arno.app/share/XXX. Open it in incognito → see the project in read-only mode. You can walk through the prototype.
Technical scope:
project_share_linktable (id, project_id, token, scope, created_by, revoked_at)- "Generate share link" UI in project settings
- Public route
/share/[token]— no auth required - Server-side fetch project snapshot (no Liveblocks!) — per §III.10
- Read-only render: same canvas + screen views, edit controls disabled
- Preview mode works (kicks into prototype walk)
- Revoke link UI
Dependencies: Phase 9.
Throwaway-or-keep: keep.
Acceptance criteria:
- Generate share link creates a unique token — base64url 24 bytes (~32 chars), UNIQUE constraint in DB
- Share URL opens without sign-in —
/share?token=...not wrapped in AuthGate;/api/v1/public/share/*skips Bearer middleware - Viewer mode read-only —
ViewModeContext + xyflow flags(nodesDraggable/Connectable/Selectable = false, no toolbar add, no context menu, deleteKeyCode = []) - Preview mode works for viewer —
/share/preview?token=&id=&name=with PreviewFrame + chain navigation history stack (like Phase 7) - Revoke link — old URL → 404 —
revokedAttimestamp +isNullfilter in public endpoint - No Liveblocks connection from viewer — Phase 11 is not done yet; server snapshot only per master spec §I.3.10
Implementation notes (2026-05-21):
- Public endpoint isolation:
/api/v1/public/*bypassesrequireAuthmiddleware via a prefix check inside the middleware. Cleaner than splitting into two mount points. - Token format: base64url 24 random bytes — URL-safe, no pattern leak (24 bytes = ~192 bits entropy, well beyond brute-force feasibility).
- Snapshot endpoint aggregates in one response: project meta + workflow + compositions[]. One HTTP roundtrip for the viewer. Phase 11 (Liveblocks) keeps this endpoint as a viewer fallback per §I.3.10.
- ViewModeContext instead of prop drilling —
useViewMode()is available at any depth. Currently used only in WorkflowCanvas; Phase 11+ may extend it to the composition canvas / props panel (if a viewer drill-in wants to see props without edit). - Drill-in routing: WorkflowCanvas reads
?tokenfrom the URL — if readonly+token, double-click →/share/preview(public route, hits the public API). Auth-required/app/screendoes not leak into the share flow. - DB cascade:
project_share_link.project_id → project.id ON DELETE CASCADE. Deleting a project automatically clears all share links. - Phase 10 NOT shipped: scope='screen' (single-screen subgraph share) — parked in §V. Phase 10 ships scope='project' only. Schema is ready for the extension (scope_screen_id column nullable).
- Phase 10 NOT shipped: require_login flag (privacy gradient) — parked. Phase 10 = fully public.
- Phase 10 NOT shipped: expiry — parked. Revoke = manual only.
Phase 11 — Real-time collab
Goal: multi-user editing demo. One user's changes visible to the others in real time.
Visible result: you and a colleague open the same project in different browsers / devices. You see their cursor. When they move a screen, you see the move in real time. Two people can edit different screens in parallel.
Technical scope:
- Liveblocks account created (free tier)
- Y.Doc setup for workflow state (
screens,edges) - Liveblocks Yjs provider integration
- Migrate workflow state from REST to Yjs CRDT
- Awareness API for presence (cursors, current screen)
- Sync REST → Yjs (existing data migration)
- Conflict-free editing demo
Dependencies: Phase 10.
Throwaway-or-keep: keep. Major architecture shift — workflow data now lives in Liveblocks Storage, not Postgres.
Validation: prototype Liveblocks Yjs Storage REST API access (master spec §VI #3). Backup strategy is initially manual export.
Acceptance criteria:
- Liveblocks integrated, workflow in Yjs Storage —
LiveblocksYjsProviderconnects to the Y.Doc; doc.getMap("nodes")+doc.getMap("edges") - Two browsers → see the same workflow, both can edit —
RoomProvider id=project:<projectId>with server-auth (Bearer JWT + ownership check) - [~] Cursors visible (presence) — userInfo is forwarded through the session (name, avatar, login), but the visible cursor layer on the canvas is deferred to Phase 11.1 (requires a custom React-Flow overlay + awareness state binding)
- Mutations propagate <1s — Yjs Liveblocks WebSocket transport; verified in production multi-tab demo
- No conflicts even on simultaneous edits — Yjs CRDT guarantees (Y.Map last-write-wins per key, transactions atomic)
- Existing REST workflows migrated to Yjs —
seedFromSnapshot()loads Postgres data → Y.Doc only if the doc is empty (idempotent, does not overwrite collab state)
Implementation notes (2026-05-21):
- Dual-write to Postgres mirror. Workflow primary in Yjs, but every mutation triggers a Postgres save through a debounced (500ms) workflowStore subscriber. This is needed for:
/shareviewer endpoint (master spec §I.3.10: viewer does NOT connect to Liveblocks → reads server snapshot)- Disaster recovery (Liveblocks backup retention 90d per master spec §0.4)
- Phase 16 invariant probes (Yjs ↔ git invariant — server snapshot for compare)
- Y.Map records — plain JS objects. Not nested Y.Maps. Atomic update via
map.set(id, newRecord). Field-level merge is not needed for screens (coarse-grained units). Phase 12 for compositions may revisit this decision. - Composition stays on REST. Master spec §I.3.2 explicit: "MD-edit live state — our DB, not Yjs". Phase 12 MD smart editor will stay on REST + versions, Yjs only for the workflow.
@liveblocks/reactv3 API:authEndpoint+publicApiKeymutually exclusive. We use authEndpoint (server-side JWT verification) — secure flow. The public key is not needed in the bundle (NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEYenv var removed from requirements).- Bundle impact: /app/workflow 158KB → 258KB First Load (+100KB Liveblocks+Yjs). Acceptable per master spec 4MB Workers budget.
- Awareness cursors deferred to Phase 11.1 — server-side userInfo (name/avatar/login) is already forwarded in the session, but a visible canvas overlay requires a custom react-flow layer + awareness binding (~1 day). The multi-tab edit demo works without cursors.
Phase 12 — MD smart editor
Goal: edit MD specs directly in ARNO. Smart UI for fenced structural blocks.
Visible result: in the Components tab — click a component → MD editor opens. You see markdown text. Fenced blocks (<!-- arno:props v1 -->) render as a structured form with inputs for props. Edit → auto-save. Conflict UI on collision.
Technical scope:
- MD editor component (textarea + parsed structural overlay)
- Fenced blocks v1 parser → structured form (inputs by prop type)
- REST API: save MD content (server validates frontmatter)
component_md_versionstable + Postgres trigger purge OFFSET 20- Conflict detection (session_id + base_version logic)
- Conflict UI with user choice (view diff / save mine / discard mine / merge manually)
- Real-time awareness via Liveblocks broadcast (
md_savedevent) - Multi-tab coordination via BroadcastChannel API
Dependencies: Phase 11.
Throwaway-or-keep: keep.
Acceptance criteria:
- Click on component → MD editor opens — "edit md" link on hover of the library card →
/app/component?project=&path= - Fenced blocks rendered as a structured form — sidebar structural overlay shows frontmatter ✓ + props/slots line count (simplified form for Phase 12; full prop-by-prop UI deferred to Phase 16)
- Edit fenced field → markdown updates → auto-saves — textarea + debounced 800ms PUT
- Edit prose → markdown saves verbatim — no client-side parsing, raw markdown in textarea
- [~] Open same MD in two tabs → BroadcastChannel coordinates — deferred to Phase 16, master spec §I.3.2 calls for it but it is not critical for MVP (conflict UI handles the cross-tab case via server-side 409)
- Two users save the same MD simultaneously → conflict UI shows — server returns 409 + currentVersion/currentContent, UI shows "Use remote" / "Override mine"
- Versions list shows last 20 — Postgres trigger
purge_old_md_versions()AFTER INSERT keeps OFFSET 20
Implementation notes (2026-05-21):
- Postgres trigger for purge — exactly per master spec §I.3.2. PL/pgSQL function appended manually to the Drizzle-generated migration (
CREATE OR REPLACE FUNCTION+CREATE TRIGGER). Drizzle does not generate triggers automatically, so this is a hand-written tail in the migration file. - SHA-256 via
crypto.subtle.digest("SHA-256", ...)— Web Crypto API native in Workers. Used for the content fingerprint incomponent_md_raw.content_shaandcomponent_md_versions.content_sha. - Session ID generated through
sessionStorage(per tab/window) — Phase 16 will extend this to same-session fast-forward (master spec §I.3.2: "if conflict.author == request.user AND conflict.session_id == request.session_id → auto-rebase silent"). - Simplified conflict UI — 2 buttons (use remote / override). Full diff view + 3-way merge → Phase 16. Master spec §V already records "ARNO-side 3-way merge UI" as a parked item.
- Simplified structural overlay — sidebar shows counts (
frontmatter ✓,N props,N slots), not a prop-by-prop form. The full structured editor (input per prop, dropdown per enum, textEditable badge) lands in Phase 16 polish after the launch checklist. - GitHub raw fetch for initial content — the client hits raw.githubusercontent.com itself if the ARNO DB is empty. After the first save the DB becomes the source of truth. Phase 13 (sync with repo) will push back to GitHub.
- Phase 12 NOT shipped (deferred):
- BroadcastChannel multi-tab editor ownership coordination
- Liveblocks broadcast
md_savedevent for cross-tab realtime (master spec §I.2.2) - Full diff/3-way merge conflict UI
- Per-prop structured form (prop-name input, enum dropdown, textEditable badge)
- Version restore / label UI All these features are in master spec §I.3.2 but not critical for MVP scope. They return in Phase 16 polish.
Phase 13 — Sync with repo (write-back)
Goal: MD changes made in ARNO land in the real repository through a git workflow (PR).
Visible result: the maker edits an MD spec in ARNO → clicks "Submit changes" → sees a PR appear on GitHub in the connected repo. The PR contains the modified MD files. After merge ARNO updates (webhook).
Technical scope:
- GitHub App created (ARNO GitHub App with required permissions per §I.3.8)
- App installation flow in ARNO settings
- Session-branch model:
arno/{user-handle}branch - Debounced push (~30s idle) to session-branch
- Auto-PR creation on first push
- Pre-push HEAD check + Redis SETNX mutex (§I.3.3)
- Webhook handler for push events (dedup + fan-out)
- Sync component_md_raw on webhook
- Token refresh middleware (KV lock)
Dependencies: Phase 12. Repo connected to project (for MVP — manual config in DB, full onboarding in Phase 15).
Throwaway-or-keep: keep.
Acceptance criteria:
- GitHub App installed on test repo
- Edit MD in ARNO → 30s later → commit on session-branch
- First push creates PR
- Subsequent pushes update the existing PR
- HEAD divergence (IDE push) → no auto-push, UI prompt
- Webhook on main merge → updates
component_md_raw - Concurrent push attempts blocked by mutex
Phase 14 — Drift detection + CI
Goal: automatic check that MD specs match the TSX code. Visible signal about issues.
Visible result: a comment from the ARNO check action appears on a repo PR: "Drift detected: Button.md says prop label, TSX uses lable". In the ARNO UI the Button component gets a yellow/red drift indicator. The 5 component statuses work.
Technical scope:
arno-check.ymlGitHub Action templatereact-docgen-typescriptparses TSX → extracts props- Compare with MD frontmatter
props:section (by id) - Output: PR comment + check_run status
- ARNO UI reads via GitHub API (check_run status)
- 5 component statuses: green / red / yellow / purple / gray
- Per-component opt-out via MD frontmatter
driftCheck: false
Dependencies: Phase 13.
Throwaway-or-keep: keep.
Acceptance criteria:
- arno-check.yml runs on PR in the test repo
- Drift correctly detected when MD ↔ TSX diverge
- No false positives on consistent components
- ARNO UI shows component status badges
- Opt-out (
driftCheck: false) works - TSX unparseable → purple status (not red)
Phase 15 — Onboarding flow
Goal: a new user can go from sign-up to a working project without manual setup.
Two paths:
- Big-biz path (this Phase) — GitHub App + existing repo flow (described below)
- Small-biz path — URL-import onboarding (master spec v1.3 unparked §V). The user provides a URL → staging area (no git required). See docs/url_import_spec.md. Parallel feature track, can develop independently. ADRs 0007-0018.
Visible result: a new user signs up → the wizard walks them through: install GitHub App → select repo → configure paths → confirm → PR created → user merges → initial scan runs → edit mode unlocks → bundle CI runs → preview unlocks. All steps have progress indicators.
Technical scope:
- Onboarding wizard UI (multi-step, state persisted to
onboarding_sessionper attempt) - GitHub App installation check + redirect flow
- Repo selection UI (list installations + repos)
- Path configuration (autodetect + override)
- Bundle hosting choice (gh-pages public / github-packages private)
arno initlogic — generates PR with config + workflows + entry- Progress tracking: poll PR merge status
- After merge: trigger initial GraphQL bulk scan
- Bundle CI status polling
- Email reminders (Day 1, 7, 30) for unfinished onboarding
- Project lifecycle states (pending_setup → active → archived)
Dependencies: Phase 14. Resend email account.
Throwaway-or-keep: keep.
Acceptance criteria:
- Brand new user signs up → onboarding wizard launches
- GitHub App installation flow seamless
- Init PR created with correct config + workflows
- After PR merge: initial scan completes <30s
- Edit mode unlocked before bundle CI done
- Bundle CI status visible in UI
- Preview unlocks once bundle is ready
- Abandon wizard mid-way → resume from last step
- Email reminder sent on Day 1 if PR not merged
Phase 16 — Production readiness
Goal: product-ready for public launch. Observability, accessibility, monitoring, disaster recovery.
Visible result: running in production. Sentry catches errors. Grafana dashboards show metrics. axe-core CI gates passing. Invariant probes running. Status page operational. Disaster recovery playbook tested.
Technical scope:
Observability (master spec Part II):
- OpenTelemetry SDK instrumentation (HTTP, DB, Redis, external API wrappers)
- Direct OTLP push to Grafana Cloud (Loki + Mimir + Tempo)
- Sentry SDK setup (frontend lazy-load, backend middleware)
- PII scrubbing
- Release tagging (
<service>@<git_sha[:8]>) - Healthcheck endpoints (
/health,/ready,/metrics) - Healthchecks.io dead-man-switch + cron heartbeats
- Invariant probes (fast hourly + deep weekly)
- Tiered alerting (PAGE / NOTIFY / TICKET) with runbooks
Accessibility (master spec §I.4):
- Keyboard navigation for the workflow canvas (Tab, arrows, Enter, Esc)
- ARIA labels on all interactive controls
- Focus visible indicators
- Color contrast audit (≥4.5:1)
- axe-core in Playwright CI
- Lighthouse Accessibility score >90 (CI gate)
- Screen reader testing (NVDA + VoiceOver)
SPOF mitigation (master spec §III.2.9):
- Multi-owner Cloudflare account configured
- Domain via Porkbun (separate registrar)
- DNS TTL 300s on critical records
- Secrets backed up in 2 password managers
- Disaster recovery playbook (
docs/runbooks/cloudflare_account_loss.md)
Backup strategy:
- Neon PITR 7 days (automatic)
- Monthly DB snapshot to R2
- Weekly Yjs Storage backup to R2 (cron)
- Recovery scripts documented
Load testing:
- k6 scenarios for realistic usage
- Pre-launch load test (100K simulated users)
- Performance budget enforcement (Lighthouse)
Status page:
- Manual MVP (README or simple Cloudflare Pages page)
- Update procedure documented
Compliance:
- ToS + Privacy Policy published
- DPAs signed with vendors
- GDPR data export/delete endpoints
Launch readiness checklist (master spec §VII.2):
- All items checked
- First alpha customer onboarded successfully
Dependencies: Phase 15.
Throwaway-or-keep: keep.
Acceptance criteria:
- Sentry errors visible in dashboard
- Grafana dashboards show latency / error rate / queue depth
- axe-core CI passing on all pages
- Lighthouse Accessibility >90
- Invariant probes running, no false positives
- Disaster recovery dry-run completed
- Load test (10K users simulated) — no SLO violations
- All §VII.2 launch readiness checklist items ✅
Transition gates
After each phase:
- Demo — open it, show the visible result
- Acceptance check — all criteria met
- Sign-off (4 axes per CLAUDE.md): compact / documented / observable / testable
- Commit with a descriptive message
- Next phase brief — update scope if anything changed
Throwaway ↔ Production timeline
| Phase | Code quality target |
|---|---|
| 0 | Production foundation (keep) |
| 1-2 | Spike — fast & loose |
| 3-4 | Spike — patterns emerge, throwaway accepted |
| 5-7 | Spike to demo killer feature |
| Refactor | Cleanup, extract to packages |
| 8+ | Production grade, per master spec |
Phase 17 — Studio tools → reusable packages
Goal: make the four Studio tools (dito, tela, layers, punta) reusable across any part of ARNO with a single source of truth — one edit to a tool propagates to every consumer on rebuild, no copies to sync. Canonical decision: ADR 0063.
Visible result: /app/library (Sorgente tokens view) renders and drives all four tools exactly as before, but every tool is now imported from its own @arno/<tool> package instead of apps/web/src/dev-tools/studio/<tool>/. A change made in packages/dito/src/** shows up wherever @arno/dito is imported, with no local copy.
Technical scope (extraction order forced by the dependency graph — see ADR 0063):
@arno/ponte— extract the shared kernel first:studio-bus,studio-events(transport injected viaconfigureStudioEvents),types,tree-ops,adaptive/primitives,render/even-size. All four tools bind to it. Preservestudio-bussingle-instance semantics (one bus per app — bundler must dedupe the package). Note (impl refinement):useStudioTreestays app-side — its only consumer is the canvas and it binds persistence (sorgente-draft,api-client); it reaches the kernel via@arno/ponte. The shared pure cross-tool utilsingle-child(mergedLeafChild, used by punta/dito/layers) was promoted into ponte to break the punta↔layers cycle, leaving an acyclic tool DAGponte ← punta ← {layers, dito}.@arno/tela— autonomous tool; validates the package template. The size applier inapps/web/src/components/sorgente-library-catalog.tsxtakes a@arno/teladep.@arno/punta— before its consumers; exposeslayer-type,node-bridge,PuntaIcons.@arno/layers— before dito (dito runtime-depends on it). Name is chord-debt per ADR 0063 (rename toordito/stratiin a separate ADR + PR).@arno/dito— last; depends on@arno/punta+@arno/layers.
Each package: name @arno/<tool>, version 0.0.0, private, type module, peerDependencies react/react-dom ^18, barrel src/index.ts, @arno/ponte: workspace:*, devDeps @arno/eslint-config/@arno/tsconfig/vitest — template from packages/foundation. pnpm-workspace.yaml already globs packages/*.
Dependencies: Studio tools exist and work at /app/library (feat/sorg-layout).
Throwaway-or-keep: keep — steady-state architecture.
Acceptance criteria (per extraction step + final):
-
@arno/ponteextracted;single-childpromoted;pnpm --filter @arno/ponte typecheck+ 179 tests green. -
@arno/telaextracted; canvas size applier imports from@arno/tela; typecheck + 42 tests green. -
@arno/puntaextracted; dito + layers import punta internals from@arno/punta; typecheck + 233 tests green. -
@arno/layersextracted; typecheck + 35 tests green. -
@arno/ditoextracted; hover-bus edge (source:"layers"+[data-layer-id]) kept as runtime coupling, not an import; typecheck + 5 tests green. - Kernel shims removed; app-side Studio code imports
@arno/pontedirectly; telemetry DI relocated tostudio/configure-ponte.ts. - Static regression gate each step:
@arno/webtypecheck + unit tests green (101 web + 494 package = 595 total); pre-commit monorepo typecheck + lint green. - Browser regression on the stand: drove
/app/library(Sorgente Tokens) ontest-sorg-layout.arnomake.comunder auth (2026-07-12). All four tools work across package boundaries: layers-row pick → Punta inspector populates (layers→ponte bus→punta); Tela375×667preset → canvas device frame (tela→catalog applier); Punta RADIUS instant-write → box rounds on canvas (punta→bus→renderer); Dito hover panel appears on layer-row hover (dito subscribes ponte hover-bussource:"layers"). Zero console errors on load + after interaction.studio-bussingleton deduped correctly. Test edit reverted. - Proof of reuse (pending): import a tool from a second site and confirm a source edit propagates without a copy. Capability is ready (packages import via
workspace:*); no second consumer added yet.
Implemented (2026-07-12): commits 2fb922d (ponte) · 71133be (tela) · c034108 (punta + single-child) · 3120f10 (layers, scaffold repaired in 06d3763) · 83ff95a (dito) · 06d3763 (layers-scaffold repair + shim removal). All on feat/sorg-layout. Note: intermediate commits 3120f10/83ff95a are internally incomplete (a concurrent session sharing the worktree disturbed the index; layers scaffold landed in 06d3763) — harmless under squash-merge; HEAD is complete and green.
Open items (parking):
@arno/layerschord rename (ordito/strati) — separate ADR + PR (chord-debt per ADR 0063).- Browser regression + proof-of-reuse (the two unchecked criteria above).
Parallel work opportunities (if there is a team)
- Phase 6 + Phase 7 (render + interactivity) — frontend dev
- Phase 8 + Phase 9 (backend + auth) — backend dev
- Phase 12 + 13 (MD editor + sync) — full-stack pair
- Phase 16 (production readiness) — DevOps + a11y in parallel
Open items per phase (parking)
- Phase 6: render-adapter security review (post-MVP pen test)
- Phase 11: Yjs backup mechanism verification (master spec §VI #3)
- Phase 13: GitHub App permission scope minimization
- Phase 15: empty repo case (ARNO Studio parking — small-biz path)
- Phase 16: OTel Collector deployment (when traces >40GB/mo)
Status tracking
Every phase has an explicit status:
- 🔵 Not started — backlog
- 🟡 In progress — active
- 🟢 Done — acceptance ✅, demo ✅, committed
- 🔴 Blocked — dependency issue, parking trigger, etc.
Update this file as progress happens. Master spec (_index.md) is for architecture; this file is for execution sequence.