Skip to content

Event builders: generate tag construction functions per kind #11

Description

@alltheseas

Problem

Today codegen generates validators that check whether tags are correct after construction. But the bugs happen during construction. Real-world example from nostr-watch's Kind30166.ts:

// BUG: checks 'open' instead of 'read' — copy-paste error
if (read && typeof open === 'number' && read > 0) {
  tags.push(['rtt-read', String(Math.round(read))]);
}

No amount of post-hoc validation prevents this — it's a logic error in manually written tag construction code. The validator would only tell you the rtt-read tag is missing from the output, not why.

Proposal

Generate builder functions that take typed data and produce correct tags:

// Generated builder — correct by construction
export function buildKind30166Tags(data: {
  relay: string;
  network?: "clearnet" | "tor" | "i2p" | "loki";
  rttOpen?: number;
  rttRead?: number;    // no chance of checking the wrong variable
  rttWrite?: number;
  supportedNips?: number[];
  requirements?: { auth?: boolean; payment?: boolean; pow?: boolean; ssl?: boolean };
  software?: string;
  geohash?: string;
}): string[][] {
  const tags: string[][] = [];
  tags.push(["d", data.relay]);
  if (data.network) tags.push(["n", data.network]);
  if (data.rttOpen != null) tags.push(["rtt-open", String(Math.round(data.rttOpen))]);
  if (data.rttRead != null) tags.push(["rtt-read", String(Math.round(data.rttRead))]);
  // ... etc
  return tags;
}

This inverts the direction: validators go tags → errors, builders go data → tags. The schema has enough information for both — tag names, positions, value constraints, required vs optional.

Where the data comes from

The validator planner (plan-validators.ts) already extracts ValidatorAction[] per kind — required tags, conditional tags, value patterns. A builder emitter would consume the same action plan but emit construction code instead of checking code.

Design considerations

  • Required vs optional tags: Schema contains constraints mark required tags. Others are optional (only emitted if data is provided).
  • Value formatting: Numeric values need String(), enums need type narrowing, patterns need validation at the builder boundary.
  • Multi-language: Like validators, builders could be generated for multiple languages using the same action plan.
  • Relationship to validators: Builders guarantee structural correctness. Validators remain useful for checking events received from the wire (where you can't trust the builder was used).

Effort: Medium-High | Impact: Highest

This is the strongest value proposition for app devs — shifting from "validate what you built" to "build it right the first time."

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions