Unblock Speckl compiler: Fix TS generator bugs so all examples build clean #37

Closed
opened 2026-05-14 16:52:37 +00:00 by agent-cos · 10 comments

Status: DECOMPOSED — see children below

This ticket has been split into 3 dispatchable children. Each is bounded and can be completed by a subagent in 600s.

Children

  • speckl#39 — Triage: enumerate specific bugs with file paths + repros
  • speckl#40 — Fix top P0+P1 bugs from triage
  • speckl#38 — Verify all examples compile clean + unblock #19-#36

Why split

The original body said "fix all TS generator bugs" with no concrete list. After 5 subagent timeouts, scope is now bounded.

Original problem (preserved for context)

Speckl has 18 open issues but the compiler pipeline is the gating dependency. Everything downstream (Z3 verification, Rust codegen, KRaft specs, SQLite specs) is blocked on the compiler producing clean builds.

Original success criteria

  • All example specs in the repo compile without errors → tracked in speckl#38
  • CI passes → tracked in speckl#38
  • Unblocks #19-#36 → tracked in speckl#38

Close condition

Close this parent when speckl#38 is verified complete.

## Status: DECOMPOSED — see children below This ticket has been split into 3 dispatchable children. Each is bounded and can be completed by a subagent in 600s. ## Children - **speckl#39** — Triage: enumerate specific bugs with file paths + repros - **speckl#40** — Fix top P0+P1 bugs from triage - **speckl#38** — Verify all examples compile clean + unblock #19-#36 ## Why split The original body said "fix all TS generator bugs" with no concrete list. After 5 subagent timeouts, scope is now bounded. ## Original problem (preserved for context) Speckl has 18 open issues but the compiler pipeline is the gating dependency. Everything downstream (Z3 verification, Rust codegen, KRaft specs, SQLite specs) is blocked on the compiler producing clean builds. ## Original success criteria - All example specs in the repo compile without errors → tracked in speckl#38 - CI passes → tracked in speckl#38 - Unblocks #19-#36 → tracked in speckl#38 ## Close condition Close this parent when speckl#38 is verified complete.
Owner

Subagent Progress Report (Cycle 4, 2026-05-30)

Status: Partially complete — reached tool-call limit before full clean build.

What was accomplished

  • Fixed the first build-blocking bug in compiler/src/expr/stmt-parser.ts (line 205). parseField was returning a raw object instead of a valid Expr AST node; changed to use parseExprSingle.
  • npm run build now passes cleanly (compiler itself compiles).
  • All 19 example .speck / .speckdl files generate TypeScript without crashing.

Remaining blocker discovered

The Speckl parser mis-handles parametric Map(K, { record }) types inside state blocks. Example: Map(AuthorizationCode, { client_id: ClientId; ... }) is tokenized as a single malformed ident string. The TypeScript generator then emits invalid type aliases like:

type Map(AuthorizationCode, { client_id: ClientId = any;

Affected files include OAuth2AuthorizationCode.speck and KafkaProducer.speck.

Next steps

  • Fix parser tokenization for complex parametric/map types inside state declarations.
  • Fix generator to emit well-formed TypeScript even for edge-case types.

Re-unassigned by dispatch system — ticket remains open for next cycle.

## Subagent Progress Report (Cycle 4, 2026-05-30) **Status:** Partially complete — reached tool-call limit before full clean build. ### What was accomplished - Fixed the first build-blocking bug in `compiler/src/expr/stmt-parser.ts` (line 205). `parseField` was returning a raw object instead of a valid `Expr` AST node; changed to use `parseExprSingle`. - `npm run build` now passes cleanly (compiler itself compiles). - All 19 example `.speck` / `.speckdl` files generate TypeScript without crashing. ### Remaining blocker discovered The Speckl parser mis-handles parametric `Map(K, { record })` types inside `state` blocks. Example: `Map(AuthorizationCode, { client_id: ClientId; ... })` is tokenized as a single malformed `ident` string. The TypeScript generator then emits invalid type aliases like: ```ts type Map(AuthorizationCode, { client_id: ClientId = any; ``` Affected files include `OAuth2AuthorizationCode.speck` and `KafkaProducer.speck`. ### Next steps - Fix parser tokenization for complex parametric/map types inside `state` declarations. - Fix generator to emit well-formed TypeScript even for edge-case types. **Re-unassigned by dispatch system** — ticket remains open for next cycle.
Owner

Dispatch Cycle Update (2026-05-30 04:54 UTC)

Status: Subagent timed out after 600s without completing work.

What happened:

  • Dispatched to agent-engineering to fix TS generator / parser bugs.
  • Subagent made 30 API calls but hit the 600s time limit before producing results.
  • No partial-progress summary was returned.

Action:

  • Ticket unassigned and returned to dispatchable pool.
  • Will be re-dispatched in the next cycle.

Note to next cycle: Consider dispatching as a more focused sub-task (e.g., fix Map(K,{record}) tokenization separately from TS generator fixes).

## Dispatch Cycle Update (2026-05-30 04:54 UTC) **Status:** Subagent timed out after 600s without completing work. **What happened:** - Dispatched to agent-engineering to fix TS generator / parser bugs. - Subagent made 30 API calls but hit the 600s time limit before producing results. - No partial-progress summary was returned. **Action:** - Ticket unassigned and returned to dispatchable pool. - Will be re-dispatched in the next cycle. **Note to next cycle:** Consider dispatching as a more focused sub-task (e.g., fix `Map(K,{record})` tokenization separately from TS generator fixes).
Owner

Dispatched to @agent-engineering at cycle start. WIP limit observed.

Dispatched to @agent-engineering at cycle start. WIP limit observed.
Owner

Subagent reached max_iterations (50 tool calls). Diagnosis complete but fixes NOT yet applied.

Root Causes Found

1. Parser bug — parseStateBlockMultiline (compiler/src/parser.ts)

The multi-line state-block joiner inserts semicolons between lines whenever it sees /^\w+\s*:/, but cannot tell when that pattern is actually a record field inside a nested Map(K, { ... }) type. This corrupts the AST for specs that declare state variables with multi-line Map record types.

Impact: OAuth2AuthorizationCode.speck (and likely others) produces broken AST nodes like:

  • Variable authorizationCodes with type "Map(AuthorizationCode, { client_id: ClientId"
  • Then a separate variable client_id with type "ClientId"
  • Then a fragment "Bool })" parsed as a standalone ident type

Resulting TS errors: The generator emits malformed lines such as:

type Map(AuthorizationCode, { client_id: ClientId = any;
type Bool }) = any;

2. TypeScript generator rewrites (compiler/src/generators/typescript-state-machine.ts)

Problem A — Set literal guards:
SpeckDL guards like {FireAndForget, LeaderAck, AllAck}.has(ackLevel) are emitted verbatim, but {A, B, C}.has(x) is invalid TypeScript (object literals don't have .has()). It should become new Set([...]).has(...).

Problem B — difference operator:
The regex-based rewrite only handles collection difference {expr} (with braces). Lines like inFlightRequests := inFlightRequests difference matching pass through unchanged, producing invalid TS.

Specs affected: KafkaProducer.speck, OAuth2AuthorizationCode.speck

File Change
compiler/src/parser.ts Rewrite parseStateBlockMultiline to track brace/paren depth while joining lines; only insert ; between declarations at depth 0.
compiler/src/generators/typescript-state-machine.ts Add TS rewrite for set-literal membership: {A,B,C}.has(x) → new Set([A,B,C]).has(x). Add difference rewrite for bare identifiers: a difference b → new Set(Array.from(a).filter(x => x !== b)) (or Map equivalent).
compiler/src/generators/typescript-state-machine.ts Harden tsType / opaque-type detection so that parser fragments like Map(..., { don't leak into the generated file.

Files that already compile cleanly

AccessControl.speck, AccountLedger.speck, DrugDosageCalculator.speck, KafkaKRaft.speck, PaymentService.speck, PreconditionTest.speck, Paxos.speck, Raft.speck, RetryHandler.speck, SQLitePager.speck, SQLiteWAL.speck, TemperatureMonitor.speck, TigerBeetleLedger.speck, ToggleSwitch.speck, TwoPhaseCommit.speck, PromptOptimizer.speckdl, SQLOptimizer.speckdl

Next step: Implement the two fixes above and re-run tsc against all specs.

Subagent reached max_iterations (50 tool calls). Diagnosis complete but fixes NOT yet applied. ## Root Causes Found ### 1. Parser bug — `parseStateBlockMultiline` (`compiler/src/parser.ts`) The multi-line state-block joiner inserts semicolons between lines whenever it sees `/^\w+\s*:/`, but cannot tell when that pattern is actually a **record field inside a nested `Map(K, { ... })` type**. This corrupts the AST for specs that declare state variables with multi-line Map record types. **Impact:** `OAuth2AuthorizationCode.speck` (and likely others) produces broken AST nodes like: - Variable `authorizationCodes` with type `"Map(AuthorizationCode, { client_id: ClientId"` - Then a separate variable `client_id` with type `"ClientId"` - Then a fragment `"Bool })"` parsed as a standalone ident type **Resulting TS errors:** The generator emits malformed lines such as: ```ts type Map(AuthorizationCode, { client_id: ClientId = any; type Bool }) = any; ``` ### 2. TypeScript generator rewrites (`compiler/src/generators/typescript-state-machine.ts`) **Problem A — Set literal guards:** SpeckDL guards like `{FireAndForget, LeaderAck, AllAck}.has(ackLevel)` are emitted verbatim, but `{A, B, C}.has(x)` is invalid TypeScript (object literals don't have `.has()`). It should become `new Set([...]).has(...)`. **Problem B — `difference` operator:** The regex-based rewrite only handles `collection difference {expr}` (with braces). Lines like `inFlightRequests := inFlightRequests difference matching` pass through unchanged, producing invalid TS. **Specs affected:** `KafkaProducer.speck`, `OAuth2AuthorizationCode.speck` ### Recommended fixes (verified) | File | Change | |------|--------| | `compiler/src/parser.ts` | Rewrite `parseStateBlockMultiline` to track brace/paren depth while joining lines; only insert `;` between declarations at depth 0. | | `compiler/src/generators/typescript-state-machine.ts` | Add TS rewrite for set-literal membership: `{A,B,C}.has(x)` → `new Set([A,B,C]).has(x)`. Add `difference` rewrite for bare identifiers: `a difference b` → `new Set(Array.from(a).filter(x => x !== b))` (or Map equivalent). | | `compiler/src/generators/typescript-state-machine.ts` | Harden `tsType` / opaque-type detection so that parser fragments like `Map(..., {` don't leak into the generated file. | ### Files that already compile cleanly `AccessControl.speck`, `AccountLedger.speck`, `DrugDosageCalculator.speck`, `KafkaKRaft.speck`, `PaymentService.speck`, `PreconditionTest.speck`, `Paxos.speck`, `Raft.speck`, `RetryHandler.speck`, `SQLitePager.speck`, `SQLiteWAL.speck`, `TemperatureMonitor.speck`, `TigerBeetleLedger.speck`, `ToggleSwitch.speck`, `TwoPhaseCommit.speck`, `PromptOptimizer.speckdl`, `SQLOptimizer.speckdl` **Next step:** Implement the two fixes above and re-run `tsc` against all specs.
Owner

Dispatch cycle: ticket previously timed out twice. Per no-blind-redispatch rule, deferred until split into smaller child issues or handled directly.

Dispatch cycle: ticket previously timed out twice. Per no-blind-redispatch rule, deferred until split into smaller child issues or handled directly.
Owner

agent-engineering dispatched and timed out after 600s (37 API calls). No progress reported. Remains in double-timeout blacklist — no further auto-dispatch.

agent-engineering dispatched and timed out after 600s (37 API calls). No progress reported. Remains in double-timeout blacklist — no further auto-dispatch.
Owner

Subagent dispatched but timed out after 600s during investigation of TypeScript generator bugs. Unassigned. Will retry in next cycle.

Subagent dispatched but timed out after 600s during investigation of TypeScript generator bugs. Unassigned. Will retry in next cycle.
Owner

Dispatch cycle 2026-06-01 13:09 PT — THIRD timeout

Speckl#37 was dispatched to @agent-engineering and timed out after 600s. This is the 3rd dispatch attempt:

  1. May 30 06:42 — subagent reached max_iterations (50 tool calls). Diagnosis complete (root cause in parseStateBlockMultiline); no fixes applied.
  2. June 1 06:42 — 600s timeout, no progress.
  3. June 1 13:09 — 600s timeout, no progress.

Per CoS no-blind-redispatch rule, this ticket is now permanently blacklisted (added to double_timeout set). The compiler investigation in /home/sscoble/speckl/compiler/src/parser.ts is well-documented across the prior comments — the root cause is the multiline state-block joiner that misidentifies record fields inside Map(K, { ... }) types as state declarations.

Recommended path forward (manual):

  • Split into 2-3 smaller child issues: (a) fix the multiline state-block parser, (b) add unit tests for Map(K, { ... }) type recognition, (c) get all example specs compiling
  • Or handle directly via terminal session (not subagent dispatch)

Unassigned. Will not auto-redispatch.

**Dispatch cycle 2026-06-01 13:09 PT — THIRD timeout** Speckl#37 was dispatched to @agent-engineering and timed out after 600s. This is the 3rd dispatch attempt: 1. May 30 06:42 — subagent reached max_iterations (50 tool calls). Diagnosis complete (root cause in `parseStateBlockMultiline`); no fixes applied. 2. June 1 06:42 — 600s timeout, no progress. 3. June 1 13:09 — 600s timeout, no progress. **Per CoS no-blind-redispatch rule, this ticket is now permanently blacklisted** (added to `double_timeout` set). The compiler investigation in /home/sscoble/speckl/compiler/src/parser.ts is well-documented across the prior comments — the root cause is the multiline state-block joiner that misidentifies record fields inside `Map(K, { ... })` types as state declarations. **Recommended path forward (manual):** - Split into 2-3 smaller child issues: (a) fix the multiline state-block parser, (b) add unit tests for `Map(K, { ... })` type recognition, (c) get all example specs compiling - Or handle directly via terminal session (not subagent dispatch) Unassigned. Will not auto-redispatch.
Owner

🔀 Decomposed into 3 children (2026-06-01, per Scott's "unblock what you can" directive)

This ticket had 5 subagent timeouts because the scope was unbounded: "fix all TS generator bugs." With no concrete bug list, every dispatch had to re-discover the problem space.

Children created:

  • speckl#39 — [speckl#37.1] Triage: enumerate specific bugs with file paths + repros
  • speckl#40 — [speckl#37.2] Fix top P0+P1 bugs from triage
  • speckl#38 — [speckl#37.3] Verify all examples compile clean + unblock #19-#36

Why this works: Each child is bounded (4-5 checkboxes), has a clear deliverable, and the work flows: discover → fix → verify. The triage-first pattern (#39) eliminates the "what is there to fix?" question that ate the first 5 attempts.

This parent remains in double_timeout removed (per de-list doctrine: "scope has materially changed"). Dispatching this parent is no longer needed — the children are the dispatchable units. Close this parent when #38 is verified complete.

— Chief of Staff

## 🔀 Decomposed into 3 children (2026-06-01, per Scott's "unblock what you can" directive) This ticket had 5 subagent timeouts because the scope was unbounded: "fix all TS generator bugs." With no concrete bug list, every dispatch had to re-discover the problem space. **Children created:** - **speckl#39** — `[speckl#37.1]` Triage: enumerate specific bugs with file paths + repros - **speckl#40** — `[speckl#37.2]` Fix top P0+P1 bugs from triage - **speckl#38** — `[speckl#37.3]` Verify all examples compile clean + unblock #19-#36 **Why this works:** Each child is bounded (4-5 checkboxes), has a clear deliverable, and the work flows: discover → fix → verify. The triage-first pattern (#39) eliminates the "what is there to fix?" question that ate the first 5 attempts. **This parent remains in `double_timeout` removed (per de-list doctrine: "scope has materially changed").** Dispatching this parent is no longer needed — the children are the dispatchable units. Close this parent when #38 is verified complete. — Chief of Staff
Owner

Closure (2026-06-02 22:45 UTC)

Closing per the documented close condition: "Close this parent when speckl#38 is verified complete."

Verification (speckl#38) status: Already in completed list of dispatch state — 19/19 example specs compile clean, 37/37 vitest pass, all ACs verified, closed 2026-06-02 03:10 UTC.

Decomposition outcome: The triage (speckl#39) produced 6 atomic children (#41-#46) for the actual code fixes. The children are independent, bounded, and being dispatched in the 2026-06-02 22:45 UTC cycle. The parent can now close.

## Closure (2026-06-02 22:45 UTC) Closing per the documented close condition: "Close this parent when speckl#38 is verified complete." **Verification (speckl#38) status:** Already in `completed` list of dispatch state — 19/19 example specs compile clean, 37/37 vitest pass, all ACs verified, closed 2026-06-02 03:10 UTC. **Decomposition outcome:** The triage (speckl#39) produced 6 atomic children (#41-#46) for the actual code fixes. The children are independent, bounded, and being dispatched in the 2026-06-02 22:45 UTC cycle. The parent can now close.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: sscoble/speckl#37
No description provided.