Implementation workflow

Goal: sequence of ARNO development phases where every iteration produces a visible artifact — something you can open, interact with, demo.

Principles

  1. Vertical slicing > horizontal layers. Each phase = full-stack vertical through the current feature.
  2. Demo-driven. Each phase ends with something you can open in a browser and show.
  3. Throwaway acceptable in spike phases (1-7). Mocks → real services incrementally.
  4. Cumulative. Each phase builds on the previous one.
  5. Connected design-system as first integration target. Real MD + TSX already exist in Desktop/Claude/<connected-repo>/. Dogfood from day one.
  6. Master spec (_index.md) — canonical reference for tech decisions. The workflow here is the sequencing; it does not duplicate architectural decisions.

Phase overview

#PhaseVisible resultDaysStatus
0Foundation skeletonBlank Next.js on *.pages.dev1🟢 done — https://arno-ijr.pages.dev (opens in a new tab)
1Static workflow canvasMock graph of 5 screens with edges2🟢 done — https://arno-ijr.pages.dev/app (opens in a new tab)
2Editable canvas (local)Create/move screens and edges2🟢 done — https://arno-ijr.pages.dev/app (opens in a new tab)
3Screen editor (local)Drop components from mock library, configure props3🟢 done — https://arno-ijr.pages.dev/app/screen (opens in a new tab)
4Local persistenceIndexedDB — work survives reload1🟢 done — auto-saves to arno-local DB
5Real DS from connected repoLibrary shows real components from MD3🟢 done — GitHub Contents API → raw MD → parse (repo <connected-repo>)
6Preview mode = live renderiframe with real React render of the connected Button4🟢 done — bundle on <connected-DS-owner>.github.io + sandboxed iframe
7Interactivity (clickable prototype) 🎯Clickable prototype with real components2🟢 done — + picker + navigations + history stack
Spike→steady refactorCleanup throwaway, prep prod-stack2🟢 done — 3 packages, 18 unit tests, CI, 4 ADRs
8Cloud persistenceNeon + Hono + Drizzle, work cloud-backed3🟢 done — https://arno-api.vadimpianof.workers.dev (opens in a new tab)
9Auth + projectsGitHub OAuth, projects scoped to user2🟢 done — backend OAuth + per-user projects (ADR-0006)
10SharingShare-link, anonymous viewer2🟢 done — /share + /share/preview public read-only
11Real-time collabLiveblocks + Yjs, multi-user live3🟢 done — workflow in Yjs Storage, multi-tab sync
12MD smart editorFenced blocks, versions, conflict UI5🟢 done — /app/component editor + versions trigger-purged
13Sync with repoSession branches, auto-PR, GitHub App4
14Drift detection + CIarno-check.yml, 5 statuses in UI3
15Onboarding flowFull first-run UX3
16Production readinessObservability, a11y, probes, launch checklist5
17Studio tools → reusable packagesSame tools on /app/library, but imported from @arno/*; one edit propagates everywhere, no copies4🟢 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 wrangler or git integration
  • GitHub repository created + first commit + remote push

Dependencies: none.

Throwaway-or-keep: keep — this is steady-state foundation.

Acceptance criteria:

  • pnpm dev starts Next.js locally on :3000 (verified via pnpm build static export — out/index.html contains "ARNO", "Cloud design editor", "Coming soon"; pnpm lint / pnpm typecheck clean)
  • 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-pages or 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.jsonc with the pages_build_output_dir field. Documented in docs/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 (useState or useReducer) 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 + useEdgesState from @xyflow/react (local React state, throwaway → IndexedDB in Phase 4).
  • The onRename callback is injected via a nodesWithHandlers memo (xyflow requires immutable data refs).
  • onKeyDownCapture stopPropagation on the rename input is critical, otherwise Backspace during editing deletes the node through ReactFlow's deleteKeyCode.
  • ReactFlowProvider wrap — so we can use useReactFlow inside (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/:id for 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 — PropsPanel with dynamic form from schema
  • Editing a prop reflects on the canvas (labeled box updates) — useSyncExternalStore subscribed, instant update
  • Nested composition: BgPlate → Button inside works — Slot accepts drops into children

Implementation notes (2026-05-20):

  • Route /app/screen — single static file, screenId+name from query (useSearchParams + Suspense boundary, required for Next.js static export). Equivalent to an [id] dynamic param without a backend.
  • State: composition-store.ts — module-level singleton + React 18 useSyncExternalStore. Persists across /app/app/screen navigation (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 stopPropagation in ScreenNode), double-click on node body → drill-in (via onNodeDoubleClick in ReactFlow).
  • 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 — subscribe on both stores, debounce 500mssaveWorkflow / saveComposition
  • Reload → state restores — PersistenceLoader loads from IDB on mount, hydrates stores before first render
  • Closing/opening browser preserves work — IndexedDB arno-local persists 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-local v1, 2 object stores — workflow (keyPath id, single record id="default"), compositions (keyPath screenId).
  • Per-screen save delivery: compositionStore.subscribeChanges() compares prev/next snapshots and delivers only changed ScreenCompositions — 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.
  • onRename callback (non-serializable) is stripped before IDB write via nodes.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.ts custom 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 library store, 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_system for listing → fetches MD in parallel via raw.githubusercontent.com (not counted against the API rate limit). CORS * on both endpoints. A local snapshot at public/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=604800 default). The URL /design-system/Buttons.md returns 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: loadDesignSystem encapsulates the source, the rest of the code goes through the useLibrary() 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:

  1. Create arno.entry.tsx at 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)
      }
    })
  2. GitHub Action .github/workflows/arno-build.yml — builds the bundle, publishes to gh-pages
  3. Enable GitHub Pages on the connected repo (settings)
  4. 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 inline srcdoc (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 — useEffect subscribed to the composition store, posts a new tree on every change
  • Nested composition (BgPlate → Button) renders correctly — renderNode recursive, slot children
  • 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 to main with a paths filter on the bundle source. Enable: gh api repos/<connected-repo>/pages -X POST --input JSON {"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 (onRename etc.) — only plain props/children are sent
  • 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.jsonbundleHosting.

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"} in WorkflowEdge.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. setInteractiveEdge filters 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/InputDesktop receive onClick via a native prop (valid API). Title/BgPlate — wrap with a div, cursor: pointer when onActivate is present.
  • Visual nav indicator: → TargetName badge always, + badge only when selected, has-navigation CSS class for border accent.
  • Cross-iframe security: sandbox allow-scripts only (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 v1
    • packages/render-adapter/ — postMessage protocol
    • packages/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:
    1. Static export → edge adapter timing (Phase 0 → 8)
    2. Module stores + useSyncExternalStore (Phase 2 → 7)
    3. Design-system source: local → GitHub API (Phase 5)
    4. 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/*.ts are now thin React wrappers: they import classes from @arno/editor and wrap them in useSyncExternalStore. Each file shrank from ~150 lines to ~20.
  • workflow-store.ts stays in apps/web — it is xyflow-specific (applyNodeChanges/applyEdgeChanges from @xyflow/react). Extracting it into packages makes no sense while xyflow is the only canvas backend.
  • preview-frame.tsx imports RenderNode / ARNOIncomingMessage from @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, edges tables
  • 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 — arno project 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 / saveComposition graceful catch + console.warn; PUT idempotent (upsert via onConflictDoUpdate), 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 into component_md_versions once 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_id FK.
  • Strip non-serializable before PUT — workflow nodes have a data.onRename callback (runtime-only). The persistence loader does a { id, type, position, data: { label, description } } projection.
  • HTTP driver, not Pool@neondatabase/serverless via drizzle-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 in tools/migrate/.
  • Idempotent ensure project — every PUT first does INSERT ... ON CONFLICT DO NOTHING into project, then upserts the specific table. Safe for first-write from any client.
  • Explicit CORS allowlisthttps://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_member tables
  • "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/login 302 → 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/projects scoped by WHERE owner_id = userId
  • Create project flow works — POST /api/v1/projects with auto-generated id (prj-<rand8>), owner_id = JWT.sub
  • Logout clears session — POST /auth/logout revokes jti in KV TTL=remaining_exp, frontend clears localStorage
  • Backend rejects requests without a valid JWT — requireAuth middleware on /api/v1/*; manually verified 401 unauthorized
  • Token revocation (logout) works via KV — revoked:{jti} lookup in verifyJwt; 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 user table (UNIQUE(provider, provider_user_id)) + project.owner_id FK ON DELETE CASCADE. Migration 0001_rapid_infant_terrible.sql applied.
  • 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: /app is 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: PersistenceLoader takes projectId as 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_link table (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 — revokedAt timestamp + isNull filter 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/* bypasses requireAuth middleware 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 ?token from the URL — if readonly+token, double-click → /share/preview (public route, hits the public API). Auth-required /app/screen does 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 — LiveblocksYjsProvider connects 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:
    • /share viewer 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/react v3 API: authEndpoint + publicApiKey mutually exclusive. We use authEndpoint (server-side JWT verification) — secure flow. The public key is not needed in the bundle (NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY env 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_versions table + 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_saved event)
  • 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 in component_md_raw.content_sha and component_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_saved event 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.yml GitHub Action template
  • react-docgen-typescript parses 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 pathURL-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_session per 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 init logic — 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:

  1. Demo — open it, show the visible result
  2. Acceptance check — all criteria met
  3. Sign-off (4 axes per CLAUDE.md): compact / documented / observable / testable
  4. Commit with a descriptive message
  5. Next phase brief — update scope if anything changed

Throwaway ↔ Production timeline

PhaseCode quality target
0Production foundation (keep)
1-2Spike — fast & loose
3-4Spike — patterns emerge, throwaway accepted
5-7Spike to demo killer feature
RefactorCleanup, 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):

  1. @arno/ponte — extract the shared kernel first: studio-bus, studio-events (transport injected via configureStudioEvents), types, tree-ops, adaptive/primitives, render/even-size. All four tools bind to it. Preserve studio-bus single-instance semantics (one bus per app — bundler must dedupe the package). Note (impl refinement): useStudioTree stays 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 util single-child (mergedLeafChild, used by punta/dito/layers) was promoted into ponte to break the punta↔layers cycle, leaving an acyclic tool DAG ponte ← punta ← {layers, dito}.
  2. @arno/tela — autonomous tool; validates the package template. The size applier in apps/web/src/components/sorgente-library-catalog.tsx takes a @arno/tela dep.
  3. @arno/punta — before its consumers; exposes layer-type, node-bridge, PuntaIcons.
  4. @arno/layers — before dito (dito runtime-depends on it). Name is chord-debt per ADR 0063 (rename to ordito/strati in a separate ADR + PR).
  5. @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/ponte extracted; single-child promoted; pnpm --filter @arno/ponte typecheck + 179 tests green.
  • @arno/tela extracted; canvas size applier imports from @arno/tela; typecheck + 42 tests green.
  • @arno/punta extracted; dito + layers import punta internals from @arno/punta; typecheck + 233 tests green.
  • @arno/layers extracted; typecheck + 35 tests green.
  • @arno/dito extracted; 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/ponte directly; telemetry DI relocated to studio/configure-ponte.ts.
  • Static regression gate each step: @arno/web typecheck + 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) on test-sorg-layout.arnomake.com under auth (2026-07-12). All four tools work across package boundaries: layers-row pick → Punta inspector populates (layers→ponte bus→punta); Tela 375×667 preset → 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-bus source:"layers"). Zero console errors on load + after interaction. studio-bus singleton 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/layers chord 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.