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.
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:
elo/src/parser.tsowns plugin-program parsingelo/src/transform.tsowns generic expression semanticselo/src/plugin-validator.tsowns plugin-only rules such as binding order, scope accumulation, and capability validationelo/src/embed.tsremains the main public façade
parsePluginProgram()validates grammar onlycompilePlugin()validates only the score expressiondo_callis intentionally rejected by generic compilation, so plugin bindings never receive semantic validation- unknown capability names such as
nost.querycurrently pass parsing without any plugin-aware diagnostic - app-side validation in
src/lib/features/elo-publisher/validator.tscannot be the long-term source of truth
- 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
Public exports should remain intentionally small.
PluginCapabilitySpecPluginValidationOptionsPluginDiagnosticValidatedPluginProgramvalidateExpressionAst()validatePluginProgram()
Re-exported from elo/src/embed.ts
compilePlugin()should remain fail-fastcompileFromAst()should remain generic and unaware of plugin capability policy
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;
};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:
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]
- 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 expression can reference all validated binding names
- score expression must not contain
do_call
do_callis valid only inside round bindings- unknown capability names produce diagnostics
- capability argument validation is delegated through
PluginCapabilitySpec
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
- Create
elo/src/plugin-validator.ts - Add shared types for diagnostics and options
- Add
validateExpressionAst()backed by generic expression semantics and caller-provided scope
- Parse via
parsePluginProgram() - Walk rounds and bindings in source order
- Maintain accumulated scope
- Validate score after bindings are processed
- Return diagnostics instead of throwing
- Inspect
do_calldirectly in plugin validation - Resolve capability names against the supplied registry
- Invoke optional argument validators
- Report diagnostics with
phase,roundIndex, andbindingName
- Re-export the new functions from
elo/src/embed.ts - Leave
compilePlugin()unchanged in behavior
- Replace ad hoc logic in
src/lib/features/elo-publisher/validator.ts - Update copy in
src/routes/plugins/publisher/+page.svelte - Keep the publisher app thin and library-driven
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_callin score expression - valid earlier-binding references
- invalid forward references if scope is sequential
- 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
After this iteration:
@contextvm/eloprovides 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