diff --git a/compiler/__tests__/parser.test.ts b/compiler/__tests__/parser.test.ts index 4678ab3..0a92083 100644 --- a/compiler/__tests__/parser.test.ts +++ b/compiler/__tests__/parser.test.ts @@ -766,4 +766,32 @@ speck Second { } }); }); + + // Cycle 61: top-of-file `event_suffix:` directive. When set, the .proto + // generator appends the suffix to every event message name. + describe('event_suffix directive (cycle 61)', () => { + it('should record eventSuffix on the SpeckNode', () => { + const content = `speck FederatedMeetup { + event_suffix: "Payload" + event CreateGroup { name: String } +}`; + const ast = parseSpeckContent(content); + expect(ast.specks[0].eventSuffix).toBe('Payload'); + }); + + it('should leave eventSuffix undefined when not set', () => { + const content = `speck FederatedMeetup { + event CreateGroup { name: String } +}`; + const ast = parseSpeckContent(content); + expect(ast.specks[0].eventSuffix).toBeUndefined(); + }); + + it('should tolerate quoted and bare values', () => { + const a = parseSpeckContent('speck A {\n event_suffix: "Payload"\n}'); + const b = parseSpeckContent('speck B {\n event_suffix: Payload\n}'); + expect(a.specks[0].eventSuffix).toBe('Payload'); + expect(b.specks[0].eventSuffix).toBe('Payload'); + }); + }); }); diff --git a/compiler/src/generators/protobuf.ts b/compiler/src/generators/protobuf.ts index 91b8c42..4fb895a 100644 --- a/compiler/src/generators/protobuf.ts +++ b/compiler/src/generators/protobuf.ts @@ -229,7 +229,11 @@ function emitSpeck(speck: SpeckNode, packageName?: string): string { // Then emit events as messages for (const ev of events) { - lines.push(`message ${ev.name} {`); + // Cycle 61: honor `event_suffix: "Payload"` directive so emitted + // message names match consumer Go bindings (e.g. CreateGroupPayload + // instead of CreateGroup). + const evName = speck.eventSuffix ? ev.name + speck.eventSuffix : ev.name; + lines.push(`message ${evName} {`); if (ev.fields.length === 0) { lines.push(' // empty event'); } else { diff --git a/compiler/src/parser.ts b/compiler/src/parser.ts index ed90599..e4c66cb 100644 --- a/compiler/src/parser.ts +++ b/compiler/src/parser.ts @@ -16,6 +16,12 @@ export interface SpeckNode { // go_package: e.g. "github.com/sscoble/federated-meetup/proto/federated_meetup/v1;federatedmeetupv1" protoPackage?: string; goPackage?: string; + // Cycle 61: optional event-name suffix. When set, the .proto generator + // appends this string to every `event` block's message name (e.g. an + // event CreateGroup with event_suffix: "Payload" emits + // `message CreateGroupPayload { ... }`). Matches the federated-meetup + // convention where every transition payload is named XxxPayload. + eventSuffix?: string; } export interface ImportNode { @@ -318,6 +324,16 @@ 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; + let i = startIndex + 1; let braceCount = 1; @@ -336,7 +352,7 @@ function parseSpeck(lines: string[], startIndex: number): SpeckNode | null { // `author:`, `version:`, `license:` continue to be parsed as members // (AuthorNode, etc.) for backward compatibility with the existing // PROV-O / CycloneDX generators. - const metaMatch = line.match(/^(proto_package|go_package)\s*:\s*(.+)$/); + const metaMatch = line.match(/^(proto_package|go_package|event_suffix)\s*:\s*(.+)$/); if (metaMatch) { // Strip surrounding quotes if present const value = metaMatch[2].trim().replace(/^['"]|['"]$/g, ''); @@ -344,6 +360,8 @@ function parseSpeck(lines: string[], startIndex: number): SpeckNode | null { (parseSpeck as any)._protoPackage = value; } else if (metaMatch[1] === 'go_package') { (parseSpeck as any)._goPackage = value; + } else if (metaMatch[1] === 'event_suffix') { + (parseSpeck as any)._eventSuffix = value; } i++; continue; @@ -417,7 +435,8 @@ function parseSpeck(lines: string[], startIndex: number): SpeckNode | null { return { type: 'speck', name, members, protoPackage: (parseSpeck as any)._protoPackage, - goPackage: (parseSpeck as any)._goPackage }; + goPackage: (parseSpeck as any)._goPackage, + eventSuffix: (parseSpeck as any)._eventSuffix }; } function findBlockEnd(lines: string[], startIndex: number, startBraceCount: number): number {