LogoCyanPrint

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:

template.ts
// This would be different every time!
const id = crypto.randomUUID();
const now = Date.now();
template.py
# This would be different every time!
import uuid
import time
id = str(uuid.uuid4())
now = int(time.time())
Template.cs
// 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):
@abstractmethod
def 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:

  1. First generation: If the key doesn't exist, calls origin() and stores the result
  2. Subsequent generations: Returns the cached value for that key

Usage Examples

Deterministic UUIDs

template.ts
import { startTemplateWithFn } from 'cyanprintsdk';
export default startTemplateWithFn(async (i, d) => {
// Safe to use crypto.randomUUID() - result is cached
const 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 }
}
}]
};
});
template.py
import uuid
from cyanprintsdk.main import start_template_with_fn
async def template(i: IInquirer, d: IDeterminism) -> Cyan:
# Safe to use uuid.uuid4() - result is cached
project_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)
Template.cs
using CyanPrintSDK;
public class Template
{
[TemplateMain]
public static async Task<Cyan> Run(IInquirer i, IDeterminism d)
{
// Safe to use Guid.NewGuid() - result is cached
var 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

example.ts
const createdAt = d.get('created-at', () => Date.now().toString());
const date = new Date(parseInt(createdAt)).toISOString();
example.py
import time
from datetime import datetime, timezone
created_at = d.get('created-at', lambda: str(int(time.time() * 1000)))
date = datetime.fromtimestamp(int(created_at) / 1000, tz=timezone.utc).isoformat()
Example.cs
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:

example.ts
const userId1 = d.get('user-id-1', () => '1');
const userId2 = d.get('user-id-2', () => '2');
const orderId1 = d.get('order-id-1', () => '1');
example.py
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')
Example.cs
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 state
cyanprint create my-org/template:1.0.0 ./my-project
# Update existing project - reuses pin
cyanprint update ./my-project

The deterministic states are stored in .cyan_state.yaml:

filename=".cyan_state.yaml"
templates:
myorg/template-name:
active: true
history:
- version: 1
time: '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 determinism
const id = crypto.randomUUID();
// Good - deterministic
const id = d.get('my-id', () => crypto.randomUUID());
# Bad - breaks determinism
id = str(uuid.uuid4())
# Good - deterministic
id = d.get('my-id', lambda: str(uuid.uuid4()))
// Bad - breaks determinism
var id = Guid.NewGuid().ToString();
// Good - deterministic
var id = d.Get("my-id", () => Guid.NewGuid().ToString());

Namespace Your Keys

Use descriptive, namespaced keys to avoid collisions:

// Good - namespaced keys
const 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 templates
const id = d.get('id', () => crypto.randomUUID());
# Good - namespaced keys
user_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 collide
id = d.get('id', lambda: str(uuid.uuid4()))
// Good - namespaced keys
var 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 collide
var 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.