speckl/docs/compilation-layer.md
Marcus (agent-cos) 34b8de549f
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
docs: GP architecture — SIR schema, extraction protocol, validation engine, compilation layer
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).
2026-05-09 23:46:57 -07:00

42 KiB
Raw Blame History

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:

  1. SIR is hash-addressed: Every SIR gets a sha256 hash on validation
  2. Compiler is version-pinned: Target compilers declare their exact version in the SIR
  3. No ambient state: Compilation reads nothing from the environment — no Date.now(), no Math.random(), no OS locale
  4. 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 transitions
  • prov-o — Provenance ontology from state history
  • spdx — License compliance from component states
  • wasm — Executable state machine runtime

Future Targets:

  • openapi — API specification from interface SIR
  • json-schema — Data validation schemas
  • cloudformation / terraform — Infrastructure as code from deployment SIR
  • kubernetes-crd — Custom resource definitions
  • solana-program / evm-contract — Smart contracts from protocol SIR
  • github-actions — CI/CD pipelines from workflow SIR
  • opa-rego — Policy rules from constraint SIR
  • protobuf — 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 S and compiler version C, 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:

  1. Cross-platform execution: The same state machine runs in browser, server, edge
  2. Deterministic execution: WASM is deterministic by design (no floats with NaN boxing)
  3. Compact: Binary format, suitable for on-chain / embedded use
  4. 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

  1. Pure functional compilation: The compiler is a function (SIR, TargetConfig) → CompilationResult. No side effects guarantee determinism.

  2. Content-addressing over versioning: Artifacts aren't "version 1.2.3" — they're sha256:abc123.... You know exactly what you have.

  3. Gap-tolerant by design: The compiler handles incomplete SIRs gracefully with stubs, debt tracking, and readiness levels. Works from conversation day 1.

  4. Plugin architecture for targets: New targets are standalone modules. The compiler core doesn't know about CycloneDX or Terraform — it knows about the CompilerTarget interface.

  5. Manifest as proof: Every compilation produces cryptographic proof of what was compiled, by whom, from what. The manifest is the chain of custody.

  6. Incremental over rebuild: Large SIRs with many entities benefit from partial recompilation. The dependency map enables surgical updates.

  7. Bundle as distribution unit: A .speckl.bundle.json contains all artifacts and the manifest. Self-contained, verifiable, distributable.