refactor(speckl): replace static-field hack with local-object pattern (cycle 73)

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.
This commit is contained in:
Scott Scoble 2026-06-29 14:29:30 -07:00
parent 121ac7de33
commit e5ee3fe1c0

View file

@ -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 {