Pulliv0.1
8px
10 — Governance

Made to be erased.

The fourth principle is a release policy, not a poem. A kolam lasts a day and is redrawn the next morning; the value is in the drawing, not the artefact. Systems that cannot remove things die of accretion, so this one schedules its own sweeping.

Versioning

Semantic versioning, with one breaking release a year and a name for it.

ChangeBumpExamples
Major1.0.0 → 2.0.0Removing a token or component; renaming a semantic role; changing the seed relationship; altering a default that shifts layout.
Minor1.0.0 → 1.1.0Adding a token, component or variant; deprecating something (with the warning, not the removal); new generator options with safe defaults.
Patch1.0.0 → 1.0.1Bug fixes; contrast corrections that keep the same role; documentation; test coverage.
The Margazhi release. One breaking version per year, published in December — the month when kolams are drawn most elaborately, every morning, across Tamil Nadu. Consumers plan one migration a year instead of watching for breakage continuously. The migration guide is written and published before the change lands, not after.

What counts as breaking

  • Any semantic token rename or removal. Semantics are the public API.
  • A primitive change that alters resolved values at the default pitch.
  • A component's default appearance changing enough to shift layout — a height, a padding, a radius step.
  • Generator output changing for a given seed. Determinism is a promise; if a fix must change output, it is a major version and the release notes say which seeds moved.

Not breaking: adding a token, adding a variant, tightening a contrast value within the same role, or any change confined to the palette layer that preserves every published contrast figure.

Contribution

One unusual rule, and it is the one that keeps the system coherent.

Derivation proof. Every pull request that introduces a value must show that value expressed in terms of P and r. A raw length literal is a review block, not a nit. If the value genuinely cannot be derived, the PR must say which external constraint forces it, land it in tokens.css with an inline comment, and add a row to the table in Lattice → where derivation breaks.

Pull request template

## What
One sentence.

## Derivation
| value | expression | at P=8 |
|-------|-----------|--------|
| height | 5.5 P     | 44px   |
| radius | 2 r       | 8px    |

## Layer
[ ] palette  [ ] primitive  [ ] semantic  [ ] component
References only the layer above it: yes / no

## Accessibility
Contrast measured: …
Target size at P=6: …
Reduced motion: …

## Sweep plan
What replaces this, and when, if it does not last.

Two layers only

Primitives and semantic aliases. There is no third layer of component-scoped tokens — no --button-padding-x. A component that needs its own token is a component whose spec has drifted from the system, and the correct fix is to bring it back rather than to give it a private vocabulary.

The generator is the source of truth

No hand-drawn SVG pattern assets enter the repository. They cannot be re-derived when the pitch or the palette changes, which means they cannot be swept and redrawn — and a pattern that cannot be redrawn is exactly what principle 04 exists to prevent.

Review criteria

What a reviewer checks, in order. The first failure ends the review — no point discussing colour if the layer direction is wrong.

#CheckFails if
1Layer directionThe change references a layer below itself, or a component references a palette or primitive value.
2DerivationA length literal appears outside tokens.css without a documented exception.
3Accessibility floorsContrast under 4.5 for text or 3.0 for non-text; target under 44px at P = 6; focus indicator removed or colour-only.
4Both themesThe change was only checked in one theme.
5All three densitiesLayout breaks at P = 6 or P = 10.
6Principle conflictThe change violates a principle without invoking the precedence table in Principles → when they conflict.
7Emphasis budgetThe change adds a second emphasised element to a view without removing one.
8Sweep planA component is added with no account of what would replace it.

Deprecation

Sweeping is scheduled, announced, and tooled. It is never a surprise and never indefinite.

The lifecycle

minor n

Marked. Deprecation notice in the docs, a console warning in development builds, and a codemod published the same day. The replacement must already exist.

minor n+1

Last call. The warning escalates and names the version that will remove it. Docs move the component to a "sweeping" section.

next major

Swept. Removed from the codebase entirely. No legacy directory, no compatibility shim, no flag to bring it back.

permanent

In the ledger. The entry stays in the removal ledger forever, with the date, the reasoning and the replacement. The record of what was swept is part of the system.

The removal ledger

A single page listing everything ever removed. It is the cheapest documentation in the system and the most useful in two years, when someone asks why there is no accordion.

## Swept

### `.btn-tertiary` — removed 2.0.0
Three button variants were carrying two levels of
priority. Ghost absorbed the use cases.
→ replacement: `.btn-ghost`
→ codemod: `npx pulli-codemod btn-tertiary`

### `--color-text-muted` — removed 2.0.0
Duplicated `--color-text-tertiary` at a different
contrast, and shipped below 4.5 twice.
→ replacement: `--color-text-tertiary`

Adoption tiers

Three ways to use this, each honest about what it gives you and what it costs.

Tier 1 · Pattern only

Take: kolam.js.

Get: generated marks, empty states and grounds inside your existing system.

Cost: 4 KB. You still owe the density limit and the cultural handling.

Tier 2 · Tokens

Take: tokens.css, map your components onto the semantic layer.

Get: derivation discipline, both themes, three densities, the measured contrast matrix.

Cost: a migration of every hardcoded value you own. This is the real work and it is worth doing once.

Tier 3 · Full system

Take: everything.

Get: twelve components with accessibility contracts, and the principles as a shared review vocabulary.

Cost: twelve components is a foundation, not a product library. You will build the other thirty and should expect to contribute them back.

Do not adopt tier 3 for an application that needs fifty components next quarter. Adopt tier 2, keep your own component layer, and take the pattern engine if it fits your brand. A system that oversells its coverage is how teams end up maintaining two libraries.

The verification harness

What is checked automatically, what it found the first time it was run in full, and why several of those things had been asserted on other pages for weeks before anything checked them.

A design system that publishes numbers has to be able to prove them. Five harnesses run over the whole system: the generator's geometry, the token graph, every contrast pair, the documented usage limits, and the link and document structure of all twenty-one pages. They are the reason the figures elsewhere in this documentation can be disagreed with rather than merely believed.

What each harness asserts

HarnessAssertionsResult
GeometryEvery arc has radius r = U/2 and its centre on a lattice dot; no open endpoints; deterministic output per seed; dot counts normalise to odd; cells = (n−1)²; boundary loops match the published formula; the draw-on never exceeds its cap; r = unit/2 for both frame styles at every unit.480 configurations · 71,354 arcs · 0 failures
Token graphEvery var() resolves; the palette is not referenced outside the token layer; no raw hex in product CSS; type does not scale with pitch; every density tier resolves; no dead tokens.141 defined · 131 consumed · 0 unresolved · 0 leaks
ContrastEvery text and non-text pair in five themes — light, dark by media query, dark by attribute, AAA high-contrast, and print — against bg, surface and surface-sunken.0 failures
LimitsNo CSS duration exceeds the motion cap; the cap and segment duration have exactly one source of truth; the inventory's group counts sum to the published denominator.0 breaches · 46 + 4 + 8 = 58
StructureAll internal links and anchors resolve; the sidebar is byte-identical on every page; exactly one aria-current="page" per document, pointing at itself; one h1 per page; no skipped heading levels.813 links · 21/21 pages · 0 failures

What it found

Eleven defects, on a system whose earlier rounds had each ended with a verification pass. They are listed in full because the pattern in them is more useful than the fixes.

DefectSeverityHow it survived
--color-border-strong at 2.63:1 on surface-sunken in dark, against a 3.0 requirement. This is the colour the pulli and the kambi are drawn in, and table headers use the sunken surface — so the sort indicator, the system's own example of state carried by shape, failed contrast on the surface it renders on.CriticalEvery previous contrast table tested against --color-bg only. Components do not sit on the page background.
--color-text-tertiary at 4.46 light / 4.14 dark on surface-sunken, against 4.5.CriticalSame blind spot.
The 44px target-size rule was documented as a condition of use, marked in the token table, listed in the audit checklist — and implemented nowhere. .btn-sm resolved to 32px at default pitch and 24px at compact with no hit-area expansion in the stylesheet.CriticalWriting a requirement down is not the same as enforcing it, and nothing checked.
--motion-cap was published as the ceiling and consumed by nothing; kolam.js restated 600ms as a literal. Two independent declarations of one rule.MajorThe token existed, so it looked wired up.
framePath() silently ignored unit for the curl style, reading only radius. A caller setting unit got a frame whose local pitch disagreed with the one they asked for, and the two frame styles could disagree with each other from a single call.MajorBoth defaults happened to give r = 16, so the bug was invisible at default settings.
Two palette leaks into product CSS — --maa-50 in .btn-danger, --mann-900 in .scrim — breaking the one-way layer dependency the system publishes as a rule.MajorTwo roles were missing from the semantic layer, so the palette was the only place to reach.
aria-current="page" on five in-page anchors across four pages, giving those documents more than one current item.MajorCopied from the sidebar pattern, where it was correct.
Heading levels skipped from h2 to h4 on twenty of twenty-one pages — 110 panel titles.Majorh4 was chosen for its default size rather than its level, on the first page, and copied everywhere after.
Two pages carried two page-level h1s.MinorA demo heading and a page heading in one document.
Six tokens defined and referenced by nothing.MinorLeft behind by earlier revisions.
The curl and scallop frames were seeded from different local pitches — 4P and 6P — from the same call site, with no stated reason.MinorTwo parameters were passed where one rule applies.

The pattern

Nine of the eleven are the same failure in different clothing: a rule that was written down and then not connected to anything that could check it. The 44px floor, the motion cap, the layer dependency, the heading hierarchy — each was stated as a principle on one page while the code went its own way on another. The two contrast failures are a variation: the check existed and ran clean, but it tested the wrong backgrounds, so it certified a system that failed on the surfaces components actually sit on.

This is worth being precise about, because the obvious lesson — write more tests — is the wrong one. Every one of these had documentation. What none of them had was a path from the documentation to an assertion. The correction is not more prose about correctness; it is that a claim in the documentation and a check in the harness are now required to ship together, and that is a rule in review criteria rather than an aspiration here.

The harness certified this system as sound four times before it found any of this. Each earlier round ended with a verification pass, and each pass was real — it checked the things it knew to check. The generator has been correct at every size and seed from the beginning and remains so. What moved was the scope of the checking, and the honest reading of that is not that the system is now verified, but that the previous four clean results meant less than they appeared to. A passing test suite is a statement about coverage, not about correctness.

Decision records

Short, dated, never edited. New decisions supersede old ones rather than overwriting them.

# DR-004 · Elevation has three levels, not six

Date: 2026-03-11
Status: accepted (supersedes DR-002)

## Context
Six elevation levels were proposed to match the component set.

## Decision
Three. Depth is carried primarily by surface stepping
(sunken / bg / surface); shadow is reserved for things
that genuinely float and can be dismissed.

## Reasoning
A kolam is flour on a floor and casts no shadow. More
practically, elevation ramps become a second unowned
hierarchy that competes with type and colour, and nobody
can say what level 4 means versus level 5.

## Consequences
Popovers and cards share --elevation-panel. Distinguishing
them is the job of position and border, not depth.
DRDecisionStatus
DR-001r = P/2 is fixed and forks may not change itaccepted
DR-002Six elevation levelssuperseded by DR-004
DR-003Type scale uses √2 and does not follow the pitchaccepted
DR-004Elevation has three levelsaccepted
DR-005Target size floors override derivation, inside the tokenaccepted
DR-006Dark is the native theme; light is the inversionaccepted
DR-007No component-scoped token layeraccepted

Roadmap

Stated as intentions with reasons, not as dates.

Next

  • Figma variable library mapping the three layers, with density and theme as modes.
  • Style Dictionary build emitting CSS, JSON, Swift and Kotlin from one source.
  • Density audit tooling — the pattern-coverage check as a CI script rather than a screenshot instruction.
  • Diamond-grid kolams in the generator. The tile logic holds; the boundary-closure proof needs redoing for a non-square perimeter.

Considered and declined

  • A React package. The CSS is framework-agnostic on purpose; a component wrapper would halve its audience and double the maintenance.
  • More elevation levels. See DR-004.
  • Coloured kolams. Multi-colour powder forms are festival-specific and land squarely in the sacred set the generator avoids.
  • An icon set. Out of scope, and there are good ones already. Pulli specifies icon sizing, not icon drawing.

Credit & licence

The code is MIT. Take it, fork it, ship it commercially, no attribution required for the software.

The practice is not licensed by anyone here. Kolam belongs to the households and communities who draw it, and has for far longer than any of this. What the licence cannot require, the documentation asks for: name the source, keep the glosses, avoid the sacred set, and route credit and resources back where you can.

Research credit. The observation that kolams form a generable picture language — that a formal grammar can produce them — comes from Gift Siromoney, Rani Siromoney and Kamala Krithivasan at Madras Christian College in the 1970s. The full account — including Siromoney's own, notably conservative reading of the tradition's age — is in 01 · Origins. This engine is a small applied consequence of that work and would not exist without it.

If you ship this

  • Say in your own README that the visual language derives from kolam practice.
  • Keep the Tamil terms and their glosses. Renaming them to English erases exactly the thing worth crediting.
  • Read Accessibility → cultural handling before deciding this is right for your product.
  • If your product has no connection to the tradition, say that out loud in your design review. It may still be the right call — but it should be a call, not a default.