fix: test path resolution — use __dirname instead of process.cwd() (cycle 74)

This commit is contained in:
Scott Scoble 2026-06-29 19:09:47 -07:00
parent fa6f5bcf21
commit 857fa6ceb4
17 changed files with 430 additions and 416 deletions

View file

@ -1,6 +1,10 @@
import { describe, it, expect } from 'vitest';
import path from 'path';
import { fileURLToPath } from 'url';
import { parseSpeckContent, parseSpeckFile } from '../src/parser.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('SpeckDL Parser', () => {
describe('parseSpeckContent', () => {
it('should parse a minimal speck definition with name', () => {
@ -533,7 +537,7 @@ speck Second {
});
it('should parse full TigerBeetle speck from file', () => {
const ast = parseSpeckFile('./__tests__/tigerbeetle.speck');
const ast = parseSpeckFile(path.join(__dirname, 'tigerbeetle.speck'));
expect(ast.specks).toHaveLength(1);
const speck = ast.specks[0];

View file

@ -1,10 +1,13 @@
import { describe, it, expect, beforeAll } from 'vitest';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
import { parseSpeckFile } from '../parser.js';
import { generateProvenanceGraph } from '../generators/provenance.js';
import { generateCycloneDX } from '../generators/cyclonedx.js';
import { generateSPDX } from '../generators/spdx.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('Compiler E2E', () => {
let ast;
const testOutputDir = './test-output';
@ -15,7 +18,7 @@ describe('Compiler E2E', () => {
}
});
it('should parse RetryHandler.speck', () => {
const specPath = path.join(process.cwd(), '..', 'examples', 'RetryHandler.speck');
const specPath = path.join(__dirname, '..', '..', '..', 'examples', 'RetryHandler.speck');
ast = parseSpeckFile(specPath);
expect(ast).toBeDefined();
expect(ast.specks).toBeDefined();
@ -54,4 +57,164 @@ describe('Compiler E2E', () => {
expect(spdx.spdx).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Regression tests for Z3 / TypeScript / Rust expression translation fixes.
//
// These tests compile the FederatedMeetupProduct spec against each backend
// (z3, typescript, rust) and assert that the generated output contains the
// expected translated patterns. They guard against regressions in:
// - implies() argument preservation (Z3 + TS + Rust)
// - multi-level field access flattening (Z3)
// - len()/length() normalization (Z3)
// - nil → speckl_nil translation (Z3)
// - != → (distinct X Y) wrapping (Z3)
// - constraint checks emitting real every() / iter().all() (TS + Rust)
// - 'in' operator → .has() in verify expressions (TS)
// - multiple verify blocks all emitted (memberKey dedup fix) (TS + Rust)
// ---------------------------------------------------------------------------
const SPEC_PATH = '/home/sscoble/projects/federated-meetup/specs/FederatedMeetupProduct.speckdl';
const OUT_DIR = './out'; // CLI writes to ./out relative to compiler cwd
const COMPILER_DIR = '/home/sscoble/speckl/compiler';
describe('Z3 / TS / Rust expression translation regressions', () => {
let z3Output;
let tsOutput;
let rustOutput;
beforeAll(() => {
// Clean out directory to avoid stale output
execSync(`rm -rf ${OUT_DIR}`, { cwd: COMPILER_DIR });
execSync(`mkdir -p ${OUT_DIR}`, { cwd: COMPILER_DIR });
// Compile to all three targets
execSync(`npx tsx src/index.ts ${SPEC_PATH} --target z3 --output ${OUT_DIR}`, { cwd: COMPILER_DIR, stdio: 'pipe' });
z3Output = fs.readFileSync(path.join(COMPILER_DIR, OUT_DIR, 'FederatedMeetupProduct.smt2'), 'utf-8');
execSync(`npx tsx src/index.ts ${SPEC_PATH} --target typescript --output ${OUT_DIR}`, { cwd: COMPILER_DIR, stdio: 'pipe' });
tsOutput = fs.readFileSync(path.join(COMPILER_DIR, OUT_DIR, 'FederatedMeetupProduct.ts'), 'utf-8');
execSync(`npx tsx src/index.ts ${SPEC_PATH} --target rust --output ${OUT_DIR}`, { cwd: COMPILER_DIR, stdio: 'pipe' });
rustOutput = fs.readFileSync(path.join(COMPILER_DIR, OUT_DIR, 'FederatedMeetupProduct', 'src', 'lib.rs'), 'utf-8');
}, 120_000);
// 1. Z3: implies() preserves both arguments → (=> P Q) not (=> true true)
it('Z3: implies() emits (=> P Q) preserving both arguments', () => {
expect(z3Output).toContain('(=>');
expect(z3Output).toContain('(=> (= o_status Refunded) (distinct o_refunded_at speckl_nil))');
// Ensure we did NOT regress to (=> true true)
expect(z3Output).not.toContain('(=> true true)');
expect(z3Output).not.toContain('(=> true');
});
// 2. Z3: multi-level field access m.price.amount → m_price_amount
it('Z3: multi-level field access m.price.amount flattens to m_price_amount', () => {
expect(z3Output).toContain('m_price_amount');
// Ensure we did NOT regress to single-level m_price or bare m.price.amount
expect(z3Output).not.toContain('m.price.amount');
// The constraint m_price_amount >= 0 should be present
expect(z3Output).toContain('(>= m_price_amount 0)');
});
// 3. Z3: len() handled same as length() → speckl_len_Int
it('Z3: len() is translated to speckl_len_Int (same as length())', () => {
// The spec uses len(m.price.currency) — should become speckl_len_Int
expect(z3Output).toContain('(speckl_len_Int m_price_currency)');
// Ensure we did NOT regress to bare len() or length()
expect(z3Output).not.toMatch(/\blen\s*\(/);
expect(z3Output).not.toMatch(/\blength\s*\(/);
});
// 4. Z3: nil translated to speckl_nil
it('Z3: nil is translated to speckl_nil sentinel', () => {
expect(z3Output).toContain('(declare-const speckl_nil Int)');
expect(z3Output).toContain('speckl_nil');
// Ensure we did NOT regress to bare nil / null
expect(z3Output).not.toMatch(/\bnil\b/);
});
// 5. Z3: != produces (distinct X Y) not bare 'distinct'
it('Z3: != produces wrapped (distinct X Y) not bare distinct', () => {
// The constraint g.hosting_tier != Free should emit (distinct g_hosting_tier Free)
expect(z3Output).toContain('(distinct g_hosting_tier Free)');
// Ensure we did NOT regress to bare 'distinct g_hosting_tier Free' without parens
// (the fix wraps the operator in parens)
// A bare distinct would look like "g_hosting_tier distinct Free" without surrounding parens
expect(z3Output).not.toMatch(/[^(]\w+\s+distinct\s+\w+/);
});
// 6. TS: implies(p,q) → (!p || q) not (p || q)
it('TS: implies(p, q) translates to (!p || q), not (p || q)', () => {
// constraint 6: implies(o.status == Refunded, o.refunded_at != nil)
// Should become (!(o.status === ...) || (o.refunded_at !== ...))
expect(tsOutput).toContain('(!(');
expect(tsOutput).toContain('||');
// The specific pattern: (!(o.status === ... || (o.refunded_at !== nil))
expect(tsOutput).toMatch(/!\(o\.status === .* \|\| \(o\.refunded_at !== /);
// Ensure the CODE (not comments) has negation on p.
// A regression would have `return this.orders.every((o: any) => (o.status ===`
// (without the `!(` negation before `o.status`).
// Strip comments to avoid matching the original spec text in comments.
const tsNoComments = tsOutput.replace(/^\s*\/\/.*$/gm, '');
// Positive: the every() body starts with (!(o.status — the (! is the negation
expect(tsNoComments).toMatch(/every\(\(o:\s*any\)\s*=>\s*\(!\(o\.status/);
// Negative: a regression would have every((o: any) => (o.status without the ! negation
expect(tsNoComments).not.toMatch(/every\(\(o:\s*any\)\s*=>\s*\(o\.status/);
});
// 7. TS: constraint checks emit real every() not return true
it('TS: constraint checks emit real .every() calls, not return true', () => {
expect(tsOutput).toContain('.every(');
// Multiple constraints use every() — at least 8 constraints
const everyCount = (tsOutput.match(/\.every\(/g) || []).length;
expect(everyCount).toBeGreaterThanOrEqual(8);
// Ensure we did NOT regress to `return true` stubs
// A stub would have checkConstraintN() { return true; }
expect(tsOutput).not.toMatch(/checkConstraint\d+\(\):\s*boolean\s*\{\s*return\s*true\s*;\s*\}/);
});
// 8. TS: 'in' operator in verify expressions → .has() not raw 'in'
it('TS: "in" operator in verify expressions translates to .has(), not raw in', () => {
// V1: always(implies(event == PurchaseTicket, ticket_id in tickets))
// Should become tickets.has(ticket_id)
expect(tsOutput).toContain('.has(');
expect(tsOutput).toContain('tickets.has(ticket_id)');
// Ensure we did NOT regress to raw 'in' operator (JS `in` checks property keys, not membership).
// A regression would look like `ticket_id in tickets` in CODE (not comments).
// Strip comments to avoid matching the original spec text in comments.
const tsNoComments = tsOutput.replace(/^\s*\/\/.*$/gm, '');
expect(tsNoComments).not.toMatch(/\bticket_id\s+in\s+tickets\b/);
});
// 9. TS/Rust: multiple verify blocks all emitted (memberKey dedup fix)
it('TS: all 6 verify blocks are emitted (memberKey dedup fix)', () => {
// The spec has 6 verify blocks; previously dedup killed all but the first
expect(tsOutput).toContain('verify1');
expect(tsOutput).toContain('verify2');
expect(tsOutput).toContain('verify3');
expect(tsOutput).toContain('verify4');
expect(tsOutput).toContain('verify5');
expect(tsOutput).toContain('verify6');
expect(tsOutput).not.toContain('verify7'); // only 6 verify blocks in spec
});
it('Rust: all 6 verify blocks are emitted (memberKey dedup fix)', () => {
expect(rustOutput).toContain('verify_1');
expect(rustOutput).toContain('verify_2');
expect(rustOutput).toContain('verify_3');
expect(rustOutput).toContain('verify_4');
expect(rustOutput).toContain('verify_5');
expect(rustOutput).toContain('verify_6');
expect(rustOutput).not.toContain('verify_7');
});
// 10. Rust: constraint checks emit iter().all() not return true
it('Rust: constraint checks emit iter().all(), not return true stubs', () => {
expect(rustOutput).toContain('.iter().all(');
// Multiple constraints use iter().all() — at least 8 constraints
const iterAllCount = (rustOutput.match(/\.iter\(\)\.all\(/g) || []).length;
expect(iterAllCount).toBeGreaterThanOrEqual(8);
// Ensure we did NOT regress to `return true` stubs
expect(rustOutput).not.toMatch(/check_invariant_\d+\(&self\)\s*->\s*bool\s*\{\s*true\s*\}/);
});
// 11. Rust: implies() → (!p || q) in Rust
it('Rust: implies(p, q) translates to (!p || q) in Rust', () => {
// constraint 6: implies(o.status == Refunded, o.refunded_at != nil)
// Should become (!(o.status == ... || (o.refunded_at != ...))
expect(rustOutput).toContain('(!(');
expect(rustOutput).toMatch(/!\(o\.status == .* \|\| \(o\.refunded_at != /);
// Ensure the CODE (not comments) has negation on p.
// A regression would have `self.orders.iter().all(|o| (o.status ==`
// (without the `!(` negation before `o.status`).
// Strip comments to avoid matching the original spec text in comments.
const rustNoComments = rustOutput.replace(/^\s*\/\/.*$/gm, '');
// Positive: the iter().all() body starts with (!(o.status — the (! is the negation
expect(rustNoComments).toMatch(/iter\(\)\.all\(\|o\|\s*\(!\(o\.status/);
// Negative: a regression would have iter().all(|o| (o.status without the ! negation
expect(rustNoComments).not.toMatch(/iter\(\)\.all\(\|o\|\s*\(o\.status/);
});
});
//# sourceMappingURL=compiler.test.js.map

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
{"version":"2.1.9","results":[[":__tests__/parser.test.ts",{"duration":54.53141900000003,"failed":false}],[":src/__tests__/compiler.test.ts",{"duration":15.892710999999963,"failed":false}]]}
{"version":"2.1.9","results":[[":__tests__/parser.test.ts",{"duration":55.17420900000002,"failed":false}],[":src/__tests__/compiler.test.ts",{"duration":3350.1431570000004,"failed":false}]]}

View file

@ -1,29 +0,0 @@
{
"@context": {
"prov": "http://www.w3.org/ns/prov#",
"speckl": "https://speckl.scoble.me/ns/v0.2#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#"
},
"@graph": [
{
"@type": "prov:Entity",
"@id": "speck:SpecklCompiler",
"prov:qualifiedGeneration": {
"@type": "prov:Generation",
"prov:activity": {
"@type": "prov:Activity",
"prov:startTime": "2026-05-03T21:19:53.222Z"
}
},
"rdfs:label": "SpecklCompiler",
"prov:wasAttributedTo": {
"@id": "agent:sscoble"
}
},
{
"@type": "prov:Agent",
"@id": "agent:sscoble",
"rdfs:label": "sscoble"
}
]
}

View file

@ -1,30 +0,0 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [],
"metadata": {
"tools": [],
"component": {
"type": "application",
"name": "SpecklCompiler",
"version": "0.2.0",
"properties": []
}
},
"annotations": [
{
"subject": "speck:SpecklCompiler",
"annotations": [
{
"type": "tag",
"value": "provenanceType",
"text": "author"
}
],
"author": {
"name": "sscoble"
},
"timestamp": "2026-05-03T21:19:53.229Z"
}
]
}

View file

@ -1,42 +0,0 @@
{
"@context": [
"https://spdx.github.io/spdx-v3/spdx-context.jsonld",
{
"speckl": "https://speckl.scoble.me/ns/v0.2#"
}
],
"@type": "spdx:Document",
"spdx": {
"@id": "spdx:SpecklCompiler",
"@type": "spdx:Snippet",
"name": "SpecklCompiler",
"spdxVersion": "3.0.1",
"dataLicense": "CC0-1.0",
"creationInfo": {
"created": "2026-05-03T21:19:53.229Z",
"createdBy": [
"Organization: Speckl",
"tool: speckl-compile-0.2.0"
]
}
},
"builtBy": [],
"relationships": [
{
"from": {
"@id": "spdx:SpecklCompiler"
},
"to": {
"@id": "spdx:agent:sscoble"
},
"relationshipType": "spdx:wasAttributedTo"
}
],
"elements": [
{
"@type": "spdx:Person",
"@id": "spdx:agent:sscoble",
"name": "sscoble"
}
]
}

View file

@ -1,153 +0,0 @@
// Auto-generated by speckl-compile from SpecklCompiler
// DO NOT EDIT MANUALLY — regenerate from .speckdl source
export const SpecklCompilerCompilerPhase = {
BOMsGenerated: 'bomsgenerated',
Idle: 'idle',
Parsed: 'parsed',
Parsing: 'parsing',
ProvenanceGenerated: 'provenancegenerated',
TypeScriptGenerated: 'typescriptgenerated',
WASMGenerated: 'wasmgenerated',
} as const;
export type SpecklCompilerCompilerPhase = typeof SpecklCompilerCompilerPhase[keyof typeof SpecklCompilerCompilerPhase];
export const SpecklCompilerArtifactKind = {
CycloneDX: 'cyclonedx',
Done: 'done',
Provenance: 'provenance',
SPDX: 'spdx',
TypeScript: 'typescript',
WebAssembly: 'webassembly',
} as const;
export type SpecklCompilerArtifactKind = typeof SpecklCompilerArtifactKind[keyof typeof SpecklCompilerArtifactKind];
export interface CompilationStarted {
source: string;
output: string;
at: string;
}
export interface PhaseCompleted {
phase: SpecklCompilerCompilerPhase;
durationMs: number;
at: string;
}
export interface ArtifactGenerated {
kind: SpecklCompilerArtifactKind;
path: string;
bytes: number;
at: string;
}
export interface ErrorEmitted {
phase: SpecklCompilerCompilerPhase;
message: string;
line: number;
col: number;
at: string;
}
export class SpecklCompilerMachine {
phase: SpecklCompilerCompilerPhase;
sourcePath: String;
outputDir: String;
ast: any;
errors: String[];
artifacts: any;
decisionLog: CompilationStarted[] = [];
constructor() {
this.phase = SpecklCompilerCompilerPhase.Idle;
this.sourcePath = "";
this.outputDir = "./out";
this.ast = {};
this.errors = [];
this.artifacts = {};
}
compile(sourcePath: string, outputDir: string) {
if (!(this.phase === SpecklCompilerCompilerPhase.Idle)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.Idle');
if (!(sourcePath.length > 0)) throw new Error('Guard failed: sourcePath.length > 0');
this.decisionLog.push({
source: sourcePath,
output: outputDir,
at: Date.now()
} as CompilationStarted);
}
parse() {
if (!(this.phase === SpecklCompilerCompilerPhase.Parsing)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.Parsing');
if (!(this.sourcePath.length > 0)) throw new Error('Guard failed: this.sourcePath.length > 0');
this.decisionLog.push({
phase: SpecklCompilerCompilerPhase.Parsing,
durationMs: 0,
at: Date.now()
} as PhaseCompleted);
}
generateProvenance() {
if (!(this.phase === SpecklCompilerCompilerPhase.Parsed || this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.WASMGenerated)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.Parsed || this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.WASMGenerated');
this.decisionLog.push({
kind: SpecklCompilerCompilerPhase.Provenance,
path: join(this.outputDir,
bytes: 0,
at: Date.now()
} as ArtifactGenerated);
}
generateBOMs() {
if (!(this.phase === SpecklCompilerCompilerPhase.ProvenanceGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.ProvenanceGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed');
this.decisionLog.push({
kind: SpecklCompilerCompilerPhase.CycloneDX,
path: join(this.outputDir,
bytes: 0,
at: Date.now()
} as ArtifactGenerated);
this.decisionLog.push({
kind: SpecklCompilerCompilerPhase.SPDX,
path: join(this.outputDir,
bytes: 0,
at: Date.now()
} as ArtifactGenerated);
}
generateTypeScript() {
if (!(this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed');
this.decisionLog.push({
kind: SpecklCompilerCompilerPhase.TypeScript,
path: join(this.outputDir,
bytes: 0,
at: Date.now()
} as ArtifactGenerated);
}
generateWASM() {
if (!(this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed');
this.decisionLog.push({
kind: SpecklCompilerCompilerPhase.WebAssembly,
path: join(this.outputDir,
bytes: 0,
at: Date.now()
} as ArtifactGenerated);
}
report() {
if (!(this.phase === SpecklCompilerCompilerPhase.ProvenanceGenerated || this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.WASMGenerated)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.ProvenanceGenerated || this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.WASMGenerated');
this.decisionLog.push({
phase: SpecklCompilerCompilerPhase.Done,
durationMs: 0,
at: Date.now()
} as PhaseCompleted);
}
reset() {
}
}

View file

@ -1,151 +0,0 @@
;; Auto-generated by speckl-compile from SpecklCompiler
;; Compile: wat2wasm SpecklCompiler.wat -o SpecklCompiler.wasm
(module
;; Host imports for Map/Set/alloc
(import "speckl" "map_create" (func $map_create (result i32)))
(import "speckl" "map_set" (func $map_set (param i32 i32 i32)))
(import "speckl" "map_get" (func $map_get (param i32 i32) (result i32)))
(import "speckl" "map_has" (func $map_has (param i32 i32) (result i32)))
(import "speckl" "map_remove" (func $map_remove (param i32 i32)))
(import "speckl" "set_create" (func $set_create (result i32)))
(import "speckl" "set_add" (func $set_add (param i32 i32)))
(import "speckl" "set_has" (func $set_has (param i32 i32) (result i32)))
(import "speckl" "set_size" (func $set_size (param i32) (result i32)))
(import "speckl" "alloc" (func $alloc (param i32) (result i32)))
(import "speckl" "write_i32" (func $write_i32 (param i32 i32)))
(import "speckl" "write_f64" (func $write_f64 (param i32 f64)))
;; Linear memory (exported so host can read state)
(memory (export "memory") 1)
;; State globals
(global $phase (mut i32) (i32.const 0))
(global $sourcePath (mut i32) (i32.const 0))
(global $outputDir (mut i32) (i32.const 0))
(global $ast (mut i32) (i32.const 0))
(global $errors (mut i32) (i32.const 0))
(global $artifacts (mut i32) (i32.const 0))
;; ---- init ----
(func $init (export "init")
(i32.const 0)
(global.set $phase)
(i32.const 0)
(global.set $sourcePath)
(i32.const 0)
(global.set $outputDir)
(i32.const 0)
(global.set $ast)
(i32.const 0)
(global.set $errors)
(i32.const 0)
(global.set $artifacts)
)
;; ---- Actions ----
;; action: compile
(func $compile (export "compile") (param $sourcePath i32) (param $outputDir i32) (result i32)
;; require: phase === Idle
;; Complex right-hand side not compiled: phase === Idle
;; require: len(sourcePath) > 0
;; Complex guard not compiled: len(sourcePath) > 0
;; emit CompilationStarted { ... }
(i32.const 1)
)
;; action: parse
(func $parse (export "parse") (result i32)
;; require: phase === Parsing
;; Complex right-hand side not compiled: phase === Parsing
;; require: len(sourcePath) > 0
;; Complex guard not compiled: len(sourcePath) > 0
;; emit PhaseCompleted { ... }
(i32.const 1)
)
;; action: generateProvenance
(func $generateProvenance (export "generateProvenance") (result i32)
;; require: phase === Parsed || phase === TypeScriptGenerated || phase === WASMGenerated
;; Complex right-hand side not compiled: phase === Parsed || phase === TypeScriptGenerated || phase === WASMGenerated
;; emit ArtifactGenerated { ... }
(i32.const 1)
)
;; action: generateBOMs
(func $generateBOMs (export "generateBOMs") (result i32)
;; require: phase === ProvenanceGenerated || phase === Parsed
;; Complex right-hand side not compiled: phase === ProvenanceGenerated || phase === Parsed
;; emit ArtifactGenerated { ... }
;; emit ArtifactGenerated { ... }
(i32.const 1)
)
;; action: generateTypeScript
(func $generateTypeScript (export "generateTypeScript") (result i32)
;; require: phase === BOMsGenerated || phase === Parsed
;; Complex right-hand side not compiled: phase === BOMsGenerated || phase === Parsed
;; emit ArtifactGenerated { ... }
(i32.const 1)
)
;; action: generateWASM
(func $generateWASM (export "generateWASM") (result i32)
;; require: phase === TypeScriptGenerated || phase === BOMsGenerated || phase === Parsed
;; Complex right-hand side not compiled: phase === TypeScriptGenerated || phase === BOMsGenerated || phase === Parsed
;; emit ArtifactGenerated { ... }
(i32.const 1)
)
;; action: report
(func $report (export "report") (result i32)
;; require: phase === ProvenanceGenerated || phase === BOMsGenerated || phase === TypeScriptGenerated || phase === WASMGenerated
;; Complex right-hand side not compiled: phase === ProvenanceGenerated || phase === BOMsGenerated || phase === TypeScriptGenerated || phase === WASMGenerated
;; emit PhaseCompleted { ... }
(i32.const 1)
)
;; action: reset
(func $reset (export "reset") (result i32)
(i32.const 1)
)
;; ---- getState ----
(func $getState (export "getState") (result i32 i32)
(i32.const 0)
(global.get $phase)
(i32.store)
(i32.const 4)
(global.get $sourcePath)
(i32.store)
(i32.const 8)
(global.get $outputDir)
(i32.store)
(i32.const 12)
(global.get $ast)
(i32.store)
(i32.const 16)
(global.get $errors)
(i32.store)
(i32.const 20)
(global.get $artifacts)
(i32.store)
(i32.const 0)
(i32.const 24)
)
;; checkConstraints — runtime constraint evaluation stub
(func $checkConstraints (export "checkConstraints") (result i32)
;; TODO: evaluate constraint expressions at runtime
(i32.const 1)
)
)

View file

@ -1,12 +1,15 @@
import { describe, it, expect, beforeAll } from 'vitest';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
import { parseSpeckFile } from '../parser.js';
import { generateProvenanceGraph } from '../generators/provenance.js';
import { generateCycloneDX } from '../generators/cyclonedx.js';
import { generateSPDX } from '../generators/spdx.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('Compiler E2E', () => {
let ast: any;
const testOutputDir = './test-output';
@ -19,7 +22,7 @@ describe('Compiler E2E', () => {
});
it('should parse RetryHandler.speck', () => {
const specPath = path.join(process.cwd(), '..', 'examples', 'RetryHandler.speck');
const specPath = path.join(__dirname, '..', '..', '..', 'examples', 'RetryHandler.speck');
ast = parseSpeckFile(specPath);
expect(ast).toBeDefined();

View file

@ -12,7 +12,7 @@
"@type": "prov:Generation",
"prov:activity": {
"@type": "prov:Activity",
"prov:startTime": "2026-06-30T00:31:19.971Z"
"prov:startTime": "2026-06-30T01:54:05.948Z"
}
},
"rdfs:label": "RetryHandler",

View file

@ -38,7 +38,7 @@
"name": "Scott Scoble",
"email": "scott@scoble.me"
},
"timestamp": "2026-06-30T00:31:19.973Z"
"timestamp": "2026-06-30T01:54:05.951Z"
},
{
"subject": "speck:RetryHandler",
@ -53,7 +53,7 @@
"name": "Speckl Compiler",
"email": "compiler@speckl.scoble.me"
},
"timestamp": "2026-06-30T00:31:19.974Z"
"timestamp": "2026-06-30T01:54:05.951Z"
},
{
"subject": "speck:RetryHandler",
@ -68,7 +68,7 @@
"name": "Speckl Compiler",
"email": "compiler@speckl.scoble.me"
},
"timestamp": "2026-06-30T00:31:19.974Z"
"timestamp": "2026-06-30T01:54:05.951Z"
}
]
}

View file

@ -13,7 +13,7 @@
"spdxVersion": "3.0.1",
"dataLicense": "CC0-1.0",
"creationInfo": {
"created": "2026-06-30T00:31:19.976Z",
"created": "2026-06-30T01:54:05.952Z",
"createdBy": [
"Organization: Speckl",
"tool: speckl-compile-0.2.0"
@ -27,7 +27,7 @@
"@type": "spdx:Tool",
"name": "speckl-compile"
},
"timestamp": "2026-06-30T00:31:19.976Z",
"timestamp": "2026-06-30T01:54:05.952Z",
"comment": "Review type: manual"
}
]

View file

@ -0,0 +1 @@
{"version":"4.1.9","results":[[":compiler/dist/__tests__/compiler.test.js",{"duration":21.732912999999996,"failed":false}],[":compiler/__tests__/parser.test.ts",{"duration":56.655685000000005,"failed":false}],[":compiler/src/__tests__/compiler.test.ts",{"duration":3388.7825,"failed":false}]]}

View file

@ -0,0 +1,61 @@
{
"@context": {
"prov": "http://www.w3.org/ns/prov#",
"speckl": "https://speckl.scoble.me/ns/v0.2#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#"
},
"@graph": [
{
"@type": "prov:Entity",
"@id": "speck:RetryHandler",
"prov:qualifiedGeneration": {
"@type": "prov:Generation",
"prov:activity": {
"@type": "prov:Activity",
"prov:startTime": "2026-06-30T02:09:28.187Z"
}
},
"rdfs:label": "RetryHandler",
"prov:wasDerivedFrom": {
"@id": "speck:FaultHandler"
},
"rdfs:comment": "specialization for retry logic",
"speckl:satisfies": {
"@id": "requirement:RESILIENCE-REQ-01"
},
"prov:wasAttributedTo": {
"@id": "agent:Scott Scoble"
},
"prov:wasInformedBy": {
"@id": "source:architecture_review:AR-2024-0215"
}
},
{
"@type": "prov:Entity",
"@id": "speck:FaultHandler"
},
{
"@type": "prov:Entity",
"@id": "requirement:RESILIENCE-REQ-01",
"rdfs:label": "RESILIENCE-REQ-01",
"speckl:clause": "3.1.2"
},
{
"@type": "prov:Entity",
"@id": "requirement:RESILIENCE-REQ-02",
"rdfs:label": "RESILIENCE-REQ-02"
},
{
"@type": "prov:Agent",
"@id": "agent:Scott Scoble",
"rdfs:label": "Scott Scoble",
"foaf:email": "scott@scoble.me"
},
{
"@type": "prov:Activity",
"@id": "source:architecture_review:AR-2024-0215",
"prov:type": "architecture_review",
"rdfs:label": "architecture_review - AR-2024-0215"
}
]
}

View file

@ -0,0 +1,74 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [],
"metadata": {
"tools": [],
"component": {
"type": "application",
"name": "RetryHandler",
"version": "0.2.0",
"properties": [
{
"name": "speckl:derivesFrom",
"value": "FaultHandler"
},
{
"name": "speckl:satisfies",
"value": "RESILIENCE-REQ-01 clause 3.1.2"
},
{
"name": "speckl:satisfies",
"value": "RESILIENCE-REQ-02"
}
]
}
},
"annotations": [
{
"subject": "speck:RetryHandler",
"annotations": [
{
"type": "tag",
"value": "provenanceType",
"text": "author"
}
],
"author": {
"name": "Scott Scoble",
"email": "scott@scoble.me"
},
"timestamp": "2026-06-30T02:09:28.192Z"
},
{
"subject": "speck:RetryHandler",
"annotations": [
{
"type": "tag",
"value": "provenanceType",
"text": "source"
}
],
"author": {
"name": "Speckl Compiler",
"email": "compiler@speckl.scoble.me"
},
"timestamp": "2026-06-30T02:09:28.192Z"
},
{
"subject": "speck:RetryHandler",
"annotations": [
{
"type": "tag",
"value": "reviewType",
"text": "manual"
}
],
"author": {
"name": "Speckl Compiler",
"email": "compiler@speckl.scoble.me"
},
"timestamp": "2026-06-30T02:09:28.192Z"
}
]
}

View file

@ -0,0 +1,113 @@
{
"@context": [
"https://spdx.github.io/spdx-v3/spdx-context.jsonld",
{
"speckl": "https://speckl.scoble.me/ns/v0.2#"
}
],
"@type": "spdx:Document",
"spdx": {
"@id": "spdx:RetryHandler",
"@type": "spdx:Snippet",
"name": "RetryHandler",
"spdxVersion": "3.0.1",
"dataLicense": "CC0-1.0",
"creationInfo": {
"created": "2026-06-30T02:09:28.194Z",
"createdBy": [
"Organization: Speckl",
"tool: speckl-compile-0.2.0"
]
},
"annotations": [
{
"@type": "spdx:Annotation",
"annotationType": "spdx:REVIEW",
"annotator": {
"@type": "spdx:Tool",
"name": "speckl-compile"
},
"timestamp": "2026-06-30T02:09:28.194Z",
"comment": "Review type: manual"
}
]
},
"builtBy": [],
"relationships": [
{
"from": {
"@id": "spdx:RetryHandler"
},
"to": {
"@id": "spdx:speck:FaultHandler"
},
"relationshipType": "spdx:derivesFrom"
},
{
"from": {
"@id": "spdx:RetryHandler"
},
"to": {
"@id": "spdx:requirement:RESILIENCE-REQ-01"
},
"relationshipType": "speckl:satisfies",
"additionalProperties": [
{
"name": "speckl:clause",
"value": "3.1.2"
}
]
},
{
"from": {
"@id": "spdx:RetryHandler"
},
"to": {
"@id": "spdx:requirement:RESILIENCE-REQ-02"
},
"relationshipType": "speckl:satisfies"
},
{
"from": {
"@id": "spdx:RetryHandler"
},
"to": {
"@id": "spdx:agent:Scott Scoble"
},
"relationshipType": "spdx:wasAttributedTo"
},
{
"from": {
"@id": "spdx:RetryHandler"
},
"to": {
"@id": "spdx:source:architecture_review:AR-2024-0215"
},
"relationshipType": "spdx:wasInformedBy"
}
],
"elements": [
{
"@type": "spdx:Requirement",
"@id": "spdx:requirement:RESILIENCE-REQ-01",
"name": "RESILIENCE-REQ-01"
},
{
"@type": "spdx:Requirement",
"@id": "spdx:requirement:RESILIENCE-REQ-02",
"name": "RESILIENCE-REQ-02"
},
{
"@type": "spdx:Person",
"@id": "spdx:agent:Scott Scoble",
"name": "Scott Scoble",
"email": "scott@scoble.me"
},
{
"@type": "spdx:Activity",
"@id": "spdx:source:architecture_review:AR-2024-0215",
"name": "architecture_review - AR-2024-0215",
"type": "architecture_review"
}
]
}