Pin Determinism
How to pin determinism for reproducible template generation
How to Pin Determinism
Pin determinism to ensure reproducible template generation. This is essential for updating existing projects.
The Determinism Problem
Templates often need to generate values like UUIDs or timestamps that would normally change on every run:
// This would be different every time!const id = crypto.randomUUID();const now = Date.now();
# This would be different every time!import uuidimport timeid = str(uuid.uuid4())now = int(time.time())
// This would be different every time!var id = Guid.NewGuid().ToString();var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
When updating a previously generated project, new IDs would break all references.
Solution: Use IDeterminism
The d parameter (implementing IDeterminism) caches values on first generation and reuses them on updates.
The Interface
interface IDeterminism {get(key: string, origin: () => string): string;}
class IDeterminism(ABC):@abstractmethoddef get(self, key: str, origin: Callable[[], str]) -> str:pass
interface IDeterminism{string Get(string key, Func<string> origin);}
How It Works
The get() method implements a cache-through pattern:
- First generation: If the key doesn't exist, calls
origin()and stores the result - Subsequent generations: Returns the cached value for that key
Usage Examples
Deterministic UUIDs
import { startTemplateWithFn } from 'cyanprintsdk';export default startTemplateWithFn(async (i, d) => {// Safe to use crypto.randomUUID() - result is cachedconst projectId = d.get('project-id', () => crypto.randomUUID());const apiKey = d.get('api-key', () => crypto.randomUUID());return {processors: [{name: 'cyan/default',files: [],config: {vars: { projectId, apiKey }}}]};});
import uuidfrom cyanprintsdk.main import start_template_with_fnasync def template(i: IInquirer, d: IDeterminism) -> Cyan:# Safe to use uuid.uuid4() - result is cachedproject_id = d.get('project-id', lambda: str(uuid.uuid4()))api_key = d.get('api-key', lambda: str(uuid.uuid4()))return Cyan(processors=[CyanProcessor(name='cyan/default',files=[],config=ProcessorConfig(vars={'projectId': project_id, 'apiKey': api_key}))])template_main = start_template_with_fn(template)
using CyanPrintSDK;public class Template{[TemplateMain]public static async Task<Cyan> Run(IInquirer i, IDeterminism d){// Safe to use Guid.NewGuid() - result is cachedvar projectId = d.Get("project-id", () => Guid.NewGuid().ToString());var apiKey = d.Get("api-key", () => Guid.NewGuid().ToString());return new Cyan{Processors = new[]{new CyanProcessor{Name = "cyan/default",Files = new List<CyanFile>(),Config = new ProcessorConfig{Vars = new Dictionary<string, string>{["projectId"] = projectId,["apiKey"] = apiKey}}}}};}}
Deterministic Timestamps
const createdAt = d.get('created-at', () => Date.now().toString());const date = new Date(parseInt(createdAt)).toISOString();
import timefrom datetime import datetime, timezonecreated_at = d.get('created-at', lambda: str(int(time.time() * 1000)))date = datetime.fromtimestamp(int(created_at) / 1000, tz=timezone.utc).isoformat()
var createdAt = d.Get("created-at", () => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());var date = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(createdAt)).ToString("o");
Sequential IDs
For sequential numbers, use unique keys for each:
const userId1 = d.get('user-id-1', () => '1');const userId2 = d.get('user-id-2', () => '2');const orderId1 = d.get('order-id-1', () => '1');
user_id_1 = d.get('user-id-1', lambda: '1')user_id_2 = d.get('user-id-2', lambda: '2')order_id_1 = d.get('order-id-1', lambda: '1')
var userId1 = d.Get("user-id-1", () => "1");var userId2 = d.Get("user-id-2", () => "2");var orderId1 = d.Get("order-id-1", () => "1");
Automatic Pin Handling
CyanPrint automatically handles pin persistence:
# First generation - stores statecyanprint create my-org/template:1.0.0 ./my-project# Update existing project - reuses pincyanprint update ./my-project
The deterministic states are stored in .cyan_state.yaml:
templates:myorg/template-name:active: truehistory:- version: 1time: '2024-01-15T10:30:00Z'answers:project-name: 'my-project'deterministic_states:project-id: '550e8400-e29b-41d4-a716-446655440000'created-at: '1704067200000'
Best Practices
Always Use d.get for IDs
Never use Date.now(), Math.random(), or crypto.randomUUID() directly - always wrap them in d.get(). Direct calls break reproducibility.
// Bad - breaks determinismconst id = crypto.randomUUID();// Good - deterministicconst id = d.get('my-id', () => crypto.randomUUID());
# Bad - breaks determinismid = str(uuid.uuid4())# Good - deterministicid = d.get('my-id', lambda: str(uuid.uuid4()))
// Bad - breaks determinismvar id = Guid.NewGuid().ToString();// Good - deterministicvar id = d.Get("my-id", () => Guid.NewGuid().ToString());
Namespace Your Keys
Use descriptive, namespaced keys to avoid collisions:
// Good - namespaced keysconst userId = d.get('my-template.users.primary-id', () => crypto.randomUUID());const orderId = d.get('my-template.orders.first-id', () => '1');// Avoid - generic keys might collide with other templatesconst id = d.get('id', () => crypto.randomUUID());
# Good - namespaced keysuser_id = d.get('my-template.users.primary-id', lambda: str(uuid.uuid4()))order_id = d.get('my-template.orders.first-id', lambda: '1')# Avoid - generic keys might collideid = d.get('id', lambda: str(uuid.uuid4()))
// Good - namespaced keysvar userId = d.Get("my-template.users.primary-id", () => Guid.NewGuid().ToString());var orderId = d.Get("my-template.orders.first-id", () => "1");// Avoid - generic keys might collidevar id = d.Get("id", () => Guid.NewGuid().ToString());
Why This Enables Safe Updates
Pinned values enable safe 3-way merges:
- Base - Original generated files
- Ours - User modifications
- Theirs - New template version
Because IDs remain consistent, references don't break when merging updates.
Related
- Determinism Explained - Deep dive into how determinism works
- 3-Way Merge - Update mechanics
- Client State - State management