ADRs
ADR 0063 — Studio tools as per-tool reusable packages over a shared `ponte` kernel
  • Date: 2026-07-12
  • Status: Accepted
  • Builds on: ADR 0059 (naming chord), ADR 0061 (layerTypeOf shared dispatch), ADR 0062 (carrier contract)

Context

The four Studio tools — dito (bus-driven hover panel), tela (viewport-preset strip), layers (layers sidebar), punta (property / spacing inspector) — currently live inside the web app at apps/web/src/dev-tools/studio/<tool>/. They are mounted from exactly one place: the /app/library route (apps/web/src/app/app/library/page.tsx).

We want to reuse these tools in other parts of ARNO, and — critically — we want a single source of truth: an edit to a tool's code must propagate to every place it is used at once, with no copies to keep in sync. As the tools evolve, every consumer inherits the change on the next build.

The tools are not four independent islands. They share a runtime kernel that every one of them binds to:

  • studio-bus.ts — module-level singleton event bus (hover / pick state, tree mutators, clipboard, capability flags, active-tree handle)
  • useStudioTree.ts — the tree store (undo history, debounced save, registerActiveTree)
  • studio-events.ts — batched telemetry POST
  • types.ts (LayoutNode, isContainer, findNode, genId) and tree-ops.ts (moveNode, groupNodes, insertFromPalette)
  • adaptive/primitives/primitive-meta.tsx — primitive glyphs / labels (shared by layers and dito)

And they cross-import each other's internals:

imports →ditotelalayerspunta
ditosingle-childlayer-type, PuntaIcons
tela
layersnode-bridge
puntasingle-child

Plus runtime (non-import) coupling: dito is driven by hover events layers emits with source:"layers" and positions itself against layers' [data-layer-id] DOM; tela's size store is applied by the canvas component apps/web/src/components/sorgente-library-catalog.tsx, not by tela itself.

Consequence: no single tool detaches cleanly on its own. The shared kernel must come out first, and the tools carry genuine inter-tool dependencies.

Decision

Each Studio tool becomes its own workspace package (packages/<tool>, name @arno/<tool>), sitting on top of one shared kernel package. Consumers everywhere in ARNO import the tool from its package (import { Dito } from "@arno/dito") via workspace:*. An edit to packages/dito/src/** propagates to every importer on the next build / HMR pass — one source of truth, zero copies. This is the reuse mechanism the request asks for.

Package set

  • @arno/ponte — the shared kernel: studio-bus, useStudioTree + tree store, studio-events, types, tree-ops, adaptive/primitives. Every tool depends on it. Name per ADR 0059: ponte = bridge (Ponte Vecchio spans the Arno) — the substrate that bridges all tools into one live tree. Rejected studio-core / shared (generic, forbidden by ADR 0059 §"No generic English replacements").
  • @arno/tela — self-contained; no other-tool imports.
  • @arno/punta — its internals (layer-type, node-bridge, PuntaIcons) are imported by dito and layers.
  • @arno/layers — see chord-debt note below.
  • @arno/dito — depends on punta + layers at import and layers at runtime.

Inter-tool imports become explicit inter-package workspace:* dependencies (dito → @arno/punta + @arno/layers; layers → @arno/punta; punta → @arno/layers). "Per-tool" (поштучно) holds at the package boundary; it does not mean zero dependencies between them.

Extraction order (forced by the dependency graph)

  1. @arno/ponte — first; all four bind to it.
  2. @arno/tela — autonomous, validates the package template on the simplest tool. Its size applier in sorgente-library-catalog.tsx takes a @arno/tela dependency.
  3. @arno/punta — before its consumers (dito, layers).
  4. @arno/layers — before dito (dito runtime-depends on it).
  5. @arno/dito — last.

Package template

Follow packages/foundation (the React-component package pattern): name @arno/<tool>, version 0.0.0, private, type module, peerDependencies react/react-dom ^18, exports barrel ./src/index.ts (none of the tools has a barrel today — each gets one created as its entry), deps @arno/ponte: workspace:*, devDeps @arno/eslint-config / @arno/tsconfig / vitest. pnpm-workspace.yaml already globs packages/* — no glob change needed.

Implementation refinement — shared cross-tool utils live in the kernel

Two couplings surfaced during extraction that the naive per-tool split did not anticipate, resolved by pulling shared pure code into @arno/ponte rather than duplicating or creating circular tool deps:

  • useStudioTree stays app-side. It is imported only by the canvas and binds persistence (sorgente-draft, api-client); it is the app's kernel binding, not part of the reusable kernel. It reaches @arno/ponte like any consumer.
  • single-child (mergedLeafChild) promoted to @arno/ponte. It is a pure tree util used by punta, dito, and layers. Left in layers, it forced punta → layers while layers → punta (node-bridge) already existed — a circular package dependency. Promoting it to the kernel severs punta → layers, yielding an acyclic DAG: ponte ← punta ← {layers, dito}.
  • General rule: a pure, side-effect-free utility imported by two or more tools belongs in @arno/ponte, not in whichever tool happened to define it. This keeps tool packages depending only downward (on the kernel) or on a single acyclic chain.
  • studio-events transport is injected (configureStudioEvents) so the kernel carries no app coupling; the app wires it once via studio/configure-ponte.ts.

Naming chord-debt (deferred, by decision)

@arno/layers is a generic English name and breaks the ADR 0059 chord (dito / tela / punta are Italian). Keeping layers is a deliberate deferral to avoid entangling the extraction refactor with a rename: the rename lands in a separate ADR + PR. Candidates on record: ordito (weaving warp — the structural threads, pairs with tela/cloth) or strati (literally "layers"). Tracked as chord-debt; do not treat the current @arno/layers name as endorsed by ADR 0059.

Consequences

Positive

  • Single source of truth: one edit in packages/<tool> reaches every consumer across ARNO on rebuild. No copy drift.
  • Tools reusable anywhere via a plain import; each evolves centrally.
  • Package boundaries make the (previously implicit) coupling explicit and lintable.

Costs / risks

  • The kernel (@arno/ponte) must be extracted before any tool — no tool ships independently first.
  • Tools are not independent islands: dito, layers, punta carry inter-package deps; only tela is standalone.
  • sorgente-library-catalog.tsx (canvas size applier) must depend on @arno/tela.
  • @arno/layers is a known chord-violation until the rename ADR.
  • studio-bus is a module-level singleton; a package must preserve single-instance semantics (one bus per app, not per import) — bundler dedupe of @arno/ponte is load-bearing.

Regression gate: after every extraction step, /app/library (Sorgente tokens view) must render and drive all four tools exactly as before — same pick / hover / edit behavior.

Alternatives rejected

  • Copy tool code into each new site — no propagation, defeats the entire request.
  • One mega @arno/studio package — contradicts the per-tool decision, couples release cadence of unrelated tools, and forces consumers to pull all four to use one.
  • Figma Code Connect — solves design↔code mapping, not runtime component reuse across the app. Different problem.