[generator] LTL inner-expression identifiers not qualified with this. — input fields + action params out of scope in rewriteExpr #55
Labels
No labels
blocked
bug
design
docs
engineering
feature
infra
priority:high
priority:low
priority:medium
question
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: sscoble/speckl#55
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The TypeScript generator's
verifyN()body (speckl#48) translates the LTL outer layer (always/eventually/implies) but the inner expression identifiers are not qualified withthis.. For LTL formulas that reference input record fields or action parameters, the emitted TS code throwsReferenceErrorat runtime because the bare identifiers are not defined in the class scope.Where it fires
examples/RetryHandler.speck—verify: always(implies(attempt > 1, delay > 0))references input fieldsattemptanddelay. The emittedverify1()is:attemptanddelayare input record fields onRetryAttempted, not state vars, sorewriteExprdoesn't prefix them withthis..Any
verifyblock referencing input record fields, action params, or non-state identifiers will hit this.Why it exists
The
rewriteExprfunction (typescript-state-machine.ts) currently only knows about state-var names (from the class'sstate {}block). The LTL inner-expression translator in speckl#48 callsrewriteExprto qualify identifiers, but the LTL body isn't pre-tagged with which identifiers are state vars vs input fields vs action params.Scope
parseMemberBlock'input:' and 'action:' AST nodes)this.when it's not a literal/operator/keywordSet<string>passed to the emit pipelineAcceptance Criteria
rewriteExpr(or a newrewriteLtlExpr) correctly prefixes input record fields and action params withthis.in LTL bodiesexamples/RetryHandler.speckemitsverify1()that referencesthis.attemptandthis.delay(or whatever the correct input field prefix is)RetryHandlerMachine, set fields to violate the LTL (e.g.,delay = 0whenattempt > 1), callverify1(), getfalse(not ReferenceError)Out of scope
verify:bodies (separate ticket: speckl#54)Depends on
Why this matters
Blocks the AC2 end-to-end test in speckl#48. The patch is correct, but the runtime test fails before getting to the boolean check. Part of the B2 PR stack.
Discovered during
speckl#55 — Cycle 2 Negative Verification (2026-06-03 13:30 UTC)
Verdict: NEGATIVE — patch applied, but tsc harness shows the patched build has MORE errors than baseline, not fewer. Per the NEGATIVE-VERIFICATION PROTOCOL, the patch was dropped; the issue is left OPEN for re-dispatch.
What was tried
Two dispatch cycles (2 × 50 calls, ~850s wall-clock) plus CoS recovery (50 calls). The first subagent did analysis only. The second subagent applied a patch covering both the parser fix (multi-line
input:/output:blocks) and the generator fix (collect input/output/action-param field names; buildclassIdentNames; pass torewriteExpr; addrecordFieldDeclsto emit field declarations). CoS re-applied the patch after the second subagent's working tree was lost (per the COMMIT-BEFORE-CLOSE PROTOCOL lesson). The patch was successfully compiled.AC table
rewriteExprcorrectly prefixes input/output/action-param names withthis.in LTL bodiesrewriteExprwas updated to use the unifiedclassIdentNamesset. Input fields ARE collected. But the AC's "RetryHandler verify1 referencesthis.attemptandthis.delay" expectation is not satisfiable becauseattemptanddelayare not in any block (input:,output:,state:,action:) inRetryHandler.speck— they're free variables in the LTL body.RetryHandler.speckemitsverify1()referencingthis.attemptandthis.delayattemptanddelayin the LTL body but never declares them. They appear in theevent RetryAttemptedpayload but event payload fields are not the same as class fields.tsc harness output (PATCHED build, all 5 examples)
Net: 18 errors. Baseline (fix/speckl-54): 4 errors (all in RetryHandler verify1 body for
attempt/delay). The patch introduced 14 new errors and only changed the shape of 4 existing errors.Root cause of the new errors
The
recordFieldDeclsfunction callstsType(f.type, ...)to get the TS type for each field declaration. For opaque idents (e.g.,Action,Operation,hasPermission,RateLimited) and SpeckDL primitive types (Real,Bool),tsTypereturns the raw type name as a TS identifier — which is not declared in the generated code. The class body ends up withhasPermission!: hasPermission;orattemptCount!: Real;which are invalid TS.Fix for the next cycle: in
recordFieldDecls, fall back toanyfor any TypeExpr whosetsTypeis not a recognized TS primitive (string,number,boolean,any,unknown, etc.). This requires either atsTypeSafehelper that returnsanyfor opaque/primitive SpeckDL types, or anisValidTsTypepredicate.Secondary finding: pre-existing dedup bug
memberKey()attypescript-state-machine.ts:864returns just'verify'for allverify:members, collapsing them in thededuped: Map<string, MemberNode>. Only the last verify block survives. This blocks the count-matching verification rule from the NEGATIVE-VERIFICATION PROTOCOL (we'd expect 15 verifyN emissions across 5 examples, but only the last verify in each spec fires). This is a pre-existing bug from #48. Out of scope for #55; should be a separate ticket.What was done
input:,output:toblockStarters; addedparseInputOutputBlockMultilinehelper (joins multi-line record body and feeds toparseTypeExprwhich produces arecordTypeExpr).inputFieldNames,outputFieldNames,actionParamNames; builtclassIdentNames; updatedrewriteExprto use the unified set; updatedemitActionand all internal callers.recordFieldDeclsto emitfieldName!: type;declarations in the class body. BUG: falls back to invalid TS types for opaque idents.d5eea7e(clean) on branchfix/speckl-55-ltl-inner-identifier-scope. Branch is empty pointer (0 unique commits). Patch is dropped.Re-dispatch directive for next cycle
fix/speckl-55-ltl-inner-identifier-scopebranch (already exists, empty pointer). Apply changes ON TOP ofd5eea7e.input:,output:toblockStarters; addparseInputOutputBlockMultiline.inputFieldNames,outputFieldNames,actionParamNames; buildclassIdentNames; pass torewriteExpr. Update all callers.tsTypeSafehelper that returnsanyfor opaque idents and SpeckDL primitives (Real,Bool,Nat,Int,Bytes,String). Use it inrecordFieldDeclsinstead oftsType.[ltl-rewriter] calleddirective at the top of the newrewriteExprpath.compiler/cwd, runnpx tsc; thennode dist/index.js -t typescript ../examples/RetryHandler.speck -o /tmp/spec-out-v55; check thatverify1()body now hasthis.attempt(or is empty ifattempttruly is a free variable — that's also a valid resolution).compiler/cwd: should show ≤4 errors (the same 4 baseline errors from RetryHandler; everything else should be 0).git -c user.email=agent-engineering@noreply.os.scoble.me -c user.name="agent-engineering" commit -m "fix(parser+generator): speckl#55 multi-line input/output blocks + LTL inner-identifier scoping".git commit.The dedup bug (
memberKeyreturns just'verify') is a separate pre-existing issue from #48. File it as a new ticket if you want it addressed; out of scope for #55.Working tree state at close
fix/speckl-55-ltl-inner-identifier-scope(empty pointer atd5eea7e, 0 unique commits)Cycle 161 CoS disposition — speckl#55 PARTIAL close with parser-fix follow-up
Verdict: PARTIAL. The generator fix is in place and verified on a synthetic single-line spec, but the canonical example
examples/RetryHandler.speckis blocked by a separate parser bug. The follow-up is filed assscoble/speckl#57.Generator side (this ticket) — DONE
thisFieldNamesset populated from input/output field namestypescript-state-machine.ts:215-231verifyN()call site mergesthisFieldNamesintostateVarNamesforrewriteExprtypescript-state-machine.ts:334-342verify1method containsthis.attemptandthis.delay(not bare identifiers)npm testpasses (all 37 tests green)vitest run→ "Tests 37 passed (37)"console.errorleft in the codeLive evidence (2026-06-09 09:35 PT) — synthetic spec
/tmp/speckl-synth/SingleLineInput.speckproduces:Parser side (BLOCKING) — see speckl#57
The canonical example
examples/RetryHandler.speckuses a 5-line multi-lineinput: { ... }block. The parser returnstype: 'ident'with nofieldsfor multi-line input/output. The generator fix above cannot be verified end-to-end on this canonical example until the parser is fixed.Live evidence (2026-06-09 09:35 PT) —
examples/RetryHandler.speckproduces:The subagent discovered this 2nd-order issue while doing the work. The cycle 161 CoS independently verified the parser finding (live probe of the AST shape) and filed speckl#57 with the correct root-cause diagnosis (per cycle 144 premise_correction pattern + cycle 130 subagent-filing-claim-verification).
Recovery recipe for speckl#57 (parser fix)
In
/home/sscoble/speckl/compiler/src/parser.ts, around line 390 (parseMember), add multi-lineinput: {/output: {block handling. Suggested approach:Once the parser fix is in place, AC3 above should be re-verified on the canonical
examples/RetryHandler.speck(not just the synthetic single-line spec). At that point, the entire speckl#55 fix is end-to-end-verified and the ticket can be re-opened and closed fully.Cycle 161 lessons
pass thisFieldNames as the 4th arg to rewriteExpr) was logically inverted. The 4th arg SUPPRESSES prefixing, not adds it. The subagent correctly identified this and applied a union-set fix on the 2nd arg.input: { ... }block is parsed astype: 'ident'— this affects ALL 5 examples in/home/sscoble/speckl/examples/. The fix is parser-side, not generator-side.Files modified (cycle 161)
/home/sscoble/speckl/compiler/src/generators/typescript-state-machine.ts— addedthisFieldNamescollection and merged it intoverifyStateNamesat theverifyN()call site. No other generator behavior changed.