- Date: 2026-08-06
- Status: Proposed (design spec — architecture-first; no code shipped this run)
- Phase / Feature: "New project → ARNO Foundation" fork of the live design system
- Builds on: ADR 0049 (Arno Sorgente), ADR 0052 (Sorgente shared-DB, prod-has-no-Sorgente), ADR 0053 (Sorgente on Foundation rules — tiers/refs/resolver), ADR 0054 (Sorgente as a Library surface), ADR 0039 (Foundation upgrade path — auto minor / major ritual), ADR 0045 (composite tokens are live refs; detach/re-attach)
- Amends / supersedes: ADR 0050 §2 ("greenfield user projects inherit from Foundation") and ADR 0050 §1a ("users do NOT get a separate repo; customization just lives inside the project" — now each project gets its own separate git repository, §6). See §7.
- Ratified: at the approval gate the maintainer ratified this ADR with changes — a separate git repo per project is core (not roadmap), projects are independent user-named entities, override-preservation extends to the component layer, and both the sparse overrides and the full-resolved set are materialized. All gate decisions are recorded under "Decisions ratified at the gate". No code is shipped this run (architecture-first).
Amendment — 2026-08-06 — Invisible provisioning, Arno-org hosting, lazy materialization (Variant A)
A follow-up gate refined how the per-project repo (§6) is provisioned and surfaced. The model (§1–§9) is unchanged; three implementation-level decisions are now settled, and the billing concern is spun out to ADR 0082.
A. Provisioning is invisible — no user-facing "Export" action. An earlier build slice surfaced an explicit Export design button the user pressed to materialize tokens→git. Ratified reversal: materialization is under the hood, driven by the project lifecycle (create → edit → share), never a manual step. Creating and editing a project requires zero git-facing activity from the user; the git artifact tracks the DB pin layer automatically. The existing design-export serializer + commitFiles code is kept as the mechanism, but its trigger moves from a button to the automatic lifecycle; the button is removed at build time.
B. Repo home = the Arno GitHub org (Variant A), not the user's GitHub. §6 offered "Arno-managed repo when no connected_repo"; this is now the primary, default model. Every project repo lives under Arno's own GitHub org (github.com/{arno-org}/arno-project-{projectId}), created via a single Arno-owned GitHub App whose credentials are provisioned once by the maintainer — never per user. The user's Arno account is not a GitHub account; they never log into GitHub. Hosting a project repo under the user's personal GitHub (via connected_repo / a per-user GitHub OAuth) is Variant B — optional, deferred, layered on top later without disturbing Variant A. At 100k users the org needs a GitHub Enterprise agreement (automated bulk repo creation under one org is atypical — pre-clear with GitHub).
C. Materialization is lazy — created at first share, not at project create. This supersedes §6's "Repo creation … at project create" and Roadmap Phase 2's "provision its per-project repo" at create. The DB fork still happens at create (empty overlay, §2). The GitHub repo is provisioned lazily on the first share — when the owner first asks for a developer link. Rationale: most of 100k projects are never shared → most GitHub repos are never created → GitHub cost and API-rate pressure stay near zero; the git-data lives in the DB pin layer (source of truth) until a share needs a real repo. Share is also the monetization gate (ADR 0082): repo creation and payment coincide — Arno never creates a repo it was not paid for.
Access model (Variant A). Repos are private. Two access flavors:
- Read / clone / pull (consumer default): a tokenized clone URL (
https://x-access-token:{token}@github.com/{arno-org}/arno-project-{projectId}.git) — no GitHub account, no seat consumed, $0. This is the "give the developer a link" path. The developer works directly on github.com ("in obhod Arno") with no Arno involvement after the link. - Write / PR (contributor, opt-in): the owner enters the developer's GitHub handle in Arno; the App adds them as a collaborator — the developer works under their own GitHub identity. A collaborator on a private repo consumes a paid GitHub seat; a deploy key is the seat-free alternative for repo-scoped write. Arno never sees the developer's GitHub credentials.
Cost (Variant A). GitHub bills per seat, not per repo — 100k private repos cost $0 to exist; storage (KB-scale token exports) is negligible. The only paid lever is a write-collaborator on a private repo (~$4–$21/seat/mo), which the read-token-link model avoids entirely. With lazy-create + read-links, the marginal GitHub cost of 100k projects is ≈ $0 above Arno's base plan.
Infra gates (maintainer-provisioned; not code). Variant A cannot ship until, in order: (1) an Arno GitHub org + GitHub Enterprise agreement sized for bulk repo creation; (2) the GitHub App created and its secrets provisioned on arno-api-test (currently absent — blocks all git writes); (3) the payment stack (ADR 0082). These are credential/account steps only the maintainer can perform.
Context
The New-project modal offers an "ARNO Foundation" entry. The maintainer wants that entry to fork the live, evolving design system into a user-owned project that:
- starts as a full copy of everything the design system currently defines;
- keeps tracking the upstream design system for values the user has not touched (new/changed upstream values flow down);
- pins any value the user has edited (upstream changes never overwrite a user edit);
- is strictly one-way — upstream → project only; a project edit is local and never propagates up.
This ADR records two decisions taken with the maintainer, each of which revises an accepted ADR:
- Upstream = Sorgente, not Foundation. Foundation is a static code artifact with no in-product path to evolve it; Sorgente is the live, maintainer-editable design system that actually mutates (ADR 0049/0052/0053). Only a thing that changes can be the propagating source. This supersedes ADR 0050 §2's "inherit from Foundation" framing.
- One-way is a first-class invariant. Sorgente → projects only; never projects → Sorgente.
What the code already gives us (grounding)
- Fork-on-create is cheap and copy-free.
POST /api/v1/projects(apps/api/src/index.ts:308-349) inserts a singleprojectrow and copies nothing; greenfield "loads Foundation" is by-reference viadsMode+foundationVersion(index.ts:327-331, hard-pinned'1.0.0'). An empty override layer already means "fully inherited." - The per-project pin layer already exists.
token_override (project_id, token_id) → override_value(packages/db/src/schema.ts:748, with adetached_fromsnapshot),brand_seed (project_id, slot)(L637),brand_binding (project_id, semantic_id)(L664). A DELETE returns a token "to the cascade" (schema comment L746-747). Presence of a row = pin; absence = inherit. This is exactly the override-preservation contract — today it is only written on detach (ADR 0045); generalizing it to "written on any user edit" is a semantics change, not a schema change. - Sorgente is a separate namespace, keyed by
user_id.const sorgente = pgSchema("sorgente")(schema L843);sorgente.token_overridecarriestier+value_type(ADR 0053, schema L909). It is the ARNO-team singleton design system, not a set of per-project columns. (Correction to earlier framing: there is no per-projectsorgente*column onproject.) resolveSorgenteis pure and override-only.packages/sorgente-resolver/src/resolver.ts:86resolves refs/brand-ramps and emits a:root{}overlay; unknown ids dead-end to"inherit"(L160-163). Its output is baked over Foundation'sglobals.css. So the resolved Sorgente artifact already folds Foundation floor ⊕ Sorgente overrides into one CSS-ready value set.- Prod has no Sorgente DB. ADR 0052 §2: the prod worker gets no
SORGENTE_DATABASE_URL; Sorgente endpoints fail closed. Prod consumes Sorgente only as a static bake produced byrelease (prod). - ADR 0039's upgrade machinery is designed but inert.
foundation_upgrade_eventdoes not exist;migrateProject(packages/foundation/src/migrations/types.ts:97) is never invoked; every Foundation-bound project is'1.0.0'forever. - No token→git exporter.
apps/api/src/sync.tspushes only component MD to brancharno/{login}(L196-217); it never writes tokens/brand_seed/globals.css. The "own repo/branch carrying forked tokens" is net-new.
Decision
§1 — The layered read model
A Sorgente-linked project resolves each design value through two layers:
effective project value(id) =
project pin layer (if a row for `id` exists) ← PINNED, wins
⊕ published Sorgente base (read-through, else) ← INHERITED, tracks upstreamThe published Sorgente base is itself the already-resolved Foundation floor ⊕ Sorgente overrides (the release (prod) bake, §3). So the full stack is three conceptual layers, collapsed to two at resolve time:
Foundation package floor → Sorgente release overrides → project pin layer
(@arno/foundation) (published, read-through) (per-project rows)
─────────── the "published Sorgente base" ─────────── the "branch" (§6,
git-materialized)Granularity — what a "value" (and therefore a pin) is:
| Kind | Pin row | Key | Inherit when |
|---|---|---|---|
| Token (primitive / semantic / component / composite) | token_override | (project_id, token_id) | no row for that token_id |
| Brand seed (a palette slot's hex) | brand_seed | (project_id, slot) | no row for that slot |
| Semantic→ramp binding | brand_binding | (project_id, semantic_id) | no row for that semantic_id |
| Variant / composition (Layers 2/3, ADR 0049 §8) | — | — | out of scope v1 — token inheritance only |
"User touched this" is the existence of the row, not a separate flag. Editing an inherited value materializes a pin row (with detached_from capturing the pre-edit upstream value for the re-attach diff, reusing the ADR 0045 mechanism). Resetting deletes the row and inheritance resumes. No touched boolean, no snapshot of untouched values — absence is the inherit signal.
Worked example (the maintainer's case). Sorgente ships a color at #ebefe9. A project has not touched it → it resolves to #ebefe9 (inherited). The user edits it to #ebefe1 → a token_override row is written (pinned). Arno later publishes a Sorgente release that changes that color again → the project keeps #ebefe1 (pin wins); the update reaches only projects that never touched the key. This is the whole contract in one value, and — per §1b — it applies identically to components.
§1a — Projects are independent, user-named entities
Each project is a unique entity with a user-defined name/identity supplied at create time. There is no shared or template project; every "ARNO Foundation" project stands on its own.
- Independent of one another. One project's pins never affect another's. This is structural: pin rows are
(project_id, …)-scoped (§1), and no resolve or write path reads or crosses project boundaries. Two projects owned by the same user are as independent as two owned by different users. - Common only at the origin. The sole thing projects share is a starting point: at creation, every project resolves identically to the pinned Sorgente release (empty overlay, §2). From there they diverge as each owner's edits accumulate as pins.
- Divergence is NOT a frozen snapshot. "Diverge" means the set of user-pinned values grows and differs per project — it does not mean the project stops receiving upstream changes. Untouched values keep tracking the Sorgente release channel (§2/§3); only the pinned subset is frozen. A reader must not mistake independence/divergence for a snapshot fork: the inherited remainder still syncs one-way from upstream for the life of the project.
§1b — The component layer follows the same pin model
Ratified at the gate: "то же самое с компонентами — если изменил пользователь, а Арно не правит уже." Override-preservation is not token-only; it applies identically to components. A component the user edited is pinned; upstream (Sorgente-release) component updates reach only components the project has not touched.
This is a clean generalization of §1 because the codebase already resolves components by the same row-present = customized, absent = inherited convention:
- Project component MD starts empty and is authored on save. Project create inserts only the
projectrow (+ share link) — nocomponent_md_raw(apps/api/src/index.tscreate flow;apps/api/src/init.ts:331seeds onlyconnectedRepo). Rows appear only when the user saves MD (apps/api/src/md.ts:149;apps/api/src/domains/editor.ts:64onConflictDoUpdate), keyed(project_id, file_path)(packages/db/src/schema.ts:143-157). So presence of acomponent_md_rawrow = "the project customized this component"; absence = inherit. Exactly thetoken_overrideshape (apps/api/src/token-overrides.ts:8— "one row per (project, tokenId); PUT upserts, DELETE re-attaches"). - The Foundation component catalogue already uses the overlay pattern.
packages/origine/src/backend-store.ts:11-15: "the backend stores OVERRIDES, not the whole catalogue… an edited component is persisted as a full-contract entity that replaces its seed on the next load"; cascadecomps.get(c.id) ?? c(backend-store.ts:110) over the 51-entryFoundationComponentsseed (packages/origine/src/registry.ts:59). Row present = pinned; absent = seed. This is the component analog oftoken_override, and its own schema comment (schema.ts:1108-1112) names the convention explicitly.
So the pin model of §1 covers three override surfaces: tokens (token_override), brand (brand_seed/brand_binding), and components (component_md_raw for the MD spec; the Foundation-contract overlay for variant/behavior). The read model, the one-way invariant (§4), and the release-channel upstream (§3) all apply unchanged — the component "base" is the pinned Sorgente release's component set, and project component edits are local pins never pushed upstream.
Keying delta to resolve at build time (grounded, not glossed). The two component surfaces are keyed differently today: project MD (component_md_raw, component_md_versions, component_drift) is project_id-scoped, but the Foundation-contract overlay (sorgente.foundation_entity, sorgente.variant_override) is user_id-scoped (schema.ts:1114-1132, unique (user_id, kind, entity_id)). Per-project component override-preservation therefore requires the Foundation-contract overlay to move from user_id to project_id scope so a project's contract edits pin independently of other projects (§1a). The MD pipeline already satisfies the per-project requirement; the contract-overlay layer is a data-model delta (§8) and a roadmap item, not a code change assumed done.
§2 — Fork-on-create = a live read-through link, not a snapshot copy
Creating an "ARNO Foundation" project inserts one project row with the new mode (§8) and zero override rows. It does not copy Sorgente's tokens into per-project tables.
- A snapshot copy would freeze the project at create time: new upstream keys would never appear, changed upstream values would never propagate — it directly breaks requirements (2) and (3). Rejected.
- A read-through link makes override-preserving inheritance automatic and O(1) to create: untouched keys resolve from the published Sorgente base on every read (so new/changed upstream values and even brand-new keys appear for free), and the only rows a project ever stores are its own pins. This matches the existing copy-free greenfield create exactly (
index.ts:308).
Justification against ADR 0053/0054's live-propagation model: Sorgente is already a live-resolved override overlay (resolveSorgente), and the Library surface (ADR 0054) already reads it live. A forked project is the same resolve pipeline with one extra top layer (the project pins) and one changed base (published Sorgente instead of raw Foundation).
§3 — Upstream is the published Sorgente release channel, not live head
User projects run on prod, where there is no Sorgente DB (ADR 0052 §2). Therefore projects inherit from the published Sorgente artifact, not the live editor state. Concretely, release (prod) already bakes the resolved Sorgente :root{} block; this decision names that bake a first-class, versioned, immutable release that user projects read through.
This single choice resolves three problems at once:
- Prod-safety. No live
sorgente.*read path on prod is required; the release artifact is already shipped to prod. ADR 0052 §2 stands unamended. - Versionability (reconciles ADR 0039). Each publish is a semver-tagged Sorgente release. That restores an upgrade unit for the otherwise-unversioned live Sorgente, so ADR 0039's auto-minor / major-ritual model applies at release granularity instead of per-keystroke (§5).
- One-way by construction. A published artifact is read-only to consumers; there is no API by which a project could write into a Sorgente release.
Maintainer exception (unchanged from ADR 0049 §3): the maintainer's own linked_to_arno_sorgente projects read live head on dev/test for the ~250 ms edit loop. That path is maintainer-only, non-prod, and is explicitly not what user projects use.
§4 — The one-way invariant and where it is enforced
One-way is not a new guard bolted on; it is the absence of any project→Sorgente write path, plus two pre-existing auth gates:
- Project writes land only in
token_override/brand_seed/brand_binding, all(project_id, …)-keyed, via the owner-gatedbrand.tsendpoints. Their write target is always the project's own row. - Sorgente writes land only in
sorgente.*,(user_id, …)-keyed, behindisSorgenteMaintainer()(ADR 0052 Amendment 2026-07-14). A user project's owner is not a Sorgente maintainer, so the upstream tables are structurally unreachable from a project mutation. - The only existing upstream-writing mechanism is ADR 0049 §3's maintainer mirror (
linked_to_arno_sorgente). This ADR forbids wiring any analogous project→Sorgente mirror for user projects.
Downstream writes can only land in the project layer because the two design systems are two schemas with two owners and two gates; a project edit never has a Sorgente row id to write to, and never holds the maintainer capability. Enforcement point in the build phase: a guard test asserting no project-scoped mutation endpoint touches sorgente.*.
§5 — New-key propagation and reconciliation with ADR 0039
- New upstream key (a token id that exists in the new Sorgente release, absent in the project pin layer): appears automatically via read-through (§2). Semantically an additive change → auto-adopted, matching ADR 0039 §2's "minor = auto-upgrade on read."
- Changed upstream value on an untouched key: follows on next read (auto). On a pinned key: no change (§9 case 1).
- The version anchor: a Sorgente-linked project carries a
sorgente_releasepointer (§8). Tracking "latest release" makes additive/minor changes auto-apply; a breaking release (renamed/removed ids — §9 cases 4/8) is gated exactly like an ADR 0039 major: the project stays on its pinned release and a non-blocking banner offers Preview / Apply / Stay, running the samemigrateProjectapplier (packages/foundation/src/migrations/types.ts:97) retargeted at a Sorgente-release migration file. A new key does not count as a major — it is a minor and auto-applies.
This is the reconciliation ADR 0039 needs: today Sorgente is unversioned live state, which has no upgrade unit. Publishing releases gives it one, and ADR 0039's ritual — designed but inert — becomes the mechanism that governs Sorgente→project adoption.
§6 — Every project gets its own unique git repository (core)
Ratified at the gate: "для каждого проекта создаётся своя уникальная гит ветка" and, refining the home, "отдельная репа под каждый проект" — a separate git repository per project. This is a first-class element of the model, not deferred. The DB pin layer (§1/§1b) remains the source of truth; the repo is its git materialization — a derived, self-describing artifact of the project's own divergence. This section designs it (architecture-first — no code this run).
What is materialized — BOTH the overrides AND the full resolved set (ratified). The repo carries two clearly-separated artifact classes:
- The project's own overrides — the pin layer (
token_override/brand_seed/brand_binding, and component MD) — the source-of-truth of what diverges. This is the layer that must never be clobbered by an Arno update. - The full resolved set —
Sorgente release ⊕ project overrides, resolved — a buildable artifact for the user's own developers.
The maintainer's rationale is the split's reason for being: the user customizes their own system; Arno's developers only extend the Arno platform. So the user's git repo is a first-class deliverable for their engineers and must contain the complete, buildable token/component set — not just a sparse diff that only makes sense next to Arno's internal base. Keeping the two classes in separate files preserves the pin source-of-truth (class 1) while still shipping something a downstream build can consume directly (class 2). Concretely, per project repo:
| File | Content | Class |
|---|---|---|
design/overrides.tokens.json | the pin layer as DTCG ($type/$value, refs intact) — the same shape Foundation/Sorgente use (ADR 0045/0053) | 1 — overrides (source of truth) |
design/overrides.tokens.css | the project's :root{} overlay, overrides only (same shape as resolveSorgente) | 1 — overrides |
design/tokens.full.json + design/tokens.full.css | the full resolved token set (Sorgente release ⊕ overrides) — buildable standalone | 2 — full resolved |
Design_system/*.md | project component MD (component_md_raw), pushed verbatim — already materialized by sync.ts today (§ exporter split) | 1 — overrides (components) |
design/arno-project.json | manifest: projectId, user-defined project name, pinned sorgente_release id + semver, exported-at | both — self-describing pointer to the base |
The manifest pins the sorgente_release so a reader can always reconstruct which base the full set was resolved against. (This revises the earlier "overrides only, never the base" stance: the overrides artifact still excludes the base, but a separate full-resolved artifact is now emitted alongside it for buildability — the two are not conflated.)
Repo identity — a SEPARATE repo per project (ratified). The gate settled the home: each project is its own git repository, not a branch inside a shared per-user repo. This matches §1a ("each project is a unique, independent entity") at the git layer — clean per-project isolation, no shared-repo blast radius, a project delete drops a whole repo rather than pruning a branch. The trade-off recorded: more repos to provision and authorize (one per project vs one per user) — accepted for the isolation it buys. Inside each repo the working ref can be a stable default branch (e.g. main); the identity is the per-project repo, named e.g. arno-project-{projectId} (projectId = globally-unique PK → collision-free, CLAUDE.md §"Branch naming uniqueness" satisfied without a pre-check; the user-defined project name is a display label that can be renamed without touching repo identity).
Repo home — relation to connected_repo. If the project has a connected_repo, that repo is the home (the design files land beside the user's code). Otherwise Arno provisions the per-project repo (Arno-managed) so the "own repo" guarantee holds from create; on later connect, history pushes into the user's repo. Either way the unit is one repo per project.
Repo creation — the git echo of the empty-overlay fork. At project create (§2), alongside the single project row, provision the project repo with an initial commit whose overrides artifacts are empty (zero divergence), whose full-resolved artifacts equal the pinned Sorgente release verbatim, and whose manifest pins that release. This mirrors the DB state exactly: "identical to Sorgente release X, zero divergence." Every subsequent pin write appends a commit; every reset drops the value from the next export.
Exporter split — components reuse sync.ts; tokens are net-new. The two override classes materialize through two exporters into the same project repo:
- Components (already built).
sync.ts:197-254readscomponent_md_rawfor the project and pushes each row verbatim as its.mdfile (row.filePath, sync.ts:208-216; commit +updateRefat 254). Component override-materialization is therefore done — it operates on the project's override rows (not a resolved set), exactly the pin layer. It re-targets from the current per-user branch (arno/{login}) to the per-project repo. - Tokens (net-new). A new serializer writes the
design/*token artifacts (overrides + full-resolved + manifest, table above). It reuses the same git plumbing (auth, blob/tree/commit/updateRef) but is a new serializer and — with the full-resolved class — a resolve step (Sorgente release ⊕ overrides).
Both land in the one per-project repo; there is no longer a "one branch or two" question — the repo is the unit.
Direction — the repo is an OUTPUT; one-way holds, no re-import (ratified). Materialization flows DB → repo only (like the deterministic codegen rule in ADR 0049 §8a: same DB state always produces the same files). The repo never writes back to Sorgente, and — settled at the gate — never writes back to the project DB either: git → DB re-import is out of scope, permanently, not just v1. The DB pin layer is the single source of truth; the repo is a derived artifact. This is the maintainer's "Arno doesn't touch what the user changed" applied in both directions — Arno's updates never clobber the user's DB pins (§1/§1b), and the git materialization never feeds back to mutate them. It also preserves the §4 one-way invariant: a git artifact downstream of the DB cannot propagate up to Sorgente. (One consequence to design at build time: since git is never re-imported, the connected-repo webhook path — webhooks.ts, which today updates component_md_raw from pushes — must be reconciled so it does not constitute a back-channel for dsMode='sorgente' projects.)
§7 — Amendments to ADR 0050
- §2 (superseded). "When a greenfield user project consumes Foundation …" → user projects created via the "ARNO Foundation" entry inherit from the published Sorgente release, not the raw Foundation package. Foundation remains the floor beneath Sorgente (§1), so it is still present in the cascade — just no longer the thing users track. Foundation's own maintenance loop (ADR 0050 §§1–6) is unaffected.
- §1a (amended). ADR 0050 §1a said "users do NOT get a separate repo; the result lives inside their project." This is now directly revised: every project gets its own separate git repository (§6) that materializes the project's overrides (tokens + component MD) plus a full-resolved buildable set. The "no separate repo" clause is the specific part superseded — each project now has exactly its own repo. The DB pin layer remains the source of truth (the repo is a one-way DB→git output); it still "lives inside the project," now additionally mirrored to the project's repo.
§8 — Data-model deltas required (described, not built this run)
project.dsMode— new value'sorgente'(ratified: a distinct value, not a redefinition of the reserved'hybrid'). Marks a project whose base is the Sorgente release channel. Resolve (§1) branches on it.project.sorgenteRelease— new nullable text column, the release id/semver the project is currently resolved to (analogous tofoundation_version, schema L91). NULL = track head (maintainer/dev only, §3). Set = pinned; adopts newer releases via ADR 0039 ritual.- Sorgente release channel — the net-new substrate, specified inside this ADR (ratified: no companion ADR). A
sorgente_releaserecord (immutable resolved snapshot + semver + published-at), produced by the existingrelease (prod)bake, prod-readable. This is what §3 reads through. token_overridesemantics generalization — no schema change. New rule: any user edit to an inherited token writes a(project_id, token_id)row (today it is written only on detach).override_value jsonbalready accepts scalar / ref / composite;detached_fromalready carries the pre-edit snapshot for re-attach.- Per-project resolver branch — new code path (build phase): for
dsMode='sorgente', base = the project'ssorgente_releaseresolved value set (§3) instead of the@arno/foundationfloor; project pin rows overlay on top. ReusesresolveSorgente's ref/brand math. - One-way guard — no table; a test asserting project mutation endpoints never write
sorgente.*(§4). - Per-project repo + token exporter (core, §6). Provision a separate repo per project at create (Arno-managed when no
connected_repo); a net-new token exporter serializes both artifact classes —design/overrides.tokens.{json,css}(pin layer, DTCG/CSS) anddesign/tokens.full.{json,css}(full resolvedrelease ⊕ overrides) plusdesign/arno-project.json(manifest pinningsorgente_release). Component MD reuses the existingsync.tsexporter, re-targeted to the project repo. Needs: repo provisioning on create, the token exporter + resolve step, export-on-pin-change wiring, and reconciling the connected-repo webhook so it is not a git→DB back-channel (§6). Direction is DB → repo only; no re-import (§6). - Component pin layer — per-project scoping of the Foundation-contract overlay. The MD pipeline is already per-project (
component_md_raw/component_md_versions, keyed(project_id, file_path)). The Foundation-contract overlay that pins variant/behavior edits (sorgente.foundation_entity,sorgente.variant_override) is currentlyuser_id-scoped (schema.ts:1114-1132); to pin components per-project (§1b) it must move toproject_idscope (or gain a project dimension). No change to the MD tables; this is the contract-overlay layer only.
§9 — Conflict / edge semantics
| # | Situation | Resolution |
|---|---|---|
| 1 | Upstream changes a key the user overrode | No change. Pin wins (§1). |
| 2 | Upstream changes a key the user did not override | Project follows on next read (read-through). |
| 3 | Upstream adds a new key | Appears in project automatically (§5); minor, auto-adopted. |
| 4 | Upstream deletes a key the user overrode | Pin row persists. If the override is a self-contained literal, it still renders (that is the point of pinning). If it is a composite with refs into the deleted id, the resolver dead-ends → "inherit" + non-blocking warning (existing behavior, resolver.ts:160). Surface an "orphaned override" badge. |
| 5 | Upstream deletes a key the user did not override | Key disappears from the project; any consumer referencing it dead-ends → "inherit" + warning (same existing fallback). |
| 6 | User resets an override to inherited | DELETE the pin row; inheritance resumes next read (the ADR 0045 re-attach affordance). |
| 7 | Breaking Sorgente release under one-way sync | Project stays on its pinned sorgente_release; ADR 0039 banner offers Preview / Apply / Stay. Head-tracking maintainer projects skip the gate (§3). |
| 8 | Upstream renames a key (delete + add) | defaultMigrations re-point untouched bindings to the new id (ADR 0039 §4); an overridden key keeps its pin and may become orphaned per case 4. |
| 9 | Upstream changes a component the user edited (§1b) | No change. Component pin wins — component_md_raw row present = customized; the release update reaches only components with no project row. Same rule as case 1, component layer. |
| 10 | Upstream changes a component the user did not edit | Project follows the pinned release's component set (no component_md_raw row → inherit). |
| 11 | Upstream removes a component the user edited | The project's component_md_raw row persists and still renders (it is the full MD spec); it becomes an orphaned component relative to the base — surface the same "orphaned override" indicator as case 4. |
Anti-patterns explicit
- Do not copy Sorgente into per-project tables at create time (snapshot fork). It silently breaks upstream inheritance; the empty-overlay read-through is the whole point (§2).
- Do not give user projects a project→Sorgente mirror. ADR 0049 §3's mirror is maintainer-only and upstream-writing; replicating it for user projects violates the one-way invariant (§4).
- Do not point user-project resolve at the live
sorgente.*DB. Prod has none (ADR 0052 §2); inherit from the published release (§3). - Do not store a "touched" flag or snapshot untouched values. Row presence is the pin; absence is inheritance (§1).
- Do not treat a new upstream key as a breaking change. Additive = minor = auto (§5); only rename/remove is major.
- Do not conflate the base into the overrides artifact. The overrides class (
design/overrides.tokens.*, component MD) stays sparse — only what the project changed; the full base belongs only in the separate full-resolved class (design/tokens.full.*), which is emitted deliberately for buildability (§6). Two files, two purposes; never merge them. - Do not let the project repo write back into the project DB or into Sorgente. Materialization is one-way DB → repo (§6); git → DB re-import is permanently out of scope, and a downstream git artifact must never propagate up (§4). Reconcile the connected-repo webhook so it is not a back-channel for
dsMode='sorgente'projects. - Do not name the repo off the user-defined project name. Names are editable display labels; repo identity uses the stable
projectId(arno-project-{projectId}) so a rename never orphans it (§6). - Do not let an Arno/Sorgente update overwrite a component the user edited. Component override-preservation is first-class (§1b): a
component_md_rawrow present = pinned; upstream reaches only untouched components. Project component edits are local pins, never pushed upstream (§4).
Roadmap (design only — nothing built this run)
Ordered so each phase is independently shippable and later phases don't invalidate earlier ones.
- Phase 0 — ratify. Done at the gate (all sub-decisions settled; see "Decisions ratified at the gate").
- Phase 1 — resolve model + generalized token pins. Add
dsMode='sorgente'+project.sorgenteRelease; branch the per-project resolver to base on the Sorgente release; generalizetoken_overrideto write on any edit; the one-way guard test. (Depends on Phase 3's release, or a dev-only "track head" placeholder.) - Phase 2 — fork-on-create UX (user-named project + repo). Wire the "ARNO Foundation" modal entry to create a user-named
dsMode='sorgente'project (empty overlay, §1a) + provision its per-project repo (§6); surface inherited-vs-pinned + reset-to-inherited (reuse ADR 0045 re-attach). - Phase 3 — Sorgente release channel. Turn the
release (prod)bake into versioned, immutable, prod-readablesorgente_releaserecords; wire read-through to "latest release." - Phase 4 — per-project repo + token exporter (core, §6). Provision the separate per-project repo on create; build the token exporter for both artifact classes (overrides + full-resolved) + manifest; re-target the existing
sync.tscomponent-MD exporter to the project repo; wire export-on-pin-change; reconcile the connected-repo webhook against git→DB re-import. - Phase 5 — component pin layer (per-project contract scope, §1b/§8.8). Move the Foundation-contract overlay (
sorgente.foundation_entity/sorgente.variant_override) fromuser_idtoproject_idscope so component/variant edits pin per-project; wire component read-through against the pinned release's component set. (MD pins already per-project — this closes the contract-overlay gap.) - Phase 6 — upgrade ritual retarget. Wire
migrateProject+ afoundation_upgrade_event-style audit to Sorgente-release adoption (auto-minor, major banner) per ADR 0039 §§2-5; covers token AND component migrations.
Decisions ratified at the gate
The maintainer ratified this ADR with the following settled (previously open) decisions:
- A separate git repository per project is core (not roadmap). Each project is its own repo (
arno-project-{projectId}), materializing its overrides (§6). Amends ADR 0050 §1a (§7). - Projects are independent, user-named entities (§1a). Common only at the origin; they diverge via the user's own pins, and divergence is not a frozen snapshot.
dsMode= a distinct new value'sorgente'— not a redefinition of'hybrid'(§8.1).- The Sorgente release channel is specified inside this ADR — no companion ADR (§8.3).
- Collaborator (level-B) test projects inherit from the published release channel; live-head read-through stays maintainer-only, non-prod (§3).
- Repo home = a separate repo per project (not a per-owner shared repo) — clean per-project isolation, at the cost of more repos to provision (§6).
- Materialize BOTH artifact classes — the sparse overrides AND the full resolved set, kept in separate files — because the user's repo is a first-class deliverable for their own developers and must be buildable standalone (§6).
- Git → DB re-import is out of scope, permanently — materialization is one-way DB → repo; Arno never clobbers the user's DB pins, and git never feeds back to mutate them (§6).
- Override-preservation covers the component layer too (§1b). A user-edited component is pinned; upstream reaches only untouched components — the same rule as tokens, grounded in
component_md_raw(per-project) + the Foundation-contract overlay (per-project scoping is delta §8.8).
Naming (optional codename — not mandated)
This ADR does not mandate an Italian product/module codename. The user-visible surface is simply "user projects" (or "projects"), each carrying the user-defined name supplied at create (§1a); nothing in the model depends on a fixed product noun.
If a codename for the fork mechanism is later wanted, the ADR 0059 chord recommendation is Innesto (graft): a scion grafted onto rootstock draws sap for what it does not grow itself (untouched values track upstream) while its own growth stays local and never flows back into the stock (overrides pinned + one-way) — the one image that encodes both invariants. Offered, not required; any Rules.md § Naming chord mirror is a post-merge maintainer chore only if a codename is adopted.
Open questions (build-time detail only — none block ratification)
The gate settled every model-level decision. What remains is implementation detail for the build phase:
- Provisioning mechanics for the per-project repo — GitHub org/namespace for Arno-managed repos, rate/quota handling when many projects are created, and the push path when a
connected_repois added after create (migrate the Arno-managed repo's history vs start fresh in the user's repo). - Webhook reconciliation — the exact guard that stops
webhooks.ts(today: push → updatecomponent_md_raw) from acting as a git→DB back-channel fordsMode='sorgente'projects (§6). A build-phase design task, not a model question.
References
- ADR 0082 — per-project repo hosting under Arno's org + monetized developer-share (the Variant A billing/hosting spec split out of the 2026-08-06 amendment).
- ADR 0039 — Foundation upgrade path (auto minor / major ritual;
migrateProject). - ADR 0045 — composite tokens are live refs; detach / re-attach (the pin/unpin mechanism reused here).
- ADR 0049 — Arno Sorgente (live editor, §3 maintainer mirror = the one direction we do not extend).
- ADR 0050 — Foundation evolution (§1a, §2 amended here).
- ADR 0052 — Sorgente shared-DB (§2 prod-has-no-Sorgente — the constraint behind §3).
- ADR 0053 — Sorgente on Foundation rules (tiers, refs,
resolveSorgente). - ADR 0054 — Sorgente as a Library surface (live read of Sorgente state).
- ADR 0059 — Italian / Renaissance naming chord.
packages/db/src/schema.ts(project L63, brand/token overrides L637-769,sorgenteschema L843+;component_md_rawL143-157,component_md_versionsL159,sorgente.component_entryL1078,sorgente.foundation_entityL1114-1132);apps/api/src/index.ts:308(create — inserts no component rows);apps/api/src/init.ts:331(seeds onlyconnectedRepo);apps/api/src/md.ts:149+apps/api/src/domains/editor.ts:64(component MD written only on user save);packages/origine/src/backend-store.ts:11-15,110+packages/origine/src/registry.ts:59(Foundation component overlay pattern + seed);packages/sorgente-resolver/src/resolver.ts:86(resolveSorgente);apps/api/src/sync.ts:197-254(component-MD git exporter);apps/api/src/token-overrides.ts:8(row-present-means-pinned).