You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Persona: a Koine Studio user who wants to install a theme, a snippet pack or a starter domain someone else made — and the community member who wants to ship one.
Studio's contribution points are closed, hardcoded structures. Every one of them is a static literal a third party cannot reach:
CatalogEntry, Category union, static GROUPS / MODES / KIND
MCP servers
src/mcp/mcp.ts
MCP_CLIENTS: readonly McpClientRecipe[]
Themes
src/settings/theme.ts, appearance.ts
single palette (ADR 0004)
Templates
templates/*/template.json
already declarative and CI-validated — the one that got it right
templates/ is the proof the pattern works here: a folder of data files plus a template.json validated against templates/template.schema.json, with TemplatesValidationTests compiling every template green and validating every manifest against the schema on each dotnet test. It has three consumers already (the demo, Studio's template gallery, the website playground). What it lacks is generality — it describes exactly one kind of thing.
Nothing else in Studio can be contributed by anyone outside the repo.
Proposed solution
Generalize the templates/ pattern into one extension manifest, koine-extension.json, validated by a published JSON Schema, describing an extension that contributes one or more declarative things. Pure data — no runtime, no sandbox, no toolchain, no executable code anywhere in this stage.
Each contributes key maps onto one existing Studio registry, which stops being a static literal and becomes built-ins + contributions. A commands entry deliberately cannot carry code — it names an existing command id plus arguments, which is what keeps this tier declarative.
The manifest is the single contract artifact for the whole epic: the executable extension kinds are reserved in the schema (and rejected by the validator with a clear "not yet supported" diagnostic) so that Stage 2 is additive rather than a redesign.
Alternatives considered
A separate manifest per extension kind (one for themes, one for snippets, …). Simpler individually, but a user installs an extension, not a theme-file, and the gallery, capability model and registry would each need N code paths. One manifest, N contribution keys, is what VS Code and Zed both converged on.
Reuse template.json directly by adding keys to it. Tempting, but it would conflate "a Koine example domain" with "an installable extension" — templates would gain fields meaningless to them, and the 8 in-repo templates would all need migrating. Better: koine-extension.json can contain a templates contribution that points at a folder whose template.json is unchanged.
TOML, like Zed's extension.toml. JSON is what this repo already validates with (template.schema.json, SETTINGS_JSON_SCHEMA, settings.json), what Studio's settings editor already syntax-highlights, and what needs no new parser in the browser build.
Area
Studio (IDE)
Related:#1936 (epic), #101 (the templates/ system this generalizes), #96 (MCP enable/disable prior art), #282 (backend-derived emit-target list)
🧠 Brainstorm
Problem / context
The epic (#1936) establishes that a declarative-first model is the right shape, because three independently-verified constraints rule out hosting untrusted code in the near term (.NET cannot host wasm components; browsers cannot run them natively and the jco workaround is AOT-only; no IDE ecosystem actually ships a hard runtime sandbox). This stage is the part of that plan that carries the product — the tier that works today, in every host, with no security surface at all.
Zed's own documentation concedes the point: "most extensions will work properly without any Rust code present. In particular, only language server, context server and debugger extensions require the presence of custom Rust." Themes, icon themes, snippets and query-only language extensions ship as extension.toml + assets and nothing else.
The critical property, and the reason this stage is worth doing first: pure data loads identically in all three Koine hosts. The Tauri webview, the browser tab and the koine CLI can each consume a JSON manifest with no host-specific runtime, no native dependency, and no per-host capability gate. Nothing else in the epic has that property.
Approaches
A — One manifest, N contribution keys. ✅ Recommended.koine-extension.json with a contributes object. Matches VS Code's contributes and Zed's extension.toml sections. One schema, one loader, one gallery entry, one capability list per extension.
B — Per-kind manifests. A theme.json, a snippets.json, etc., discovered by convention. Less upfront schema design, but the unit a user installs and toggles is an extension, not a file — so the gallery, the enable/disable state, the capability grant and the registry entry would all need to invent a grouping that (A) gets for free.
C — Extend template.json in place. Lowest new surface, but conflates two concepts and forces migration of the 8 existing templates. Rejected.
Recommendation
Approach A. The contributes-map shape is what both comparable systems converged on independently, it gives the gallery and capability model a natural unit, and it leaves room for the executable kinds to be added as further keys without touching anything built here.
Two design choices worth calling out explicitly, both taken from verified evidence:
commands contributions name an existing command id, not code. This is what keeps the tier declarative and is the boundary that stops "declarative" quietly becoming "we invented a scripting language."
Unknown contributes keys are a validation error, not ignored. Reserving the executable kinds now, and failing loudly on them, means a Stage 2 extension installed into a Stage 0 Studio gets a clear message rather than silently doing nothing.
📋 Spec
Goal
One validated extension manifest that lets a third party contribute themes, snippets, project templates, palette entries, settings, MCP servers, export presets and diagnostic severity profiles — as pure data, loading identically in the Tauri webview, the browser tab and the CLI.
Scope
koine-extension.schema.json — the published JSON Schema (mirrors how templates/template.schema.json is published and validated).
A .NET manifest model + parser/validator in Koine.Compiler, so the CLI and both Studio hosts share one implementation.
Opening each of the eight Studio registries from static literal to built-ins + contributions.
Schema-validation tests in the style of TemplatesValidationTests.
Non-goals
No gallery UI, no install/uninstall, no registry — Stage 3.
No capability enforcement — Stage 1 (this stage parses and validates the capabilities array but grants nothing, because nothing declarative needs a grant).
No executable contributions — reserved in the schema, rejected by the validator.
Manifest shape
Required: id (must equal the containing folder name, mirroring template.json's rule), name, version (semver), apiVersion (semver, checked against the floor in Stage 1), description, license. Optional: author, repository, icon, tags, capabilities (default []), contributes (default {}).
additionalProperties: false throughout — the same choice template.schema.json makes, and what makes reserved-but-unimplemented keys detectable.
Contribution keys
Key
Target registry
Notes
themes
settings/theme.ts palette
must satisfy the ADR 0004 Concept-Colors contract
snippets
editor snippet source
per-language
templates
templates/ gallery
points at a folder containing an unmodified template.json
commands
launcher/catalog.ts
runs names an existing command id; args is a JSON object
settings
settings/settingsSchema.ts
one FieldDef-shaped entry per key; namespaced by extension id
mcpServers
mcp/mcp.ts
command + args, the shape Zed uses for context servers
exportPresets
export/diagramExport.ts
named option bundles over existing formats
severityProfiles
koine.config[diagnostics]
maps KOIxxxx → severity
Validation rules
id matches ^[a-z0-9][a-z0-9-]*$ and equals the folder name.
version and apiVersion are valid semver.
Every path resolves inside the extension folder — rejected if it escapes, even at this tier where nothing executes (see the cross-cutting path-containment issue; the rule is cheaper to enforce from day one than to retrofit).
A commands[].runs value that names no known command is an error.
A settings[].key not prefixed with the extension id is an error (prevents settings collisions).
Unknown contributes keys are an error naming the key and stating it is reserved.
Two contributions of the same kind with the same id, within one manifest, are an error.
Assumptions
Contributions are additive only — an extension cannot remove or override a built-in in this stage. Overrides are a Stage 3 conversation once precedence has a UI to express it.
Settings contributed by an extension live under its id namespace and are dropped from the generated schema when the extension is disabled.
The .NET side owns validation so there is exactly one implementation; Studio consumes results through the existing compiler plumbing rather than re-implementing the schema in TypeScript.
🛠️ Implementation plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: One validated koine-extension.json manifest contributing eight declarative kinds, loading in all three hosts.
Architecture: A JSON Schema plus a .NET manifest model/validator in Koine.Compiler (single implementation, shared by CLI and both Studio hosts). Each Studio registry changes from a static literal to built-ins + contributions. Pure data end to end.
Step 1: Write the failing test in ExtensionManifestTests.cs — deserialize a minimal manifest (id/name/version/apiVersion/description/license) and assert Contributes defaults to every-list-empty and Capabilities to empty.
Step 2: Run dotnet test --filter "FullyQualifiedName~ExtensionManifestTests" → FAIL (ExtensionManifest not found).
Step 3: Implement ExtensionManifest + ExtensionContributions + the eight contribution records, with Parse(string json) over System.Text.Json and [JsonPropertyName] camelCase mapping. Default every collection to Array.Empty<T>().
Step 4: Author extensions/koine-extension.schema.json — draft 2020-12, additionalProperties: false at every level, required = the six fields above, mirroring the structure and prose style of templates/template.schema.json.
Step 5: Add the new public types to src/Koine.Compiler/PublicAPI.Unshipped.txt.
Step 6: Run the filter → PASS.
Step 7: Commit: feat(cli): add the koine-extension.json manifest model and JSON Schema
Produces: public static IReadOnlyList<Diagnostic> Validate(ExtensionManifest manifest, string extensionFolder, IReadOnlySet<string> knownCommandIds).
Step 1: Write failing tests — one per rule: id/folder mismatch, bad semver, a path escaping the folder ("../../etc/passwd"), an unknown runs command id, an un-namespaced settings key, an unknown contributes key, and a duplicate contribution id.
Step 2: Run dotnet test --filter "FullyQualifiedName~ExtensionManifestValidatorTests" → FAIL (validator not found).
Step 3: Implement Validate, allocating a KOI50xx diagnostic code per rule and registering each in DiagnosticCodes with a description, matching how existing codes are catalogued.
Step 4: Implement path containment by resolving the combined path and asserting it starts with the canonicalized extension folder — reject on escape. Do not hand-roll string prefix checks on un-normalized input.
Step 5: Run the filter → PASS.
Step 6: Commit: feat(cli): validate extension manifests with containment and namespacing rules
Task 3: Extension discovery + a koine ext validate command
Produces: public static IReadOnlyList<LoadedExtension> Discover(string extensionsRoot); public sealed record LoadedExtension(ExtensionManifest Manifest, string FolderPath, IReadOnlyList<Diagnostic> Diagnostics).
Step 1: Write the failing test — a temp dir with two extension folders, one valid and one whose id disagrees with its folder; assert both are returned and only the second carries diagnostics.
Step 2: Run dotnet test --filter "FullyQualifiedName~ExtensionLoaderTests" → FAIL.
Step 3: Implement Discover — enumerate immediate subfolders, read koine-extension.json, parse, validate, and return one LoadedExtension each. A malformed manifest yields a diagnostic, never an exception (mirroring EmitterLoader's never-crash-the-compile contract).
Step 4: Wire koine ext validate <path> in Program.cs to print diagnostics and exit non-zero when any are errors.
Step 1: Write the failing test in contributions.test.ts — assert mergeContributions appends a contributed FieldDef after built-ins, preserves built-in order, and drops contributions from a disabled extension.
it('appends contributed settings after built-ins and preserves built-in order',()=>{constmerged=mergeContributions(builtIns,[acmeContributions]);expect(merged.settings.slice(0,builtIns.settings.length)).toEqual(builtIns.settings);expect(merged.settings.at(-1)?.key).toBe('acme.compactMode');});
Step 2: Run npx vitest run --project '!storybook' src/extensions/contributions.test.ts → FAIL (module not found).
Step 3: Implement contributions.ts with mergeContributions, additive-only and deterministic (built-ins first, then contributions in extension-id order).
Step 4: Change SETTINGS_FIELDS and the launcher GROUPS/catalog construction to read through mergeContributions instead of being consumed directly, keeping the exported names stable so no call site changes.
Step 5: Run npx vitest run --project '!storybook' → PASS (the whole suite, to catch call sites that assumed a static array).
Step 6: Commit: feat(studio): open the settings and palette registries to extension contributions
Task 5: Open the remaining registries + schema-validation test suite
Create: extensions/examples/acme-dark/koine-extension.json (a real, valid worked example)
Interfaces:
Consumes: mergeContributions from Task 4; ExtensionLoader.Discover from Task 3.
Step 1: Write the failing test in ExtensionsValidationTests.cs — discover every extension under extensions/examples/, assert each validates against koine-extension.schema.json with zero error diagnostics. Model it on TemplatesValidationTests.
Step 2: Run dotnet test --filter "FullyQualifiedName~ExtensionsValidationTests" → FAIL (no example extension exists yet).
Step 3: Author extensions/examples/acme-dark/koine-extension.json exercising all eight contribution kinds, with the asset files it references.
Step 4: Route mcp.ts, theme.ts and diagramExport.ts through mergeContributions the same way Task 4 did for settings and the palette.
Step 5: Run dotnet test --filter "FullyQualifiedName~ExtensionsValidationTests" and npx vitest run --project '!storybook' → both PASS.
Step 6: Commit: feat(studio): route themes, MCP servers and export presets through extension contributions
Modify: README.md (construct/feature table — add the extension manifest)
Interfaces: none (docs only).
Step 1: Write authoring.md — the manifest reference, one worked example per contribution kind, the validation rules and their KOI50xx codes, and an explicit statement that the executable tier is reserved and not yet supported.
Step 2: Add the extension manifest to README.md's capability table, linking the docs page.
Step 3: Build the docs site to verify (npm run build-samples && npm run build-docs && npx astro build in website/ — the plain npm run build needs the CI-only wasm workload).
Step 4: Commit: docs: document authoring a declarative Koine extension
Problem / motivation
Persona: a Koine Studio user who wants to install a theme, a snippet pack or a starter domain someone else made — and the community member who wants to ship one.
Studio's contribution points are closed, hardcoded structures. Every one of them is a static literal a third party cannot reach:
tooling/koine-studio/src/settings/settingsSchema.tsSETTINGS_FIELDS: readonly FieldDef[]→ generatedSETTINGS_JSON_SCHEMAsrc/launcher/catalog.tsCatalogEntry,Categoryunion, staticGROUPS/MODES/KINDsrc/mcp/mcp.tsMCP_CLIENTS: readonly McpClientRecipe[]src/settings/theme.ts,appearance.tstemplates/*/template.jsontemplates/is the proof the pattern works here: a folder of data files plus atemplate.jsonvalidated againsttemplates/template.schema.json, withTemplatesValidationTestscompiling every template green and validating every manifest against the schema on eachdotnet test. It has three consumers already (the demo, Studio's template gallery, the website playground). What it lacks is generality — it describes exactly one kind of thing.Nothing else in Studio can be contributed by anyone outside the repo.
Proposed solution
Generalize the
templates/pattern into one extension manifest,koine-extension.json, validated by a published JSON Schema, describing an extension that contributes one or more declarative things. Pure data — no runtime, no sandbox, no toolchain, no executable code anywhere in this stage.{ "$schema": "https://koine.dev/extensions/koine-extension.schema.json", "id": "acme-dark", "name": "Acme Dark", "version": "1.2.0", "apiVersion": "0.1.0", "description": "A dark theme tuned for long modelling sessions.", "author": "Acme", "license": "MIT", "capabilities": [], "contributes": { "themes": [{ "id": "acme-dark", "label": "Acme Dark", "path": "./themes/acme-dark.json" }], "snippets": [{ "language": "koine", "path": "./snippets/aggregates.json" }], "templates": [{ "path": "./templates/subscriptions" }], "commands": [{ "id": "acme.openDocs", "title": "Acme: Open docs", "group": "action", "runs": "koine.openUrl", "args": { "url": "https://acme.example/docs" } }], "settings": [{ "key": "acme.compactMode", "type": "boolean", "default": false, "label": "Compact mode", "group": "appearance" }], "mcpServers": [{ "id": "acme-mcp", "label": "Acme MCP", "command": "npx", "args": ["-y", "@acme/mcp"] }], "exportPresets": [{ "id": "acme-svg", "label": "Acme SVG", "format": "svg", "options": { "scale": 2 } }], "severityProfiles": [{ "id": "acme-strict", "label": "Acme strict", "rules": { "KOI1203": "error" } }] } }Each
contributeskey maps onto one existing Studio registry, which stops being a static literal and becomes built-ins + contributions. Acommandsentry deliberately cannot carry code — it names an existing command id plus arguments, which is what keeps this tier declarative.The manifest is the single contract artifact for the whole epic: the executable extension kinds are reserved in the schema (and rejected by the validator with a clear "not yet supported" diagnostic) so that Stage 2 is additive rather than a redesign.
Alternatives considered
template.jsondirectly by adding keys to it. Tempting, but it would conflate "a Koine example domain" with "an installable extension" — templates would gain fields meaningless to them, and the 8 in-repo templates would all need migrating. Better:koine-extension.jsoncan contain atemplatescontribution that points at a folder whosetemplate.jsonis unchanged.extension.toml. JSON is what this repo already validates with (template.schema.json,SETTINGS_JSON_SCHEMA,settings.json), what Studio's settings editor already syntax-highlights, and what needs no new parser in the browser build.Area
Studio (IDE)
Related: #1936 (epic), #101 (the
templates/system this generalizes), #96 (MCP enable/disable prior art), #282 (backend-derived emit-target list)🧠 Brainstorm
Problem / context
The epic (#1936) establishes that a declarative-first model is the right shape, because three independently-verified constraints rule out hosting untrusted code in the near term (.NET cannot host wasm components; browsers cannot run them natively and the
jcoworkaround is AOT-only; no IDE ecosystem actually ships a hard runtime sandbox). This stage is the part of that plan that carries the product — the tier that works today, in every host, with no security surface at all.Zed's own documentation concedes the point: "most extensions will work properly without any Rust code present. In particular, only language server, context server and debugger extensions require the presence of custom Rust." Themes, icon themes, snippets and query-only language extensions ship as
extension.toml+ assets and nothing else.The critical property, and the reason this stage is worth doing first: pure data loads identically in all three Koine hosts. The Tauri webview, the browser tab and the
koineCLI can each consume a JSON manifest with no host-specific runtime, no native dependency, and no per-host capability gate. Nothing else in the epic has that property.Approaches
A — One manifest, N contribution keys. ✅ Recommended.
koine-extension.jsonwith acontributesobject. Matches VS Code'scontributesand Zed'sextension.tomlsections. One schema, one loader, one gallery entry, one capability list per extension.B — Per-kind manifests. A
theme.json, asnippets.json, etc., discovered by convention. Less upfront schema design, but the unit a user installs and toggles is an extension, not a file — so the gallery, the enable/disable state, the capability grant and the registry entry would all need to invent a grouping that (A) gets for free.C — Extend
template.jsonin place. Lowest new surface, but conflates two concepts and forces migration of the 8 existing templates. Rejected.Recommendation
Approach A. The
contributes-map shape is what both comparable systems converged on independently, it gives the gallery and capability model a natural unit, and it leaves room for the executable kinds to be added as further keys without touching anything built here.Two design choices worth calling out explicitly, both taken from verified evidence:
commandscontributions name an existing command id, not code. This is what keeps the tier declarative and is the boundary that stops "declarative" quietly becoming "we invented a scripting language."contributeskeys are a validation error, not ignored. Reserving the executable kinds now, and failing loudly on them, means a Stage 2 extension installed into a Stage 0 Studio gets a clear message rather than silently doing nothing.📋 Spec
Goal
One validated extension manifest that lets a third party contribute themes, snippets, project templates, palette entries, settings, MCP servers, export presets and diagnostic severity profiles — as pure data, loading identically in the Tauri webview, the browser tab and the CLI.
Scope
koine-extension.schema.json— the published JSON Schema (mirrors howtemplates/template.schema.jsonis published and validated).Koine.Compiler, so the CLI and both Studio hosts share one implementation.TemplatesValidationTests.Non-goals
capabilitiesarray but grants nothing, because nothing declarative needs a grant).Manifest shape
Required:
id(must equal the containing folder name, mirroringtemplate.json's rule),name,version(semver),apiVersion(semver, checked against the floor in Stage 1),description,license. Optional:author,repository,icon,tags,capabilities(default[]),contributes(default{}).additionalProperties: falsethroughout — the same choicetemplate.schema.jsonmakes, and what makes reserved-but-unimplemented keys detectable.Contribution keys
themessettings/theme.tspalettesnippetstemplatestemplates/gallerytemplate.jsoncommandslauncher/catalog.tsrunsnames an existing command id;argsis a JSON objectsettingssettings/settingsSchema.tsFieldDef-shaped entry per key; namespaced by extension idmcpServersmcp/mcp.tsexportPresetsexport/diagramExport.tsseverityProfileskoine.config[diagnostics]KOIxxxx→ severityValidation rules
idmatches^[a-z0-9][a-z0-9-]*$and equals the folder name.versionandapiVersionare valid semver.pathresolves inside the extension folder — rejected if it escapes, even at this tier where nothing executes (see the cross-cutting path-containment issue; the rule is cheaper to enforce from day one than to retrofit).commands[].runsvalue that names no known command is an error.settings[].keynot prefixed with the extension id is an error (prevents settings collisions).contributeskeys are an error naming the key and stating it is reserved.Assumptions
🛠️ Implementation plan
Goal: One validated
koine-extension.jsonmanifest contributing eight declarative kinds, loading in all three hosts.Architecture: A JSON Schema plus a .NET manifest model/validator in
Koine.Compiler(single implementation, shared by CLI and both Studio hosts). Each Studio registry changes from a static literal tobuilt-ins + contributions. Pure data end to end.Tech Stack: .NET 10 / C# (
System.Text.Json), JSON Schema draft 2020-12, TypeScript + Preact (registry wiring), xUnit v3 + Shouldly.Global Constraints
Ast/target-agnostic — the extension manifest is a tooling concept and must not leak into the semantic model.TreatWarningsAsErrors.Koine.CompilerAPI goes inPublicAPI.Unshipped.txt(RS0016 gate).Directory.Build.props<Version>.git -c user.email=phmatray@gmail.com -c user.name="Philippe Matray".npx vitest run --project '!storybook'locally.dotnet test --filter "FullyQualifiedName~<Suite>".Task 1: Manifest model + JSON Schema
Files:
src/Koine.Compiler/Extensions/ExtensionManifest.csextensions/koine-extension.schema.jsontests/Koine.Compiler.Tests/ExtensionManifestTests.csInterfaces:
Produces:
public sealed record ExtensionManifest(string Id, string Name, string Version, string ApiVersion, string Description, string License, string? Author, string? Repository, string? Icon, IReadOnlyList<string> Tags, IReadOnlyList<ExtensionCapability> Capabilities, ExtensionContributions Contributes);public sealed record ExtensionContributions(IReadOnlyList<ThemeContribution> Themes, IReadOnlyList<SnippetContribution> Snippets, IReadOnlyList<TemplateContribution> Templates, IReadOnlyList<CommandContribution> Commands, IReadOnlyList<SettingContribution> Settings, IReadOnlyList<McpServerContribution> McpServers, IReadOnlyList<ExportPresetContribution> ExportPresets, IReadOnlyList<SeverityProfileContribution> SeverityProfiles).Step 1: Write the failing test in
ExtensionManifestTests.cs— deserialize a minimal manifest (id/name/version/apiVersion/description/license) and assertContributesdefaults to every-list-empty andCapabilitiesto empty.dotnet test --filter "FullyQualifiedName~ExtensionManifestTests"→ FAIL (ExtensionManifestnot found).ExtensionManifest+ExtensionContributions+ the eight contribution records, withParse(string json)overSystem.Text.Jsonand[JsonPropertyName]camelCase mapping. Default every collection toArray.Empty<T>().extensions/koine-extension.schema.json— draft 2020-12,additionalProperties: falseat every level,required= the six fields above, mirroring the structure and prose style oftemplates/template.schema.json.src/Koine.Compiler/PublicAPI.Unshipped.txt.feat(cli): add the koine-extension.json manifest model and JSON SchemaTask 2: Manifest validator with the seven rules
Files:
src/Koine.Compiler/Extensions/ExtensionManifestValidator.cstests/Koine.Compiler.Tests/ExtensionManifestValidatorTests.csInterfaces:
Consumes:
ExtensionManifestfrom Task 1.Produces:
public static IReadOnlyList<Diagnostic> Validate(ExtensionManifest manifest, string extensionFolder, IReadOnlySet<string> knownCommandIds).Step 1: Write failing tests — one per rule: id/folder mismatch, bad semver, a
pathescaping the folder ("../../etc/passwd"), an unknownrunscommand id, an un-namespaced settings key, an unknowncontributeskey, and a duplicate contribution id.dotnet test --filter "FullyQualifiedName~ExtensionManifestValidatorTests"→ FAIL (validator not found).Validate, allocating aKOI50xxdiagnostic code per rule and registering each inDiagnosticCodeswith a description, matching how existing codes are catalogued.feat(cli): validate extension manifests with containment and namespacing rulesTask 3: Extension discovery + a
koine ext validatecommandFiles:
src/Koine.Compiler/Extensions/ExtensionLoader.cssrc/Koine.Cli/Program.cs(register theextcommand branch)tests/Koine.Compiler.Tests/ExtensionLoaderTests.csInterfaces:
Consumes:
ExtensionManifest,ExtensionManifestValidator.Produces:
public static IReadOnlyList<LoadedExtension> Discover(string extensionsRoot);public sealed record LoadedExtension(ExtensionManifest Manifest, string FolderPath, IReadOnlyList<Diagnostic> Diagnostics).Step 1: Write the failing test — a temp dir with two extension folders, one valid and one whose
iddisagrees with its folder; assert both are returned and only the second carries diagnostics.Step 2: Run
dotnet test --filter "FullyQualifiedName~ExtensionLoaderTests"→ FAIL.Step 3: Implement
Discover— enumerate immediate subfolders, readkoine-extension.json, parse, validate, and return oneLoadedExtensioneach. A malformed manifest yields a diagnostic, never an exception (mirroringEmitterLoader's never-crash-the-compile contract).Step 4: Wire
koine ext validate <path>inProgram.csto print diagnostics and exit non-zero when any are errors.Step 5: Run the filter → PASS.
Step 6: Commit:
feat(cli): discover extensions and add koine ext validateTask 4: Open the Studio settings + palette registries to contributions
Files:
tooling/koine-studio/src/settings/settingsSchema.tstooling/koine-studio/src/launcher/catalog.tstooling/koine-studio/src/extensions/contributions.tstooling/koine-studio/src/extensions/contributions.test.tsInterfaces:
Produces:
export interface ExtensionContributionSet { settings: FieldDef[]; commands: CatalogEntry[]; themes: ThemeDef[]; snippets: SnippetDef[]; mcpServers: McpClientRecipe[]; exportPresets: ExportPreset[]; severityProfiles: SeverityProfile[] };export function mergeContributions(builtIns: ExtensionContributionSet, contributed: ExtensionContributionSet[]): ExtensionContributionSet.Step 1: Write the failing test in
contributions.test.ts— assertmergeContributionsappends a contributedFieldDefafter built-ins, preserves built-in order, and drops contributions from a disabled extension.npx vitest run --project '!storybook' src/extensions/contributions.test.ts→ FAIL (module not found).contributions.tswithmergeContributions, additive-only and deterministic (built-ins first, then contributions in extension-id order).SETTINGS_FIELDSand the launcherGROUPS/catalog construction to read throughmergeContributionsinstead of being consumed directly, keeping the exported names stable so no call site changes.npx vitest run --project '!storybook'→ PASS (the whole suite, to catch call sites that assumed a static array).feat(studio): open the settings and palette registries to extension contributionsTask 5: Open the remaining registries + schema-validation test suite
Files:
tooling/koine-studio/src/mcp/mcp.ts,src/settings/theme.ts,src/export/diagramExport.tstests/Koine.Compiler.Tests/ExtensionsValidationTests.csextensions/examples/acme-dark/koine-extension.json(a real, valid worked example)Interfaces:
Consumes:
mergeContributionsfrom Task 4;ExtensionLoader.Discoverfrom Task 3.Step 1: Write the failing test in
ExtensionsValidationTests.cs— discover every extension underextensions/examples/, assert each validates againstkoine-extension.schema.jsonwith zero error diagnostics. Model it onTemplatesValidationTests.Step 2: Run
dotnet test --filter "FullyQualifiedName~ExtensionsValidationTests"→ FAIL (no example extension exists yet).Step 3: Author
extensions/examples/acme-dark/koine-extension.jsonexercising all eight contribution kinds, with the asset files it references.Step 4: Route
mcp.ts,theme.tsanddiagramExport.tsthroughmergeContributionsthe same way Task 4 did for settings and the palette.Step 5: Run
dotnet test --filter "FullyQualifiedName~ExtensionsValidationTests"andnpx vitest run --project '!storybook'→ both PASS.Step 6: Commit:
feat(studio): route themes, MCP servers and export presets through extension contributionsTask 6: Documentation
Files:
website/src/content/docs/extensions/authoring.mdREADME.md(construct/feature table — add the extension manifest)Interfaces: none (docs only).
authoring.md— the manifest reference, one worked example per contribution kind, the validation rules and theirKOI50xxcodes, and an explicit statement that the executable tier is reserved and not yet supported.README.md's capability table, linking the docs page.npm run build-samples && npm run build-docs && npx astro buildinwebsite/— the plainnpm run buildneeds the CI-only wasm workload).docs: document authoring a declarative Koine extension