Governance
Decision records, how changes get in, maturity criteria, and who decides.
How Syntara changes: who decides, how a request gets in, and how something gets out again without breaking the products that use it. The full text is GOVERNANCE.md.
Syntara has one maintainer, who pairs with AI agents. The process is written for a team so it can be judged as one. Where a script enforces a rule, the command is next to it. Where nothing enforces it yet, this page says so.
Decision records
Every choice with a design trade-off gets an architecture decision record: context, decision, alternatives, consequences — one page each. Each one names who made the call: Anuj, or Claude’s recommendation that Anuj accepted or hasn’t reviewed yet.
| ADR | Decision | Status |
|---|---|---|
| ADR-001 | Token build — the engine owns the exporters | AcceptedClaude recommended, Anuj accepted (2026-09-27) |
| ADR-002 | Headless primitives — React Aria Components | Accepteddecided by Anuj (2026-09-26) |
| ADR-003 | Styling — CSS Modules + CSS custom properties | Accepteddecided by Anuj (2026-09-26) |
| ADR-004 | Docs framework | Accepteddecided by Anuj (2026-09-26): Next.js App Router + MDX, to match the shadcn/ui bar. Claude had recommended Astro Starlight; see “Decision” below for what changed. |
| ADR-005 | Token tiers and naming | AcceptedClaude recommended, Anuj accepted (2026-09-27) |
| ADR-006 | OKLCH ramps + contrast solver | AcceptedClaude recommended, Anuj accepted (2026-09-27); the open question was decided by Anuj earlier the same day |
| ADR-007 | One meta.json per component | AcceptedClaude recommended; Anuj delegated the call ("do whatever is correct", 2026-09-27). Accepted because it is built and enforced: pnpm check:meta passes for all 52 components |
| ADR-008 | Agent trust levels | Accepted as the Phase 5 planlevels defined by Anuj (BRIEF §7); enforcement design Claude recommended, Anuj accepted (2026-09-27). Not built yet: enforcement lands with the MCP server and drift auditor |
| ADR-009 | RTL via CSS logical properties from day one | AcceptedClaude recommended, Anuj accepted (2026-09-27). In use across every component and all tenants, including Qamar (RTL) |
| ADR-010 | Two DTCG dialects — canonical and Figma | AcceptedClaude recommended, Anuj accepted (2026-09-27); Anuj is on Figma Starter, so the single-mode layout was added |
| ADR-011 | Distribution — npm package and shadcn-compatible registry from one source | Reviseddecided by Anuj (2026-09-27): npm only for users. See Revision. |
| ADR-012 | Overlays copy their scope; ThemeScope owns the locale | AcceptedClaude recommended (C3's approach); Anuj delegated the call ("do whatever is correct", 2026-09-27). Accepted because it is built and tested in every overlay |
| ADR-013 | Tactile style, with finesse inspired by macOS and visionOS | Accepteddirection decided by Anuj; token design Claude recommended, Anuj accepted |
| ADR-014 | Syntara draws its own icons (@syntara/icons) | AcceptedDirection decided by Anuj ("curvy and minimalistic"); drawings by Claude, reviewed by Anuj. |
| ADR-015 | An editorial voice, proven by a Care tenant built from Anuj's KYB work | AcceptedDecided by Anuj ("match the level of my KYB prototype so it doesn't feel like generic AI components"); implementation Claude recommended, Anuj accepted. |
| ADR-016 | Chart palette, solved per brand | AcceptedClaude recommended; Anuj delegated the call ("do whatever is correct", 2026-09-27). Accepted because every tenant passes the dataviz validator and all 2,000 fuzz palettes pass |
| ADR-017 | Soft-outline fields that still meet WCAG 1.4.11 | Accepteddecided by Anuj (Claude recommended "soft outline, still AA"; Anuj accepted) |
| ADR-018 | Position Syntara on published evidence, and widen Phase 5 to match | Accepteddecided by Claude recommended, Anuj accepted |
| ADR-019 | Mobile story is a server-driven UI schema plus native tokens, not native components | Accepteddecided by Claude recommended, Anuj accepted |
| ADR-020 | Add a Hindi tenant with per-script type tokens | Accepteddecided by Claude recommended, Anuj accepted. Tenant name, industry and brand inputs are pending Anuj. |
| ADR-021 | Button's status moves to tone; deprecated APIs are removed at 1.0 only | Accepteddecided by Claude recommended, Anuj accepted. Review times in GOVERNANCE.md §3: Claude proposed, Anuj accepted (2026-09-27). |
| ADR-022 | How the drift score is computed, and how the agent eval stays honest | AcceptedClaude recommended, Anuj accepted at the Phase 5 review (2026-09-28). Eval size and model: Anuj (2026-09-27). |
| ADR-023 | The server-driven UI contract: what crosses the wire, and what a client does with the unknown | AcceptedAnuj delegated the call ("fix all of these, do what is correct", 2026-09-28); Claude decided. The scope (a schema, a validator and one web renderer; no native renderer) is ADR-019, which Anuj accepted. |
| ADR-024 | Mukta for Devanagari, and type tokens set by measurement | ProposedClaude recommended, pending Anuj. The tenant (Haat, reseller commerce, #B5179E and #F48C06) and who reviews the Hindi are Anuj's decisions (2026-09-28). |
| ADR-025 | Native token files keep the engine's exact colours, and say what wasn't compiled | Accepted for the approachClaude recommended, Anuj accepted (own exporters, 2026-09-28). The API shape of the generated files is Claude recommended, pending Anuj. |
| ADR-026 | A style that restyles a component must outweigh it; stylesheet order decides nothing | Accepted for the docs siteClaude (pending Anuj's review). Cascade layers for the package are a proposal for a later RFC. |
| ADR-027 | Tooltips swap instantly, and stay open through the scroll that focus causes | Acceptedinstant swap: Claude, delegated by Anuj (2026-09-28, "whatever you feel is best"). Focus scroll: Anuj. |
| ADR-028 | Segmented controls wrap, Key is exported, and good or bad is never colour alone | AcceptedAnuj delegated the call ("fix all of these, do what is correct", 2026-09-28); Claude decided. Anuj reviews the visuals. |
| ADR-029 | The project is called Syntara; the package scope and token prefix change with it | AcceptedAnuj. |
| ADR-030 | The docs site is a static export, and /themes reads the address on the client | AcceptedAnuj chose Cloudflare and the static export; Claude implemented it and decided the details below. |
| ADR-031 | Line heights belong to the type pair, and every value is measured | AcceptedAnuj asked for the clipping to be fixed; Claude measured and chose the values. |
| ADR-032 | Initials take the letter, date placeholders come from Intl, and a clip box gets room | AcceptedClaude (pending Anuj's review). The initials rule is a judgement about how Hindi names read and is the one to overrule if he disagrees. |
| ADR-033 | Compact numbers are pinned to the build, and the figure renders as one node | AcceptedAnuj chose to pin to the build; Claude found the cause and decided the node-merging that makes it work. |
How a change gets in
- Issue: describe the need, with the screen it’s for.
- RFC, when one is needed: the proposal, the alternatives, and what it costs every tenant.
- Design review: a designer signs off on the API, states and tokens.
- Build: component,
meta.json, examples and tests together. - Docs: the component page is generated from
meta.json, so docs ship with the code. - Release: a changeset describes the change for consumers; Changesets versions and writes the changelog.
An RFC is needed for a new component, a breaking change, a deprecation, or a change to what the theme engine guarantees. A fix, a new prop that fits a component’s purpose, or a docs change doesn’t need one.
| RFC | Change | Status | Decided by |
|---|---|---|---|
| RFC-001 | Button gets tone; variant="danger" is deprecated | Accepted | Claude recommended, Anuj accepted |
Extend, vary, add or override
A request ends in one of four outcomes, cheapest first:
- Extend an existing component — a new prop that fits its purpose.
- Add a variant — same component, a new visual style every tenant can use.
- Add a component — a new job no existing component does. Needs an RFC.
- Local override — a one-off in the product, not the system.
Versioning and deprecation
Packages follow semver through Changesets.
- Deprecate in a minor release. The old API keeps working and renders exactly as before.
- Remove in the next major release.
- Before 1.0, semver lets a minor release break things. Syntara doesn’t use that: a deprecated API keeps working through every 0.x release and is removed at 1.0.0.
- Every breaking change ships with a codemod. A codemod rewrites what it can be sure of and reports the rest with file and line. It never guesses.
- Alpha components are exempt. Beta and stable components change only through this policy.
Removing or renaming a component, prop, value or token is breaking. So is changing a default, or a data-* attribute that a consumer could target in CSS.
What a deprecation includes
| Part | How it’s checked |
|---|---|
| An accepted RFC. | pnpm check:meta (the file exists) |
A record in meta.json: since, removal, replacement, reason, codemod and RFC. | pnpm check:meta. Removal must be a later major release. |
| A notice on the component’s page. | Generated from the record. |
| A warning in development, once, and never in production. | The component’s tests. |
| A codemod with fixture tests. | pnpm --filter @syntara/codemods test |
| The codemod run on this repo. | The pull request’s diff. |
| A changeset and a changelog entry. | Review. |
The first one
Button was the only component that put a status in variant; every other component uses tone. So variant="danger" became tone="danger", which also works on outline and ghost buttons. See it on the Button page.
// Deprecated in 0.2.0, removed in 1.0.0
<Button variant="danger">Delete card</Button>
// Use this instead
<Button tone="danger">Delete card</Button>npx @syntara/codemods button-variant-danger-to-tone srcThe brief named the new value critical. It became danger because twelve components and the feedback.danger.* tokens already say danger (ADR-021).
Maturity
Every component page shows a maturity badge: alpha, beta or stable. The badge is a claim about the component, and each claim has written criteria. pnpm check:meta checks every criterion a script can check and fails the build when a beta or stable component misses one. The rest are browser checks or a dated review recorded in the component’s meta.json. Every level needs everything from the level below it.
Alpha: new, and the API may change
The API can change in any release, without a deprecation.
| Criterion | How it’s checked |
|---|---|
A complete meta.json: props, examples, keyboard, notes, do and don’t, tokens. | pnpm check:meta |
| A test file that passes. | pnpm check:meta (the file exists) and pnpm test |
If meta.json lists keyboard interactions, at least one test presses keys. | pnpm check:meta. It checks that keys are pressed, not that every listed key is covered. |
| At least three examples. | pnpm check:meta |
| Zero axe violations on its docs page, light and dark. | node scripts/axe-sweep.mjs |
| Renders in all six tenants (Vela, Harbor, Qamar right to left, Care, Haat and the house theme), light and dark. | The playground and scripts/screenshots.mjs, reviewed by eye. |
Alpha is the floor, so a component that misses an alpha criterion stays alpha. pnpm check:meta prints each gap as a note on every run until someone closes it.
Beta: used in a real screen
The API changes only through a deprecation: deprecate in a minor release, remove in the next major, with a codemod (see Versioning and deprecation above).
| Criterion | How it’s checked |
|---|---|
| Everything in alpha. | As above. |
| Used in at least one block or the homepage showcase. | pnpm check:meta scans their @syntara/react imports. |
| Someone has decided the API is settled. | A person decides. Meeting the checks makes a component eligible, not beta: pnpm check:meta lists alpha components that meet beta. |
Stable: frozen under semver
The API only breaks in a major release.
| Criterion | How it’s checked |
|---|---|
| Everything in beta. | As above. |
| Published on npm. | pnpm check:meta rejects stable while @syntara/react is unpublished. |
| A manual accessibility review: keyboard and a screen reader, by a person. | The date goes in meta.json as review.a11y. pnpm check:meta requires it. |
| Used in production by at least one product. | Recorded in the RFC that proposes the move. |
| One release with no breaking change to it. | The changelog. |
@syntara/react isn’t published on npm, so no component can be stable, and pnpm check:meta fails if one claims to be.
Why the manual review sits at stable
Axe and keyboard tests are automated, so they’re part of alpha. No person has run a screen-reader review on any component yet. Requiring one for beta would make every component alpha, and recording one from an automated run would be false. So beta rests on the automated checks and real use in a screen, and stable adds the manual review. review.a11y stays empty until a review actually happens.
Agent trust levels
AI agents change code too, so how much they may do alone depends on how far a change reaches and how easily it’s undone (ADR-008):
| Level | Agents may | People |
|---|---|---|
| Ambient | Fix token drift in consumer code, e.g. a raw hex → the matching token. | See it in the diff. |
| Soft gate | Open pull requests for docs, meta.json and stories. | Approve. |
| Hard gate | Propose new components, token-tier changes and breaking changes. | RFC, design review and merge. Agents can’t merge. |
Not enforced yet: CODEOWNERS, branch protection and the drift auditor arrive with the MCP server. Until then this is a working agreement, kept by recording who decided in every decision record, RFC and log entry.