Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
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).
1317 lines
42 KiB
Markdown
1317 lines
42 KiB
Markdown
# 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.
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
// 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:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
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.
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
// 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:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```json
|
||
{
|
||
"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)
|
||
|
||
```typescript
|
||
// 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:
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
// 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.
|
||
|
||
```typescript
|
||
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.
|