The four design docs from the GP architecture research thread. These define Speckl's general-purpose architecture: conversation → SIR → deterministic compile, with domain plugins for state machines, data schemas, and workflows. - sir-schema.md (48KB): Central IR data structure with ~20 interfaces, 8 domain element tables - extraction-protocol.md (47KB): How conversation populates the SIR - validation-engine.md (39KB): SIR validation before compilation - compilation-layer.md (43KB): How validated SIRs compile to deterministic artifacts Phase strategy: build GP core now, ship state machines first, layer data schemas and workflows on the same architecture (no rewrites).
42 KiB
Speckl Compilation Backend Architecture
Overview
The Compilation Layer is Speckl's superpower: it takes a validated SIR (Speckl Intermediate Representation) and produces deterministic, verifiable artifacts across multiple target formats. The pipeline:
Chaotic Conversation → [Extraction Layer] → [Validation Layer] → [Compilation Layer] → Artifacts
While the conversation is chaotic, the output is guaranteed deterministic. Same SIR + same compiler version = same artifact bytes, every time.
1. Compilation Model
Pure Functional Core
compile: SIR × TargetConfig × CompilerVersion → CompilationResult
No side effects. No network calls. No file system dependency. No randomness. The entire compilation is a pure function — this is what makes determinism possible.
interface CompilationResult {
// The compiled artifact
artifact: Artifact;
// Cryptographic proof of what was compiled
manifest: CompilationManifest;
// What the compiler couldn't compile
gaps: CompilationGap[];
// Readiness assessment
readiness: ReadinessLevel;
}
interface Artifact {
format: TargetFormat; // "cyclonedx", "prov-o", "spdx", "wasm"
mimeType: string; // "application/json", "application/wasm"
content: Buffer | string; // The actual artifact bytes
contentHash: string; // sha256 of content
size: number; // byte count
}
The Compilation Manifest
Every compilation produces a manifest — a cryptographic record of provenance:
interface CompilationManifest {
manifestVersion: "1.0.0";
// What went in
inputs: {
sirHash: string; // sha256 of the SIR
sirVersion: number; // SIR version counter
sirId: string; // UUID of the SIR
};
// What compiled it
compiler: {
name: "speckl-compiler";
version: string; // "2.3.1"
targets: string[]; // ["cyclonedx@1.5", "spdx@3.0"]
hash: string; // sha256 of compiler binary/config
};
// What came out
outputs: Record<TargetFormat, {
hash: string; // sha256 of the artifact
size: number;
timestamp: string; // ISO 8601, for humans only (not deterministic input)
}>;
// Integrity chain
manifestHash: string; // sha256 of this manifest (self-referential, computed last)
}
Hash-Locking for Determinism
Determinism is guaranteed through content-addressing:
- SIR is hash-addressed: Every SIR gets a
sha256hash on validation - Compiler is version-pinned: Target compilers declare their exact version in the SIR
- No ambient state: Compilation reads nothing from the environment — no
Date.now(), noMath.random(), no OS locale - Result verification: The manifest's output hash must match
sha256(artifact.content)— mismatch = compilation error
async function verifyReproducibility(
sir: SIR,
target: TargetConfig,
previousManifest: CompilationManifest
): Promise<VerificationResult> {
const result = await compile(sir, target);
const newHash = result.manifest.outputs[target.format].hash;
const oldHash = previousManifest.outputs[target.format].hash;
if (newHash !== oldHash) {
return {
reproducible: false,
expected: oldHash,
actual: newHash,
// The diff between SIR versions may explain this
sirChange: diffSir(previousManifest.inputs.sirHash, sir.hash),
};
}
return { reproducible: true };
}
The Dual BOM Pattern (Existing Speckl Convention)
Speckl already compiles state machines to dual BOMs: CycloneDX (SBOM) + PROV-O (provenance). The compilation layer generalizes this:
StateMachine SIR → compileToCycloneDX → CycloneDX 1.5 JSON
StateMachine SIR → compileToPROV-O → PROV-O RDF/Turtle
StateMachine SIR → compileToWASM → WASM binary
↑
One SIR, three targets, single compilation run
2. Target Abstraction
What Is a Compilation Target?
A compilation target consumes an SIR and produces an artifact in a specific format. It's an abstract interface that every target implements:
interface CompilerTarget {
// Identity
readonly id: TargetId; // "cyclonedx", "openapi", "terraform"
readonly version: string; // "1.5.0"
readonly format: TargetFormat; // "cyclonedx"
readonly mimeType: string; // "application/json"
// What domains of SIR can this target handle?
readonly supportedDomains: SIRDomain[];
// What does this target REQUIRE from the SIR?
readonly requirements: TargetRequirements;
// Compilation entry point
compile(sir: SIR, config: TargetConfig, context: CompilationContext): CompileOutput;
// Does this SIR have enough info for this target?
assessReadiness(sir: SIR): ReadinessAssessment;
// Generate stubs for missing information
generateStubs(gaps: CompilationGap[], sir: SIR): StubResult;
}
interface TargetRequirements {
// Required SIR sections
requiredSections: string[]; // ["entities", "transitions", "states"]
// Required entity types
requiredEntityTypes: string[]; // ["software-component", "process"]
// Required relationship types
requiredRelationshipTypes: string[];
// Minimum completeness threshold (0-1)
minCompletenessScore: number; // e.g., 0.8 = 80% of required fields present
}
Example Target Implementations
State Machine Targets (current):
cyclonedx— SBOM from state transitionsprov-o— Provenance ontology from state historyspdx— License compliance from component stateswasm— Executable state machine runtime
Future Targets:
openapi— API specification from interface SIRjson-schema— Data validation schemascloudformation/terraform— Infrastructure as code from deployment SIRkubernetes-crd— Custom resource definitionssolana-program/evm-contract— Smart contracts from protocol SIRgithub-actions— CI/CD pipelines from workflow SIRopa-rego— Policy rules from constraint SIRprotobuf— Protocol buffer definitions from data SIR
Target Registration
Targets register with the compiler via a plugin system:
class CompilerRegistry {
private targets: Map<TargetId, CompilerTarget> = new Map();
register(target: CompilerTarget): void {
const key = `${target.id}@${target.version}`;
if (this.targets.has(key)) {
throw new Error(`Target ${key} already registered`);
}
this.targets.set(key, target);
}
getTarget(id: TargetId, version?: string): CompilerTarget {
if (version) {
return this.targets.get(`${id}@${version}`)
?? this.throwNotFound(id, version);
}
// Return latest version
const versions = Array.from(this.targets.keys())
.filter(k => k.startsWith(`${id}@`))
.sort(semverDesc);
return this.targets.get(versions[0])
?? this.throwNotFound(id);
}
listTargetsForDomain(domain: SIRDomain): CompilerTarget[] {
return Array.from(this.targets.values())
.filter(t => t.supportedDomains.includes(domain));
}
}
Target-Specific Configuration
interface TargetConfig {
format: TargetFormat;
version?: string; // Pin to specific target version
options?: Record<string, unknown>; // Target-specific options
dialect?: string; // "cyclonedx:1.5" vs "cyclonedx:1.4"
}
// Example: CycloneDX with custom options
const cyclonedxConfig: TargetConfig = {
format: "cyclonedx",
version: "1.5.0",
options: {
serialNumber: "urn:uuid:3e56b1dc-...", // deterministic UUID
includeVulnerabilities: true,
componentTypes: ["application", "library", "container"],
},
dialect: "cyclonedx:1.5",
};
3. Domain-to-Target Mapping
Domain Capabilities
Not every SIR can compile to every target. The SIR has a domain that determines its capabilities:
enum SIRDomain {
StateMachine = "state-machine",
DataSchema = "data-schema",
APIInterface = "api-interface",
Deployment = "deployment",
Workflow = "workflow",
Protocol = "protocol",
Constraint = "constraint",
ServiceTopology = "service-topology",
LicenseCompliance = "license-compliance",
}
Each domain has a set of capabilities — the types of information it can encode:
| Domain | Encodes | Capabilities |
|---|---|---|
state-machine |
States, transitions, guards, actions | Entity lifecycle, event sequences, invariants |
data-schema |
Types, fields, constraints, relations | Data shape, validation rules, serialization |
api-interface |
Endpoints, methods, params, responses | HTTP contracts, auth schemes, error models |
deployment |
Resources, dependencies, configs | Infrastructure topology, scaling, networking |
workflow |
Steps, triggers, conditions, artifacts | CI/CD graphs, automation pipelines |
protocol |
Messages, participants, invariants | Communication contracts, consensus rules |
constraint |
Rules, subjects, effects | Policy definitions, access control |
service-topology |
Services, ports, dependencies | System architecture, fault domains |
license-compliance |
Components, licenses, obligations | SBOM data, license compatibility |
Target Requirements
Each target declares what it needs from an SIR to produce a valid artifact:
// A CycloneDX target needs entity information
const cyclonedxRequirements: TargetRequirements = {
requiredSections: ["entities", "relationships"],
requiredEntityTypes: ["software-component"],
minCompletenessScore: 0.6, // Can produce partial BOM from 60% complete SIR
};
// A Terraform target needs resource definitions
const terraformRequirements: TargetRequirements = {
requiredSections: ["entities", "dependencies", "configurations"],
requiredEntityTypes: ["compute-resource", "network-resource"],
requiredRelationshipTypes: ["depends-on", "exposes"],
minCompletenessScore: 0.8, // Need 80% for valid Terraform
};
// An OPA/Rego target needs rules
const opaRegoRequirements: TargetRequirements = {
requiredSections: ["rules", "subjects", "effects"],
requiredEntityTypes: ["policy-subject", "policy-action"],
minCompletenessScore: 0.7,
};
Compatibility Matrix
Domain → Target Mapping:
╔══════════════════════╦═════════╦══════════╦═══════════╦════════╦═════════════╗
║ Target ║ SM ║ Schema ║ API ║ Deploy ║ Workflow ║
╠══════════════════════╬═════════╬══════════╬═══════════╬════════╬═════════════╣
║ cyclonedx ║ ✓ ║ ✗ ║ ✗ ║ ✗ ║ ✗ ║
║ prov-o ║ ✓ ║ ✗ ║ ✗ ║ ✗ ║ ✗ ║
║ spdx ║ ✓ ║ ✗ ║ ✗ ║ ✗ ║ ✗ ║
║ wasm ║ ✓ ║ ✗ ║ ✗ ║ ✗ ║ ✗ ║
║ openapi ║ ✗ ║ ✗ ║ ✓ ║ ✗ ║ ✗ ║
║ json-schema ║ ✗ ║ ✓ ║ ✓ ║ ✗ ║ ✗ ║
║ cloudformation ║ ✗ ║ ✗ ║ ✗ ║ ✓ ║ ✗ ║
║ terraform ║ ✗ ║ ✗ ║ ✗ ║ ✓ ║ ✗ ║
║ kubernetes-crd ║ ✗ ║ ✗ ║ ✗ ║ ✓ ║ ✗ ║
║ evm-contract ║ ✓ ║ ✗ ║ ✗ ║ ✗ ║ ✗ ║
║ github-actions ║ ✗ ║ ✗ ║ ✗ ║ ✗ ║ ✓ ║
║ opa-rego ║ ✗ ║ ✗ ║ ✗ ║ ✗ ║ ✗ ║
║ protobuf ║ ✗ ║ ✓ ║ ✓ ║ ✗ ║ ✗ ║
╚══════════════════════╩═════════╩══════════╩═══════════╩════════╩═════════════╝
The compiler validates compatibility before starting:
function validateTargetCompatibility(sir: SIR, target: CompilerTarget): CompatibilityResult {
if (!target.supportedDomains.includes(sir.domain)) {
return {
compatible: false,
reason: `Target ${target.id} does not support SIR domain "${sir.domain}"`,
};
}
const missingRequirements = target.requirements.requiredSections
.filter(section => !sir.sections.has(section));
if (missingRequirements.length > 0) {
return {
compatible: false,
reason: `SIR missing required sections: ${missingRequirements.join(", ")}`,
};
}
// Check completeness threshold
const score = assessCompleteness(sir, target);
if (score < target.requirements.minCompletenessScore) {
return {
compatible: false,
reason: `SIR completeness (${score}) below target minimum (${target.requirements.minCompletenessScore})`,
};
}
return { compatible: true };
}
4. Incremental Compilation
Change Detection
When a conversation evolves, the SIR gets a new version. We need to know exactly what changed to avoid recompiling everything:
interface SIRDiff {
fromVersion: number;
toVersion: number;
fromHash: string;
toHash: string;
// What changed at each layer
sections: {
added: string[];
removed: string[];
modified: string[];
};
entities: {
added: EntityId[];
removed: EntityId[];
modified: EntityId[]; // Entity existed before, fields changed
};
transitions: {
added: TransitionId[];
removed: TransitionId[];
modified: TransitionId[];
};
// Affected targets: which targets need recompilation?
affectedTargets: TargetId[];
}
function diffSir(v1: SIR, v2: SIR): SIRDiff {
// Content-addressed comparison
const sectionDiff = {
added: [...v2.sections.keys()].filter(k => !v1.sections.has(k)),
removed: [...v1.sections.keys()].filter(k => !v2.sections.has(k)),
modified: [...v2.sections.keys()].filter(k =>
v1.sections.has(k) &&
hashSection(v1.sections.get(k)) !== hashSection(v2.sections.get(k))
),
};
// Entities: O(n) comparison with hash maps
const v1EntityHashes = new Map(
v1.entities.map(e => [e.id, hashEntity(e)])
);
const v2EntityHashes = new Map(
v2.entities.map(e => [e.id, hashEntity(e)])
);
const entityDiff = {
added: [...v2EntityHashes.keys()].filter(id => !v1EntityHashes.has(id)),
removed: [...v1EntityHashes.keys()].filter(id => !v2EntityHashes.has(id)),
modified: [...v2EntityHashes.keys()].filter(id =>
v1EntityHashes.has(id) &&
v1EntityHashes.get(id) !== v2EntityHashes.get(id)
),
};
// Determine which targets are affected
const affectedTargets = determineAffectedTargets(sectionDiff, entityDiff);
return {
fromVersion: v1.version,
toVersion: v2.version,
fromHash: v1.hash,
toHash: v2.hash,
sections: sectionDiff,
entities: entityDiff,
transitions: computeTransitionDiff(v1, v2),
affectedTargets,
};
}
Partial Compilation
Instead of recompiling everything, only recompile the changed portions:
async function incrementalCompile(
sirV2: SIR,
sirV1: SIR,
previousResults: Map<TargetId, CompilationResult>,
targets: TargetConfig[]
): Promise<Map<TargetId, CompilationResult>> {
const diff = diffSir(sirV1, sirV2);
const results = new Map(previousResults);
for (const target of targets) {
if (!diff.affectedTargets.includes(target.format)) {
// Target unaffected — reuse previous result
continue;
}
if (isTargetAdditiveOnly(diff, target)) {
// Only new entities added — merge, don't rebuild
const merged = await mergeCompilation(
previousResults.get(target.format)!,
sirV2,
diff.entities.added,
target
);
results.set(target.format, merged);
} else {
// Structural changes — full recompile for this target
const result = await compile(sirV2, target);
results.set(target.format, result);
}
}
return results;
}
Incremental Compilation Cache
interface CompilationCache {
// Content-addressed cache: hash(sir, target, version) → result
entries: Map<string, CompilationResult>;
// Dependency tracking: which SIR entities feed which target output sections
dependencyMap: Map<TargetId, Map<EntityId, OutputSection[]>>;
lookup(sir: SIR, target: TargetConfig): CompilationResult | null;
store(sir: SIR, target: TargetConfig, result: CompilationResult): void;
invalidate(sir: SIR, entities: EntityId[]): TargetId[];
}
5. Artifact Versioning and Integrity
Content-Addressable Artifacts
Every artifact is identified by its content hash, not a version number:
sir:abc123 → compile → artifact:def456 (sha256)
sir:abc123 → compile → artifact:def456 (same, always)
sir:abc124 → compile → artifact:789abc (different, because SIR changed)
Reproducibility Guarantee
The formal guarantee:
For any validated SIR
Sand compiler versionC,compile(S, C)always produces exactly the same bytes. This holds across machines, time, and operating systems.
Implementation contract:
// Compiler test suite includes reproducibility check
test("compilation is deterministic across runs", () => {
const sir = loadSir("test-fixtures/state-machine/order-processing.sir.json");
const runs = Array.from({ length: 100 }, () => compile(sir, cyclonedxConfig));
const hashes = runs.map(r => r.manifest.outputs["cyclonedx"].hash);
const uniqueHashes = new Set(hashes);
expect(uniqueHashes.size).toBe(1); // All 100 runs produce identical output
});
test("compilation is deterministic across platforms", async () => {
// Artifacts committed to repo, verified in CI on Linux, macOS, Windows
const sir = loadSir("test-fixtures/state-machine/payment-flow.sir.json");
const { stdout } = await exec("speckl compile --target cyclonedx");
expect(stdout.trim()).toBe(
fs.readFileSync("test-fixtures/expected/payment-flow.cdx.json", "utf-8").trim()
);
});
Manifest Verification
class ArtifactVerifier {
verify(manifest: CompilationManifest, artifact: Artifact): VerificationResult {
// 1. Hash check: does the artifact match its claimed hash?
const actualHash = sha256(artifact.content);
const claimedHash = manifest.outputs[artifact.format]?.hash;
if (actualHash !== claimedHash) {
return {
valid: false,
error: `Hash mismatch: claimed ${claimedHash}, actual ${actualHash}`,
};
}
// 2. Manifest self-consistency
const computedManifestHash = computeManifestHash(manifest);
if (computedManifestHash !== manifest.manifestHash) {
return {
valid: false,
error: "Manifest hash mismatch — manifest has been tampered with",
};
}
// 3. Compiler version is known and valid
if (!isKnownCompilerVersion(manifest.compiler.version)) {
return {
valid: false,
error: `Unknown compiler version: ${manifest.compiler.version}`,
};
}
return { valid: true };
}
// Can we verify an artifact was compiled from SIR X?
verifyProvenance(manifest: CompilationManifest, sir: SIR): boolean {
return manifest.inputs.sirHash === sir.hash;
}
}
6. Compilation with Gaps
The Gap Problem
Conversations are chaotic. The SIR will have gaps — missing fields, unspecified transitions, incomplete entity definitions. The compilation layer must handle this gracefully.
interface CompilationGap {
id: string;
severity: "error" | "warning" | "info";
location: {
section: string;
entity?: EntityId;
transition?: TransitionId;
field?: string;
};
description: string;
// What would fill this gap?
requiredInformation: string;
// What stub was generated in its place
stubGenerated: StubDescription;
// How this affects the compiled output
impact: GapImpact;
}
interface GapImpact {
// The artifact section(s) affected
affectedArtifactSections: string[];
// Is this a blocking gap?
blocksValidArtifact: boolean;
// Human-readable description of the impact
description: string;
}
Stub Generation
When information is missing, generate clear stubs with markers:
// Example: SIR has a state "processing" but no transition from "processing" to "shipped"
// The CycloneDX target generates:
// In compiled CycloneDX JSON:
{
"components": [
{
"name": "order-service",
"type": "application",
"bom-ref": "entity:order-service"
}
],
"dependencies": [
{
"ref": "entity:order-service",
"dependsOn": [
// ⚠️ STUB: Gap #GAP-42
"__speckl_stub__gap_GAP-42_missing_transition"
]
}
],
"metadata": {
"tools": [
{
"vendor": "Speckl",
"name": "Speckl Compiler",
"version": "2.3.1"
}
],
// ⚠️ SPECIFICATION DEBT
"properties": [
{
"name": "speckl:specification-debt",
"value": JSON.stringify([
{
"gapId": "GAP-42",
"severity": "error",
"description": "Missing transition: processing → shipped",
"requiredInformation": "Define what triggers the transition from 'processing' to 'shipped' and any guard conditions",
"blocksValid: true
}
])
}
]
}
}
Graduated Compilation Levels
Artifacts have a readiness level distinct from the SIR's completeness:
enum ReadinessLevel {
Draft = "draft", // < 40%: Conceptual, mostly stubs
Partial = "partial", // 40-60%: Key entities present, many stubs
Beta = "beta", // 60-80%: Usable for review, few stubs
Production = "production", // 80-95%: Valid artifact, minimal stubs
Complete = "complete", // 95%+: No gaps, fully valid
}
interface ReadinessAssessment {
level: ReadinessLevel;
score: number; // 0.0 - 1.0
gapCount: {
errors: number;
warnings: number;
info: number;
};
// Can this artifact be used for its intended purpose?
usability: {
canReview: boolean; // Is it readable by humans?
canValidate: boolean; // Passes format-specific validation?
canDeploy: boolean; // Could be deployed to production?
canAudit: boolean; // Would pass a compliance audit?
};
}
Specification Debt Tracking
"Specification debt" is tracked persistently in the compiled output:
{
"speckl:specification-debt": {
"totalGaps": 7,
"errors": 3,
"warnings": 4,
"readiness": "beta",
"debtItems": [
{
"id": "GAP-42",
"since": "2026-05-01T12:00:00Z",
"description": "Missing transition: processing → shipped",
"affects": ["cyclonedx:dependencies", "prov-o:wasGeneratedBy"],
"resolution": "Define the shipping trigger and guard conditions in the conversation"
}
]
}
}
7. Compiler Plugin Architecture
The CompilerTarget Interface (Full)
// Each target is a self-contained module implementing this interface
interface CompilerTarget {
// === Identity ===
readonly id: TargetId;
readonly version: string;
readonly format: TargetFormat;
readonly mimeType: string;
readonly displayName: string;
readonly description: string;
// === Domain Compatibility ===
readonly supportedDomains: SIRDomain[];
readonly requirements: TargetRequirements;
// === Primary Entry Points ===
compile(sir: SIR, config: TargetConfig, context: CompilationContext): CompileOutput;
assessReadiness(sir: SIR): ReadinessAssessment;
generateStubs(gaps: CompilationGap[], sir: SIR): StubResult;
// === Incremental Compilation ===
canIncrementalMerge(diff: SIRDiff): boolean;
mergeIncremental(previous: CompileOutput, diff: SIRDiff, sir: SIR): CompileOutput;
// === Validation ===
validateArtifact(artifact: Artifact): ValidationResult;
}
interface CompilationContext {
compilerVersion: string;
compilerHash: string;
sirHash: string;
timestamp: string; // For manifest only, NOT used in compilation
logger: CompilationLogger; // Structured logging, no side effects on artifact
}
interface CompileOutput {
artifact: Artifact;
manifest: CompilationManifest;
gaps: CompilationGap[];
readiness: ReadinessAssessment;
// Target-specific metadata for incremental merging
sections: Map<string, { inputEntities: EntityId[]; hash: string }>;
}
Target Registration and Discovery
Targets are discovered from the filesystem or loaded dynamically:
// File: targets/cyclonedx/index.ts
import { CompilerTarget, CompileOutput, CompilationContext } from "@speckl/compiler";
export const cyclonedxTarget: CompilerTarget = {
id: "cyclonedx",
version: "1.5.0",
format: "cyclonedx",
mimeType: "application/json",
displayName: "CycloneDX",
description: "OWASP CycloneDX Software Bill of Materials",
supportedDomains: [SIRDomain.StateMachine, SIRDomain.LicenseCompliance],
requirements: {
requiredSections: ["entities", "relationships"],
requiredEntityTypes: ["software-component"],
minCompletenessScore: 0.6,
},
compile(sir: SIR, config: TargetConfig, context: CompilationContext): CompileOutput {
// 1. Extract components from SIR entities
const components = sir.entities
.filter(e => e.type === "software-component")
.map(entityToComponent);
// 2. Extract dependencies from SIR relationships
const dependencies = sir.relationships
.filter(r => r.type === "depends-on")
.map(relationshipToDependency);
// 3. Map state transitions to lifecycle phases
const lifecyclePhases = sir.transitions?.map(transitionToLifecyclePhase) ?? [];
// 4. Build the CycloneDX document
const bom = buildCycloneDX15({
bomFormat: "CycloneDX",
specVersion: "1.5",
serialNumber: config.options?.serialNumber as string ?? generateDeterministicUUID(sir.hash, "cyclonedx-serial"),
version: 1,
components,
dependencies,
metadata: {
tools: [{
vendor: "Speckl",
name: "Speckl Compiler",
version: context.compilerVersion,
}],
properties: lifecyclePhases.map(phase => ({
name: `speckl:lifecycle:${phase.state}`,
value: JSON.stringify(phase),
})),
},
});
// 5. Identify gaps
const gaps = identifyGaps(sir, components, dependencies);
// 6. Generate stubs for gaps
if (gaps.length > 0) {
this.injectStubs(bom, gaps);
}
// 7. Compute artifact
const content = JSON.stringify(bom, null, 2);
const artifact: Artifact = {
format: "cyclonedx",
mimeType: "application/json",
content,
contentHash: sha256(content),
size: content.length,
};
// 8. Build manifest
const manifest = buildManifest(context, artifact, "cyclonedx");
// 9. Assess readiness
const readiness = assessReadinessFromGaps(gaps);
return {
artifact,
manifest,
gaps,
readiness,
sections: trackSections(sir, components, dependencies),
};
},
assessReadiness(sir: SIR): ReadinessAssessment { /* ... */ },
generateStubs: generateCycloneDXStubs,
canIncrementalMerge(diff: SIRDiff): boolean { /* ... */ },
mergeIncremental(prev, diff, sir) { /* ... */ },
validateArtifact: validateCycloneDX,
};
// Register
compilerRegistry.register(cyclonedxTarget);
Full CycloneDX Target Example
// Complete example: State Machine SIR → CycloneDX 1.5 JSON
// INPUT SIR (simplified):
const orderProcessingSIR: SIR = {
id: "sir:order-processing-v3",
version: 3,
hash: "sha256:abc123...",
domain: SIRDomain.StateMachine,
entities: [
{
id: "entity:order-api",
type: "software-component",
name: "Order API",
version: "2.1.0",
properties: {
language: "TypeScript",
framework: "Express",
license: "MIT",
},
},
{
id: "entity:payment-service",
type: "software-component",
name: "Payment Service",
version: "1.8.3",
properties: {
language: "Rust",
license: "Apache-2.0",
piiScope: "processes-credit-cards", // GAP: needs documentation
},
},
{
id: "entity:shipping-worker",
type: "software-component",
name: "Shipping Worker",
version: "0.9.0",
properties: {
language: "Go",
license: "MIT",
},
},
],
relationships: [
{
id: "rel:order-to-payment",
type: "depends-on",
source: "entity:order-api",
target: "entity:payment-service",
},
{
id: "rel:order-to-shipping",
type: "depends-on",
source: "entity:order-api",
target: "entity:shipping-worker",
},
],
transitions: [
{
id: "trans:created-to-paid",
from: "created",
to: "paid",
trigger: "payment-received",
guard: "payment.amount >= order.total",
},
{
id: "trans:paid-to-shipped",
from: "paid",
to: "shipped",
trigger: "shipping-label-created",
// GUARD MISSING — this is a gap
},
],
states: [
{ id: "created", initial: true },
{ id: "paid" },
{ id: "shipped", final: true },
],
};
// OUTPUT: CycloneDX 1.5 JSON
const compiledOutput = {
bomFormat: "CycloneDX",
specVersion: "1.5",
serialNumber: "urn:uuid:d5a7b3c1-e8f2-4a9d-b6c0-123456789abc",
version: 1,
metadata: {
timestamp: "2026-05-09T22:57:00Z",
tools: [{
vendor: "Speckl",
name: "Speckl Compiler",
version: "2.3.1",
}],
properties: [
{
name: "speckl:lifecycle:paid",
value: JSON.stringify({
transition: "trans:created-to-paid",
trigger: "payment-received",
guard: "payment.amount >= order.total",
}),
},
{
name: "speckl:lifecycle:shipped",
value: JSON.stringify({
transition: "trans:paid-to-shipped",
trigger: "shipping-label-created",
guard: "__speckl_stub__gap_GAP-001_missing_guard",
gapRef: "GAP-001",
}),
},
{
name: "speckl:specification-debt",
value: JSON.stringify({
totalGaps: 2,
errors: 1,
warnings: 1,
readiness: "beta",
debtItems: [
{
id: "GAP-001",
severity: "error",
description: "Missing guard condition on transition paid → shipped",
requiredInformation: "What condition must be true for an order to ship? (e.g., payment settled, inventory confirmed)",
blocksValid: false,
},
{
id: "GAP-002",
severity: "warning",
description: "Payment Service processes PII — needs documentation",
requiredInformation: "Document PII scope: what data, retention, encryption, access controls",
blocksValid: false,
},
],
}),
},
],
},
components: [
{
type: "application",
name: "Order API",
version: "2.1.0",
"bom-ref": "entity:order-api",
properties: [
{ name: "speckl:language", value: "TypeScript" },
{ name: "speckl:framework", value: "Express" },
],
},
{
type: "application",
name: "Payment Service",
version: "1.8.3",
"bom-ref": "entity:payment-service",
properties: [
{ name: "speckl:language", value: "Rust" },
{ name: "speckl:pii-scope", value: "processes-credit-cards" },
{ name: "speckl:gap-ref", value: "GAP-002" },
],
},
{
type: "application",
name: "Shipping Worker",
version: "0.9.0",
"bom-ref": "entity:shipping-worker",
properties: [
{ name: "speckl:language", value: "Go" },
],
},
// STUB: generated for the missing guard
{
type: "application",
name: "__speckl_stub__gap_GAP-001_missing_guard",
"bom-ref": "__speckl_stub__gap_GAP-001_missing_guard",
properties: [
{ name: "speckl:stub", value: "true" },
{ name: "speckl:gap-ref", value: "GAP-001" },
],
},
],
dependencies: [
{
ref: "entity:order-api",
dependsOn: ["entity:payment-service", "entity:shipping-worker"],
},
{
ref: "entity:payment-service",
dependsOn: [
"__speckl_stub__gap_GAP-001_missing_guard", // Linked to stub
],
},
],
};
Manifest for this compilation:
{
"manifestVersion": "1.0.0",
"inputs": {
"sirHash": "sha256:abc123def456...",
"sirVersion": 3,
"sirId": "sir:order-processing-v3"
},
"compiler": {
"name": "speckl-compiler",
"version": "2.3.1",
"targets": ["cyclonedx@1.5"],
"hash": "sha256:compiler-v2.3.1-hash..."
},
"outputs": {
"cyclonedx": {
"hash": "sha256:789fedcba...",
"size": 4521,
"timestamp": "2026-05-09T22:57:00Z"
}
},
"manifestHash": "sha256:manifest-hash..."
}
8. Output Formats and Packaging
Single Artifact vs. Bundle
Compilation can produce a single artifact or a bundle:
interface ArtifactBundle {
// Bundle metadata
bundleId: string;
bundleHash: string;
// The SIR this was compiled from
sir: {
id: string;
version: number;
hash: string;
};
// Compiler info
compiler: {
version: string;
hash: string;
};
// All compiled artifacts
artifacts: Record<TargetFormat, Artifact>;
// Shared manifest
manifest: CompilationManifest;
// Packaging metadata
created: string; // ISO 8601, for information only
format: "speckl-bundle@1.0";
}
Provenance Embedding
Every artifact embeds its own provenance. For formats that support metadata (CycloneDX, SPDX, PROV-O), the compilation manifest is embedded directly. For formats that don't (plain JSON, WASM), a sidecar manifest is generated:
output/
├── order-processing.cdx.json # CycloneDX with embedded provenance
├── order-processing.cdx.manifest.json # Standalone manifest
├── order-processing.wasm # WASM binary
├── order-processing.wasm.manifest.json # Sidecar manifest
└── order-processing.bundle.json # Full bundle with all artifacts
WASM as Universal Runtime Format
Speckl's current path compiles state machines to WASM, enabling:
- Cross-platform execution: The same state machine runs in browser, server, edge
- Deterministic execution: WASM is deterministic by design (no floats with NaN boxing)
- Compact: Binary format, suitable for on-chain / embedded use
- Sandboxed: Safe to execute untrusted state machines
// WASM compilation target
const wasmTarget: CompilerTarget = {
id: "wasm",
version: "1.0.0",
format: "wasm",
mimeType: "application/wasm",
supportedDomains: [SIRDomain.StateMachine],
requirements: {
requiredSections: ["states", "transitions"],
minCompletenessScore: 0.5,
},
compile(sir: SIR, config: TargetConfig, context: CompilationContext): CompileOutput {
// Generate a deterministic WASM module that:
// 1. Exports a state machine execution function
// 2. Accepts events as input
// 3. Returns new state + actions
const wasmModule = generateWasmStateMachine({
states: sir.states,
transitions: sir.transitions.map(t => ({
from: t.from,
to: t.to,
trigger: t.trigger,
guard: t.guard ? compileGuardToWasm(t.guard) : null,
actions: (t.actions ?? []).map(compileActionToWasm),
})),
// Embed SIR hash for verification
metadata: {
sirHash: context.sirHash,
compilerVersion: context.compilerVersion,
},
});
const content = wasmModule.toBytes();
return {
artifact: {
format: "wasm",
mimeType: "application/wasm",
content,
contentHash: sha256(content),
size: content.length,
},
// ... manifest, gaps, readiness
};
},
};
Self-Describing Artifacts
Can you reconstruct the SIR from the compiled artifact? Generally no — it's a lossy transformation. But some targets are more lossy than others:
| Target | Reversibility | Notes |
|---|---|---|
| CycloneDX | Partial | Entities and dependencies survive; state transitions lose semantics |
| PROV-O | High | Designed for provenance — much of the SIR structure survives |
| WASM | Low | Compiled to instructions; reconstruction is decompilation, not guaranteed |
| OpenAPI | Partial | Endpoint structure survives; validation layer intent doesn't |
| Terraform | Low | HCL is declarative; can infer resources but not the SIR abstractions |
The compilation manifest fills the gap — it records the SIR hash, so you can always look up the exact SIR that produced a given artifact. The bundle includes both, so nothing is lost.
function reconstructFromBundle(bundle: ArtifactBundle): {
sir: SIR; // Loaded from the SIR hash reference
artifacts: Record<TargetFormat, Artifact>;
manifest: CompilationManifest;
} {
// The bundle doesn't contain the SIR itself (that's stored separately)
// But it references it by hash — load it from the SIR store
const sir = sirStore.lookup(bundle.sir.hash);
if (!sir) {
throw new Error(`SIR ${bundle.sir.hash} not found in store`);
}
// Verify: does this SIR's hash match what the manifest says?
if (sir.hash !== bundle.inputs.sirHash) {
throw new Error("SIR hash mismatch — possible tampering");
}
return { sir, artifacts: bundle.artifacts, manifest: bundle.manifest };
}
Architecture Summary
┌─────────────────────────────────────────────────────────────┐
│ Compilation Layer │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Compiler Orchestrator │ │
│ │ - Validates SIR × target compatibility │ │
│ │ - Manages incremental compilation cache │ │
│ │ - Coordinates multi-target compilation │ │
│ │ - Builds the compilation manifest │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │CycloneDX │ │ PROV-O │ │ WASM │ ... │
│ │ Target │ │ Target │ │ Target │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Artifact Assembler │ │
│ │ - Bundles artifacts + manifest │ │
│ │ - Embeds provenance in output formats │ │
│ │ - Generates sidecar manifests when needed │ │
│ │ - Signs bundles (future: sigstore/cosign) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Invariants: │
│ ✓ Pure functional — no side effects │
│ ✓ Content-addressed — every artifact has a hash │
│ ✓ Reproducible — same SIR → same artifact, always │
│ ✓ Gap-tolerant — compiles what it can, marks the rest │
└─────────────────────────────────────────────────────────────┘
Key Design Decisions
-
Pure functional compilation: The compiler is a function
(SIR, TargetConfig) → CompilationResult. No side effects guarantee determinism. -
Content-addressing over versioning: Artifacts aren't "version 1.2.3" — they're
sha256:abc123.... You know exactly what you have. -
Gap-tolerant by design: The compiler handles incomplete SIRs gracefully with stubs, debt tracking, and readiness levels. Works from conversation day 1.
-
Plugin architecture for targets: New targets are standalone modules. The compiler core doesn't know about CycloneDX or Terraform — it knows about the
CompilerTargetinterface. -
Manifest as proof: Every compilation produces cryptographic proof of what was compiled, by whom, from what. The manifest is the chain of custody.
-
Incremental over rebuild: Large SIRs with many entities benefit from partial recompilation. The dependency map enables surgical updates.
-
Bundle as distribution unit: A
.speckl.bundle.jsoncontains all artifacts and the manifest. Self-contained, verifiable, distributable.