Skip to content

Latest commit

 

History

History
209 lines (149 loc) · 8.01 KB

File metadata and controls

209 lines (149 loc) · 8.01 KB

Elo plugin validator plan

Goal

Add a library-native plugin validation layer to elo/ that catches both syntax and plugin-semantic issues, while preserving the current separation of responsibilities between parsePluginProgram(), generic expression compilation in transform(), and the app-side publisher in src/lib/features/elo-publisher/validator.ts.

Architectural decision

Implement plugin validation in a dedicated module elo/src/plugin-validator.ts, then re-export its public API from elo/src/embed.ts.

This keeps the boundaries clear:

Why this approach

What is wrong today

Why this is the most consistent option

  • no plugin logic leaks into the generic compiler
  • no host-specific capability policy leaks into the parser
  • all plugin validation rules live in one module
  • the publisher UI can consume a stable library API instead of custom heuristics

Target public API

Public exports should remain intentionally small.

Re-exported from elo/src/embed.ts

Existing APIs that should stay strict

Proposed types

export type PluginCapabilitySpec = {
  name: string;
  validateArgs?: (argsExpr: Expr) => PluginDiagnostic[];
};

export type PluginValidationOptions = ParserOptions &
  JavaScriptCompileOptions & {
    allowedVariables?: Iterable<string>;
    capabilities?: Record<string, PluginCapabilitySpec>;
  };

export type PluginDiagnostic = Diagnostic & {
  phase?: "parse" | "binding" | "score" | "capability";
  roundIndex?: number;
  bindingName?: string;
};

export type ValidatedPluginProgram = {
  program: PluginProgram | null;
  diagnostics: PluginDiagnostic[];
  score?: JavaScriptCompileResult;
};

Internal design

elo/src/plugin-validator.ts should contain:

  • public API functions
  • scope-aware plugin validation orchestration
  • internal helpers for diagnostics and AST inspection

Suggested internal helpers:

Validation flow

flowchart TD
  A[plugin source] --> B[parse plugin program]
  B -->|parse error| C[return diagnostics]
  B --> D[init scope with _]
  D --> E[walk rounds in order]
  E --> F[walk bindings in order]
  F --> G{binding is do_call}
  G -->|yes| H[validate capability name and args]
  G -->|no| I[validate expression with scoped semantics]
  H --> J[add binding name to scope]
  I --> J
  J --> K[validate score with final scope]
  K --> L[return program plus diagnostics]
Loading

Semantic rules to enforce

Binding semantics

  • each binding can reference _
  • each binding can reference names defined earlier in the same round
  • each binding can reference names defined in earlier rounds
  • later bindings are not visible to earlier bindings unless you intentionally decide to support forward references

Score semantics

  • score expression can reference all validated binding names
  • score expression must not contain do_call

Capability semantics

  • do_call is valid only inside round bindings
  • unknown capability names produce diagnostics
  • capability argument validation is delegated through PluginCapabilitySpec

Generic expression semantics

Use the same semantic engine as the generic compiler, but with caller-provided scope. This should catch:

  • undefined variables
  • unknown function names
  • invalid member access patterns
  • invalid score typos such as nul

Implementation sequence

Phase 1: introduce validator primitives

  1. Create elo/src/plugin-validator.ts
  2. Add shared types for diagnostics and options
  3. Add validateExpressionAst() backed by generic expression semantics and caller-provided scope

Phase 2: add plugin-program validation

  1. Parse via parsePluginProgram()
  2. Walk rounds and bindings in source order
  3. Maintain accumulated scope
  4. Validate score after bindings are processed
  5. Return diagnostics instead of throwing

Phase 3: capability-aware validation

  1. Inspect do_call directly in plugin validation
  2. Resolve capability names against the supplied registry
  3. Invoke optional argument validators
  4. Report diagnostics with phase, roundIndex, and bindingName

Phase 4: public API integration

  1. Re-export the new functions from elo/src/embed.ts
  2. Leave compilePlugin() unchanged in behavior

Phase 5: app integration

  1. Replace ad hoc logic in src/lib/features/elo-publisher/validator.ts
  2. Update copy in src/routes/plugins/publisher/+page.svelte
  3. Keep the publisher app thin and library-driven

Test plan

Add unit tests in elo/test/unit/plugin-program.unit.test.ts for:

  • valid plugin with do 'nostr.query'
  • unknown function in a binding such as Dat(...)
  • unknown variable in a binding
  • unknown capability name such as nost.query
  • invalid score identifier such as nul
  • illegal do_call in score expression
  • valid earlier-binding references
  • invalid forward references if scope is sequential

Non-goals

  • do not teach the parser about concrete capability names
  • do not move plugin validation into elo/src/transform.ts
  • do not make the publisher app the authority for plugin semantics
  • do not broaden compilePlugin() into a diagnostic collector

Expected outcome

After this iteration:

  • @contextvm/elo provides a first-class plugin validation API
  • plugin authors get diagnostics for both syntax and semantic mistakes
  • the publisher page becomes a consumer of library truth, not a custom validator
  • the codebase stays modular, with parser, compiler, plugin validation, and UI each owning a clear responsibility