[parser] Multi-line verify: bodies truncated in parseMember (parser.ts:422) — blocks B2 PR stack #54
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#54
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
verify:parser atcompiler/src/parser.ts:422-425only handles single-line expressions. Multi-lineverify: always(implies(\n ...\n))blocks are truncated to the first line, with the multi-line body discarded. The downstreamtemporalExprfield ends up holding a partial expression like"always(implies("that fails to parse as TS or SpeckDL.Where it fires
examples/AccessControl.speck— 4 verify blocks, all multi-lineexamples/DrugDosageCalculator.speck— 3 verify blocks, all multi-lineexamples/PaymentService.speck— 2 verify blocks, all multi-lineexamples/TemperatureMonitor.speck— 4 verify blocks, all multi-lineOnly
examples/RetryHandler.speckuses single-line verify blocks and parses correctly.Why it exists
The
parseMemberfunction (parser.ts:422) callsparseVerifySingleLineforverifyand processesverify:as a single line by takingline.substring(7).trim(). This was acceptable when the B2.1 stubverifyN()returnedtrueregardless of input, but breaks the speckl#48 Z3 call-out that interprets the truncated expression as TS code.Scope
verify: ...\n ...\n )patterns inparseMemberor a newparseVerifyBlockhelpertemporalExpruntil depth-balance reaches zeroAcceptance Criteria
verify:bodies are joined into a singletemporalExprstring before emissiontemporalExprafter parsing (verifiable by logging the AST)verifyN()then compiles cleanly for all 5 examples (4 currently failing with TS1005)Out of scope
Depends on
Why this matters
This is a blocker for shipping the B2 PR stack (speckl#47 + #48). The B2.1 constraint forwarding (#47) is closed; B2.2 (#48) shipped PARTIAL with this finding. Both must be complete before opening the B2 PR.
Discovered during
CoS Dispatch — 2026-06-03 18:09 UTC (cycle: dispatch)
Branch:
fix/speckl-54-multi-line-verify-parser(baselined on82c8bdc= speckl#48's verifyN() impl)Assignee: agent-engineering (uid=11)
WIP cap: 1 (per Scott directive 2026-06-02 18:39 PT — no parallel batches)
Cycle priority: This is the pre-staged "next cycle priority #1" from cycle 17:35Z
Verification harness (use this exact path)
Expected baseline (without fix): 4 examples fail with TS1005 (parser truncation). Only
RetryHandler.tscompiles because it uses single-lineverify:.Expected result (with fix): 0 tsc errors across all 5 examples.
Pitfalls to avoid (per the 17:35Z cycle lessons)
node compiler/dist/index.jsfrom/home/sscoble/speckl/compiler/cwd, NOT from repo root. The cwd-resolution pitfall (verified 2026-06-03 16:46 UTC) makes the same input produce different emitted state blocks depending on cwd, which can look like a regression..tsfiles (compiler structural change verified 2026-06-03 10:31 UTC). The tsc command above already uses the<name>.ts/<name>.tsglob. Naivetsc --noEmit <name>.tswill fail with TS6053 "File not found".parser.ts:422-425(parseMember → verify single-line). If a fix at that location reduces but doesn't eliminate errors, the residual errors are usually a sibling bug in the caller (e.g., the multi-line joiner), not a quirk of the same bug. Search for callers ofparseMemberand audit them with the same scrutiny.git add compiler/src/parser.ts && git -c user.email=agent-engineering@noreply.os.scoble.me -c user.name="agent-engineering" commit -m "fix(parser): speckl#54 multi-line verify: body joiner"), then post a brief status comment. CoS will verify and close.What this ticket needs
Multi-line
verify: always(implies(\n ...\n))blocks are currently truncated to the first line inparseMember(parser.ts:422). The fix needs to:verify: ...\n ...\n )patterns inparseMemberor a newparseVerifyBlockhelpertemporalExpruntil depth-balance reaches zeroDepth-aware brace/paren tracking is the right pattern (the sibling fix in #53 used this for
Map(K, { ... })blocks — see commit 6bce386 for reference).ACs
verify:bodies are joined into a singletemporalExprstring before emissiontemporalExprafter parsingtsc --noEmiton all 5 examples → 0 errorsOut of scope
CoS Verification — 2026-06-03 18:19 UTC (cycle 18:09 dispatch, NEGATIVE result)
Branch:
fix/speckl-54-multi-line-verify-parser(still points at82c8bdc, 0 unique commits — patch dropped)Assignee: agent-engineering (uid=11) — STAYING ASSIGNED for next-cycle re-dispatch
Disposition: NEGATIVE — patch applied to working tree but did not fix the bug. CoS independent verification confirms regression-class failure.
What the subagent did (50 calls, 316s, max_iterations)
parseVerifyColonBlockhelper (~125 lines) incompiler/src/parser.tsmirroring the depth-aware pattern from commit6bce386(speckl#53).parseMultiLineConstraintVerifyand the mainparseSpeckloop at line 275.npx tscfromcompiler/) succeeds clean.CoS independent tsc harness (from
compiler/cwd, against the patched build)Result (PATCHED build)
(The RetryHandler 4 TS2304 errors for
attempt/delaynot in scope are pre-existing — same on baseline82c8bdc, this is the speckl#55 scope gap. They don't show in the multi-file harness because tsc reports per-file and stops processing files that have parser errors. Run on RetryHandler alone to see them.)Baseline check (unpatched
82c8bdc)Identical 4 TS1005 errors. The patch added 130 lines of code that did not change observable behavior. The cited
temporalExprin emitted TS still readsalways(implies((the truncated first line), not the joined multi-line body.The
parseVerifyColonBlockhelper was added but not actually invoked in the path that produces this output. The multi-lineverify:block inexamples/AccessControl.speckstill gets routed to the single-lineparseMemberhandler at line 422, and the first linealways(implies(is all that gets captured.Disposition
git stash dropaftergit stash, then switched back tomain).fix/speckl-54-multi-line-verify-parserstill exists but empty (points at82c8bdc, 0 unique commits). Not deleted (force-delete requires approval).What the next cycle's dispatch needs
The subagent's approach (add a new helper, wire it in) was correct, but the helper did not end up in the active code path. The next dispatch should:
console.logat the top ofparseVerifyColonBlockand re-run onAccessControl.speckto confirm the helper is actually invoked.parseSpeckline matchesverify:and dispatches to the helper? The subagent's diff said line 275 — but the harness shows the helper is not being called. There may be a precedence issue (the oldverifysingle-line match at line 422-425 still runs first).examples/RetryHandler.speckfirst (single-lineverify:) to confirm the new helper is wired correctly for the easier case before tackling multi-line.Outfall vs root cause — third cycle in a row
This is now the third dispatch where the subagent writes a patch that does not actually fix the bug (after speckl#41, speckl#42). The pattern: parser/compiler work requires a round-trip test in the harness —
parse → emit → tsc --noEmit— and the subagent exhausts its tool budget on the implementation without reaching the round-trip step. The CoS verification step is the only thing catching it.Possible mitigation for next cycle: add an explicit "do not commit until tsc harness returns 0 errors" gate, and budget the dispatch differently. Or: tighten the implementation directive to "the helper must be reachable from
parseSpeckfor the lineverify: always(implies(" with a print-debug directive.Reference
~/.hermes/cos-logs/dispatch-2026-06-03-1735Z.md(prior cycle, #54 was pre-staged as next-cycle priority #1)forgejo-workflow, § "Commit-before-close protocol" (verified 2026-06-03 21:25 UTC)references/june-2026-speckl-50-followup-implementation-baseline-trust.mdthe stash was inspected (viagit diffshowing 4 errors remaining) before dropping.CoS Verification — sscoble/speckl#54 (PASS, 2026-06-03 11:57 PT)
Branch:
fix/speckl-54-multi-line-verify-parseratd5eea7eBaseline:
82c8bdc(speckl#48's verifyN() impl)Diff stat: 1 file changed, 117 insertions, 5 deletions (parser.ts only)
Protocol validation
This is a re-dispatch under the NEGATIVE-VERIFICATION PROTOCOL (the first attempt at 18:09 UTC was NEGATIVE — 130 lines, compiled clean, but tsc harness output was byte-identical to baseline). The re-dispatch required:
Print-debug confirmation ✅ —
console.log('[parser] parseVerifyColonBlock called', {...})at the top ofparseVerifyColonBlock. Ran the simplest example first; observed the helper fire 4 times for AccessControl (matches 4 multi-lineverify:blocks). Total: 15 firings across the 5 examples (4+3+2+2+4) — exact match for theverify:line count.CoS independent re-run from the same harness the subagent used (per-file tsc, from
compiler/cwd):0 TS1005 errors in all 5 examples. The pre-fix baseline (per 18:19 UTC verification) had 4 examples failing with
TS1005: ')' expectedat the same line. The fix is observable in the harness.What was wrong in the first attempt (lessons)
The 18:09 UTC attempt added a 125-line
parseVerifyColonBlockhelper, but the helper was never called from the active dispatch path. The compiler build was clean and tsc produced some output, but the output was identical to baseline because the parser never reached the new helper. The print-debug log would have surfaced this immediately. Lesson: a cleantscbuild is not verification — only the delta in emitted output is.What the new fix does (root cause)
The root cause was in
parseSpeck(parser.ts ~line 275): the dispatch condition only routedverify(with space) andconstraintto the multi-line parser. Theverify:(colon) form fell through to single-lineparseMember(line 422), which truncated multi-line bodies to the first line.Fix:
verify:to the dispatch condition inparseSpeckso multi-lineverify:blocks reach the multi-line parser.parseVerifyColonBlock(lines, startIndex)helper:verify:prefix.RetryHandler), returns it directly.(),[],{}. Stops when depth reaches zero OR a new top-level member keyword appears.{ type: 'verify', name, temporalExpr: joined }.Well-formed
temporalExprverification (per AC2)Generated TS for
AccessControl.verify1()now contains:Parens balanced, structure preserved, full expression visible to the LTL translator. Before the fix, the line was truncated to
// Temporal: always(implies(which is what was producing the TS1005 errors downstream.Residual errors (out of scope, tracked in #55)
After the fix, 5 examples show only TS2304 / TS2552 / TS2693 errors — the shape of the #55 LTL inner-identifier scoping bug (input fields and action params not being prefixed with
this.in LTL bodies). These are pre-existing, not regressions, and #55's body explicitly states this scope:#54 now closes cleanly. #55 is unblocked and ready to dispatch next cycle.
AC table
verify:bodies joined into singletemporalExpr// Temporal:comment in emitted TStemporalExprverifyN()compiles cleanly for those 4 examplesCommit
d5eea7e—fix(parser): multi-line verify: bodies — join depth-aware lines into temporalExpr (speckl#54)Closing this issue. Next: dispatch speckl#55 (LTL inner-identifier scoping), which is now unblocked.
— CoS cron 2026-06-03 11:57 PT cycle