From e5ee3fe1c03e37e1d0c947fdeeb29cbb76fcca1d Mon Sep 17 00:00:00 2001 From: Scott Scoble Date: Mon, 29 Jun 2026 14:29:30 -0700 Subject: [PATCH] refactor(speckl): replace static-field hack with local-object pattern (cycle 73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The proto_package / go_package / event_suffix directives were stored on (parseSpeck as any)._protoPackage etc — static function fields that leaked between parse calls (cycle 61 added a reset at the top, but the pattern was still wrong). Now collected in a local metadata object inside parseSpeck, spread into the return value. No reset needed — the object is local to the invocation. This is the durable fix referenced in the spec-compiler-dogfood skill's anti-pattern section 'Storing per-speck parser metadata on a static function field as a quick hack.' 57/57 tests pass, both specs protoc-clean. --- compiler/src/parser.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/compiler/src/parser.ts b/compiler/src/parser.ts index 9a7d15e..3ccb289 100644 --- a/compiler/src/parser.ts +++ b/compiler/src/parser.ts @@ -324,15 +324,13 @@ function parseSpeck(lines: string[], startIndex: number): SpeckNode | null { const name = headerMatch[1]; const members: MemberNode[] = []; - // Cycle 61: clear static metadata fields at the start of each parse - // so per-speck directives don't leak between parse calls. This is the - // pre-existing static-field-hack pattern for proto_package / go_package - // (see spec-compiler-dogfood skill anti-pattern). Refactoring to a - // local-object approach is tracked as a separate cleanup; for now - // resetting the fields here is the minimal-touch fix. - (parseSpeck as any)._protoPackage = undefined; - (parseSpeck as any)._goPackage = undefined; - (parseSpeck as any)._eventSuffix = undefined; + // Cycle 73: refactored from static-field hack to local-object pattern. + // Previously, proto_package / go_package / event_suffix were stored + // on (parseSpeck as any)._protoPackage etc — static function fields + // that leaked between parse calls. Now collected in a local metadata + // object and spread into the return value. No reset needed because + // the object is local to this invocation. + const metadata: { protoPackage?: string; goPackage?: string; eventSuffix?: string } = {}; let i = startIndex + 1; let braceCount = 1; @@ -357,11 +355,11 @@ function parseSpeck(lines: string[], startIndex: number): SpeckNode | null { // Strip surrounding quotes if present const value = metaMatch[2].trim().replace(/^['"]|['"]$/g, ''); if (metaMatch[1] === 'proto_package') { - (parseSpeck as any)._protoPackage = value; + metadata.protoPackage = value; } else if (metaMatch[1] === 'go_package') { - (parseSpeck as any)._goPackage = value; + metadata.goPackage = value; } else if (metaMatch[1] === 'event_suffix') { - (parseSpeck as any)._eventSuffix = value; + metadata.eventSuffix = value; } i++; continue; @@ -435,9 +433,9 @@ function parseSpeck(lines: string[], startIndex: number): SpeckNode | null { } return { type: 'speck', name, members, - protoPackage: (parseSpeck as any)._protoPackage, - goPackage: (parseSpeck as any)._goPackage, - eventSuffix: (parseSpeck as any)._eventSuffix }; + protoPackage: metadata.protoPackage, + goPackage: metadata.goPackage, + eventSuffix: metadata.eventSuffix }; } function findBlockEnd(lines: string[], startIndex: number, startBraceCount: number): number {