Primitive spec

Radiant

Git-backed, YAML-defined configuration assets. Radiant is where every other primitive looks up the rules, schemas, templates, and policies it operates against.

What it owns

Radiant centralises four jobs: registering Git repositories the tenant trusts as a source of configuration, registering individual YAML assets out of those repositories, validating those assets against schemas, and resolving stable references to a specific effective version at runtime. It is the only primitive that talks to Git.

Everything declarative on the platform — pricing rules, cancellation policies, stage flow definitions, message templates, vendor configs, frontend view definitions — lives in Radiant. Other primitives carry an opaque radiantAssetId and call resolve when they need the parsed payload.

Concepts

Repository
A registered Git remote the tenant can read assets from. Tracks credentials, default branch, and sync state. Soft-deletable; soft-delete leaves dependent assets unresolvable rather than cascading.
Asset
A logical YAML document inside a repository — identified by path plus schema kind. The asset is the stable identity; its content changes through versions.
Version
An immutable snapshot of an asset's parsed payload at a particular Git commit. Versions are append-only. Each version carries its source ref, the schema it was validated against, and the parsed payload.
Alias
A stable, human-readable name that resolves to either a pinned version (tag behaviour) or the latest version on a channel (branch behaviour). Aliases are how runtime code refers to "the production pricing rules" without baking in a version id.
Channel
A named track of an asset (main, staging, preview). Aliases follow a channel; resolve returns the head of the channel at request time.
Resolution
The single runtime entry point. Given an assetId + channel or an aliasName, return the effective version's id and parsed payload. Cached, validated, and tenant-scoped.
VersionProposal
A pull-request-style review on an asset version before it lands. Carries the proposed payload, the base version it's diffing from, the target channel, an author (employee or AI agent — same surface), and the count of approvals required. Status moves DRAFT → OPEN → APPROVED → MERGED, with CLOSED and SUPERSEDED branches.
ProposalComment
Threaded review comments on a proposal. Anchored by JSON Pointer (linePath) so a comment can point at a specific field inside the proposed payload; replies are threaded through parentId; resolution flag tracks whether the conversation is closed.
ProposalApproval
One operator's vote on a proposal. Unique per (proposal, approver); re-voting updates in place. When approvals required is met, the proposal auto-promotes to APPROVED; withdrawing or requesting changes flips it back to OPEN.
Merge
The atomic write that lands an approved proposal as a new AssetVersion on the target channel. Gated by a conflict check (the channel's current head must still equal the proposal's base version) and a sibling-sweep that flips any competing proposals to SUPERSEDED. Conflict returns 409 with the proposal flipped to SUPERSEDED.
Authorship & AI proposals
Every proposal records an authorKindHUMAN, AI, or SYSTEM. An AI agent opens one through the same Layer-1 API via draft-with-ai: a prompt plus a stable agentId go to the model, the returned payload is schema-validated, and a DRAFT proposal lands on the same review surface a human's would. The author is recorded; the approval gate is identical.
Audience & fork
Assets carry an audience: tenant_private (the owner-only default), tenant_public (the tenant's customer-facing surfaces), or platform (a LatticeKit-published reference asset every tenant can read). Audience is immutable after creation — to take a platform asset and make it yours, fork copies it into your tenant as a fresh tenant_private row.
Git bridge
Layer-2 mirrors proposals to a tenant's GitHub repository — materialize a proposal as a branch + pull request, ingest PR edits back, merge from either side — behind a GitHubClientInterface with a signature-verified webhook receiver at /radiant/v1/git-webhooks. Creating a proposal now enqueues a Messenger job that materializes it to git out-of-band, so the API never blocks on GitHub; it is gated per tenant by a provisioned GitSyncState row, and the real HTTP client supersedes the original in-memory one.
Bandit handoff
A proposal can carry a banditExperimentId. When it does, approving it does not merge — it starts a Branno experiment with the proposed payload as one arm. The proposal stays APPROVED until the experiment converges; if the variant wins, Branno calls back and the merge completes, and if control wins the proposal closes as "bandit lost".
ui_view templates & instances
A ui_view asset can be a template with a declared variables: block; a ui_view_instance binds values to it per tenant. Resolution is at render time — author once, instantiate per tenant: the resolve endpoint loads the instance, pins or follows its template via an Alias, fills the variables (typed and validated: string, integer, boolean, color, enum, ref), and returns a concrete ui_view for the render-runtime to draw.
Layouts, themes & the template library
A view_ref node embeds another ui_view (or instance) by id, so a layout is just a stack of view_refs composed from picked widgets. A theme asset binds a palette that resolves the render catalog's colour tokens, layerable per view. LatticeKit publishes a library of forkable platform templates — Home hero, About, Hours & location, Contact, Gallery, FAQ, Menu (which self-fetches the tenant's live Hardin menu), and the booking landing — that a tenant binds with its own variables (no fork) or forks to restructure. Platform-audience asset versions are readable cross-tenant, which is what makes "use a template without forking" resolve.
Live preview & CEL conditions
The ui_view editor's preview pane renders through the real render-runtime (the resolve endpoint takes an inline-template path for an unpublished draft), so it is WYSIWYG by construction and data-bound widgets show live, tenant-scoped data while you edit. The render if node now evaluates a real CEL expression — slots.size() > 0, context.user.authenticated, 'vip' in context.tags — rather than bare scope-ref truthiness; a broken condition hides its children and warns rather than failing the page.

API surface

All endpoints are versioned under /radiant/v1/, return RFC 7807 problem details on error, and read tenantId from the bearer token. Single-item by default; explicit /batch variants exist where genuine bulk demand exists.

Radiant endpoints in the Foundation API reference OpenAPI 3.1 schema for Radiant with request/response shapes, parameters, and a try-it client.

Quick reference

MethodPathPurpose
POST / GET/radiant/v1/repositoriesRegister or list Git repositories for the tenant.
GET / PUT / DELETE/radiant/v1/repositories/{id}Fetch, replace, or soft-delete a repository.
POST / GET/radiant/v1/assetsRegister a YAML asset out of a repository, or list assets.
POST/radiant/v1/assets/{id}/versionsPublish a new immutable version of an asset.
POST / GET / PUT/radiant/v1/aliasesManage stable names that resolve to a version or channel head.
POST/radiant/v1/resolveResolve an assetId + channel or aliasName to an effective version.
POST/radiant/v1/ui-views/resolveResolve a ui_view template + instance bindings into a concrete view at render time.
POST / GET/radiant/v1/assets/{id}/proposalsOpen a pull-request-style proposal on an asset, or list a tenant's proposals.
POST/radiant/v1/assets/{id}/proposals/draft-with-aiDraft a proposal authored by an AI agent (authorKind=AI) from a prompt.
POST/radiant/v1/proposals/{id}/approvalsCast or withdraw an approval; auto-promotes to APPROVED when the count is met.
POST/radiant/v1/proposals/{id}/mergeMerge an approved proposal — or hand it to Branno if it carries a banditExperimentId.
POST/radiant/v1/assets/{id}/forkCopy a platform asset into the caller's tenant as a private row.
POST/radiant/v1/validateValidate a payload against a registered schema before publication. Planned.

Example

Resolve the production pricing rules for salon appointment bookings:

POST /radiant/v1/resolve
Content-Type: application/json
Authorization: Bearer <token>

{
  "aliasName": "pricing.haircut.production"
}

Response (abbreviated):

{
  "assetId": "asset_01J8K2…",
  "versionId": "ver_01J9TQ…",
  "schemaKind": "seldon.pricing.v1",
  "sourceRef": "main@7c4a2e3",
  "payload": {
    "currency": "USD",
    "rules": [
      { "dim": "daypart", "value": "peak",     "windowAdjPct": 10 },
      { "dim": "daypart", "value": "off_peak", "windowAdjPct": 0 }
    ]
  }
}

How it fits with the rest

flowchart LR
  Git[Git / YAML] --> R(Radiant)
  R --> S[Seldon]
  R --> D[Daneel]
  R --> Sp[Speaker]
  R --> Other[other primitives]
  R -- bandit handoff --> Br[Branno]
  Br -- winning arm --> R
            

Radiant is referenced, not depended on, by every other primitive. Seldon offers carry a radiantAssetId pointing at their pricing and cancellation YAML. Speaker templates resolve through Radiant. Daneel workflow definitions live in Radiant. Frontend ui_view assets that the rendering runtime consumes are Radiant assets too.

Two newer edges close the config-as-code loop. The same review surface serves human and AI authors alike — Demerzel's performance-review agent drafts proposals as authorKind=AI. And when a proposal carries a banditExperimentId, the merge is deferred to Branno: the experiment runs, and its winning arm flows back as the merged version.

The cross-primitive contract is intentionally minimal: opaque id in, parsed payload out. No primitive needs to know how Radiant stores or syncs anything.