Logo FS-Skia-UI

FS-Skia-UI Template Framework Proposal

Date: 2026-05-14

Status: proposal for turning this repository into a governed template framework for new F# / Elmish / Skia products.

Executive Recommendation

FS-Skia-UI should become a template framework, but not by turning the runtime library into a broad application framework or by asking agents to copy patterns manually. The strongest shape is a governed project family with four explicit mechanisms:

  1. dotnet new creates the initial repository shape.
  2. FAKE owns the stable command surface for humans, agents, and CI.
  3. Spec Kit presets and templates define artifact shape, methodology, and required evidence.
  4. Spec Kit extensions and project-local skills run gates that verify contracts, evidence, layout, visuals, packages, and template drift.

The highest-value next step is still a canonical FAKE build graph, but the proposal should be sharper than "add FAKE". The build graph must become the single source of truth for restore, build, test, pack, smoke, documentation, evidence, template installation, and generated baseline workflows. CI and Speckit commands should call FAKE targets instead of recreating command order.

Recommended sequencing:

  1. Add FAKE and central package governance.
  2. Move evidence and surface checks behind FAKE targets.
  3. Strengthen the Speckit preset so generated specs, plans, and tasks carry the repo's UI-specific obligations.
  4. Add dotnet new template packaging and a TemplateCheck target.
  5. Add layout, visual, package, dependency, and template-drift extensions/gates.

This order matters. A template package is much more valuable after the build and evidence behavior is already executable.

Research Basis

This analysis combines local repository inspection with primary upstream documentation:

Key external findings:

Local Baseline

The repository already has a strong seed for a governed product family:

The current shape is less like a normal "starter app" and more like a governed library/product template. New projects should inherit the governance, not only the code layout.

Current Source Links

This proposal was written as an implementation direction. The current source files that now carry those concerns are:

Concern

Current source

Build graph and V2 targets

build.fsx, fake.sh, fake.cmd

Template metadata and package

.template.config/template.json, .template.package/FS.Skia.UI.Template.fsproj

Central package versions

Directory.Packages.props

Core scene, viewer, diagnostics

src/Lib/Library.fsi, src/Lib/Library.fs

Keyboard input

src/Lib/KeyboardInput.fsi, src/Lib/KeyboardInput.fs

Charts and DataGrid

src/Charts

Layout and graph

src/Layout

Governance scripts

scripts/dependency-report.fsx, scripts/template-drift.fsx, scripts/refresh-surface-baselines.fsx

Tests

tests

Surface baselines

readiness/surface-baselines

Spec Kit assets

.specify

Current Gaps

The proposal should explicitly close these gaps:

  1. There is no canonical build graph.

    The repository has FS-Skia-UI.sln, scripts, tests, samples, and evidence artifacts, but no build.fsx, fake.sh, or fake.cmd. Agents and CI currently have to infer command order.

  2. Package versions are repeated in project files.

    Expecto, Microsoft.NET.Test.Sdk, YoloDev.Expecto.TestSdk, Silk.NET packages, SkiaSharp preview packages, Yoga.Net, YamlDotNet, and sample package validation versions are declared in individual .fsproj files. This is workable for one repo, but weak for a reusable template.

  3. Surface baselines are anchored to a feature directory.

    tests/Package.Tests/SurfaceAreaTests.fs reads baselines from specs/002-skia-feature-parity/readiness/surface-baselines. That is acceptable historical evidence, but it is not a template-owned baseline location. A template project needs a stable current baseline path, with feature readiness folders storing feature-specific evidence.

  4. The Speckit preset does not yet own enough generated artifact shape.

    The current preset provides constitution and task behavior. It does not yet replace the base spec and plan templates with UI-specific fields such as package impact, FSI impact, MVU impact, layout impact, Skia/native-resource impact, visual evidence, package impact, and build target impact.

  5. The evidence extension is necessary but not sufficient.

    It validates tasks.deps.yml and synthetic propagation. The template also needs contract, layout, visual, package, dependency, build-health, and template-drift gates.

  6. There is no dotnet new template metadata.

    No .template.config/template.json exists, so the repo cannot yet be installed, instantiated, smoke-tested, or packed as a template package.

  7. Samples are useful but not uniformly contract-smokeable.

    Several samples support package-reference validation through FsSkiaUiPackageVersion, while others use project references. The template should define a uniform sample contract: every sample either has a non-interactive --contract-smoke path or is explicitly marked visual/manual.

  8. Documentation is not yet a maintained docs set.

    docs/ currently contains this analysis file only. A template framework needs persistent docs for profile, architecture, build, testing, Speckit, dependencies, evidence, rendering boundaries, layout boundaries, and ADRs.

Design Decision Summary

Decision

Recommendation

Rationale

Initial repo creation

Use dotnet new

It is the native .NET template mechanism, supports normal source folders, template.json, sourceName, symbols, optional content, installation from directories, and NuGet template packaging.

Incremental scaffolding

Use FAKE scaffold targets

dotnet new is good for first creation; FAKE is better for adding a package, test project, sample, baseline, or readiness folder to an existing repo.

Build orchestration

Use FAKE as canonical

FAKE is F#, typed enough for maintainable build logic, cross-platform, and can expose stable targets to agents and CI.

Package version governance

Start with NuGet Central Package Management

It fits SDK-style projects and removes duplicated versions. Paket remains a later option if repo-wide transitive lock behavior becomes more important than simpler SDK-native restore.

Lock policy

Use locked restore for app/template checks, not as the library contract

NuGet lock files are strongest at application roots. For packable library projects, lock files should be evidence for this repo's verification, not a promise to downstream consumers.

Spec Kit artifact shape

Use presets

Presets should own spec, plan, task, constitution, and command text.

Spec Kit executable gates

Use extensions

Extensions should add contract, layout, visual, build-health, template-drift, and release-pack commands.

Agent behavior

Use project-local skills

Skills are the right layer for agent workflows that call the above targets and enforce local conventions.

MVU and rendering boundary

Keep view pure and declarative

Elmish and the local constitution both point to pure state transitions and declarative views. Skia native resources stay at the host edge.

Yoga boundary

Keep Yoga.Net internal to src/Layout

Public APIs should be idiomatic F# records, unions, options, and results. Yoga.Net objects, callbacks, node lifetimes, and disposal remain implementation details.

Target Architecture

The template should make the rendering and layout pipeline explicit:

Application model
  -> update Msg Model
  -> view Model dispatch
  -> declarative scene/layout/widget tree
  -> layout evaluation in logical coordinates
  -> render planning
  -> Skia host draws through SKSurface/SKCanvas
  -> input/hit-test reports messages back to update

Layer responsibilities:

Layer

Owns

Must not own

Application/MVU

Model, Msg, init, update, effects, subscriptions

SKCanvas, GPU handles, long-lived native resources

View declaration

Pure functions from model to scene/layout/widget data

I/O, mutable host state, direct drawing as the default path

Layout

Logical bounds, flex intent, measurement contracts, diagnostics, invalidation

Renderer state, GPU resources, product workflow state

Render planning

Conversion from computed tree to render commands

Window lifetime, dependency restore, feature specs

Skia host

Window loop, Vulkan/GPU setup, SKSurface, SKCanvas, screenshots, disposal

Domain decisions, business state transitions

Evidence

FSI transcripts, baselines, smoke output, screenshots, performance reports

Hidden manual proof that cannot be regenerated

Governance

Constitution, Speckit preset, extensions, task graph, template drift

Runtime product code

This separation prevents two common failure modes:

Template-Owned vs Product-Owned

The template should freeze framework shape, not product history.

Template-owned:

Product-owned after instantiation:

Do not put existing feature history such as specs/001-* through specs/005-* into the distributed template. Include a specs/000-template-profile/ or docs page that explains the inherited process and shows one minimal example.

Recommended Repository Shape

Target shape for an instantiated project:

.
|-- .config/
|   `-- dotnet-tools.json
|-- .specify/
|   |-- memory/
|   |-- presets/
|   `-- extensions/
|-- .agents/
|   `-- skills/
|-- .template.config/
|   `-- template.json
|-- docs/
|   |-- template-profile.md
|   |-- architecture.md
|   |-- build.md
|   |-- testing.md
|   |-- speckit.md
|   |-- dependencies.md
|   |-- evidence.md
|   |-- rendering-boundaries.md
|   |-- layout-boundaries.md
|   `-- adr/
|-- samples/
|-- scripts/
|-- specs/
|   `-- 000-template-profile/
|-- src/
|-- tests/
|-- Directory.Build.props
|-- Directory.Packages.props
|-- FS-Skia-UI.sln
|-- NuGet.config
|-- build.fsx
|-- fake.cmd
`-- fake.sh

For this repository itself, keep existing feature directories. For the template package, exclude old feature directories and generated bin/, obj/, readiness logs that are not canonical examples, local caches, and machine-specific files.

dotnet new Template Contract

The template package should be a normal .NET template package, not a custom generator.

Minimum template.json responsibilities:

Recommended template smoke command:

dotnet new install ./artifacts/templates/FS.Skia.UI.Template.*.nupkg
dotnet new fs-skia-ui -n TemplateSmoke --include-charts true --include-layout true
cd TemplateSmoke
./fake.sh build -t Dev
./fake.sh build -t TemplateVerify

TemplateCheck should instantiate into a temporary directory, run restore/build/tests, run at least one sample --contract-smoke, and verify no unreplaced template tokens remain.

FAKE Build Graph

FAKE should be the one command language. CI YAML, Speckit hooks, local docs, and agent skills should call named targets.

Recommended target graph:

Clean
  ==> Restore
  ==> Build
  ==> Test
  ==> PackageSurfaceCheck
  ==> FsiTranscripts
  ==> SampleContractSmoke
  ==> EvidenceGraph
  ==> EvidenceAudit
  ==> Verify

Build
  ==> Pack
  ==> PackLocal
  ==> PackageSmoke

Build
  ==> LayoutEvidence
  ==> VisualEvidence

Restore
  ==> DependencyReport

Restore
  ==> TemplateInstall
  ==> TemplateInstantiate
  ==> TemplateSmoke
  ==> TemplateCheck

Command surface:

./fake.sh build -t Dev
./fake.sh build -t Verify
./fake.sh build -t Ci
./fake.sh build -t Visual
./fake.sh build -t LayoutEvidence
./fake.sh build -t PackLocal
./fake.sh build -t PackageSmoke
./fake.sh build -t TemplateCheck
./fake.sh build -t DependencyReport

Target responsibilities:

Target

Responsibility

Output

Dev

Fast local restore/build/unit tests

Console output and test result

Verify

Full non-GPU verification

Readiness logs and evidence verdict

Ci

Non-interactive CI alias for Verify plus locked restore where configured

CI logs

Visual

GPU/window/screenshot checks

Screenshot metadata and smoke logs

LayoutEvidence

Deterministic layout reports, Yoga version, invalidation locality

readiness/layout/*.json and .txt

PackLocal

Pack all packable projects to ~/.local/share/nuget-local/

.nupkg files

PackageSmoke

Validate sample or consumer projects against packed packages

package smoke logs

TemplateCheck

Install and instantiate template in temp directory

template smoke logs

DependencyReport

Print package graph, pin owners, licenses, preview risk

readiness/dependencies.md

EvidenceGraph

Validate task DAG and propagation

task-graph.json, task-graph.md

EvidenceAudit

Diff scan and synthetic gate

audit verdict

CI must call ./fake.sh build -t Ci. It should not duplicate target order in YAML.

Package and Dependency Policy

Use NuGet Central Package Management first.

Concrete changes:

Recommended lock policy:

Paket decision:

Speckit Constitution Improvements

The current constitution is strong. For template use, add these principles or subsections:

  1. Template Contract Is Source-Controlled

    New projects inherit a versioned template profile. Any deviation from template-owned layout, build graph, evidence graph, package policy, or sample contract must be recorded in docs/template-deviations.md.

  2. View Functions Produce Declarative UI Data

    view must return framework scene, layout, and widget data. It must not perform I/O, mutate host state, allocate long-lived native resources, or draw directly to SKCanvas, except through a documented custom-render escape hatch with tests and sample evidence.

  3. Skia Resources Live at the Host Edge

    SKSurface, SKCanvas, Vulkan contexts, images, fonts, and native handles are owned and disposed by host or renderer modules. Domain, layout, widget, and MVU modules may describe resources but not own backend lifetimes.

  4. Layout Is Logical Before Physical

    Layout operates in logical coordinates. Pixel snapping, DPI scaling, and backend rounding happen at render and hit-test boundaries and must be deterministic.

  5. Build Graph Is Canonical

    build.fsx targets are the authoritative local and CI workflow. CI may call FAKE targets but must not reimplement restore/build/test/pack/smoke/evidence sequencing in YAML.

  6. Template Drift Is a Defect

    If a feature adds a new test category, public module type, sample pattern, readiness artifact, script, dependency policy, or build target, the corresponding template preset, docs, and FAKE target must be updated or an explicit deferral recorded.

  7. Golden Evidence Has Owners

    Surface baselines, screenshots, FSI transcripts, layout reports, performance files, and generated task graphs must be reproducible by named build targets. Hand-edited evidence is allowed only when explicitly marked.

  8. Dependencies Have Owners

    Every dependency must have a pinning policy, maintenance owner, upgrade command, license note, preview-risk note when applicable, and evidence target.

These updates should be applied to both .specify/memory/constitution.md and the preset's constitution-template.md.

Speckit Preset Improvements

The current fsharp-opinionated preset should become the project-family preset. It can remain one preset initially, but its sections should be organized as if it contains three layered concerns:

Preset-owned templates should include:

Required generated spec fields:

Required generated plan fields:

Required generated tasks:

Spec Kit Extensions and Skills

The current evidence extension should remain. Add the following project-local extensions.

speckit.contract

Purpose: verify public API discipline before implementation and before merge.

Inputs:

Checks:

Outputs:

speckit.layout-evidence

Purpose: make layout behavior reproducible and reviewable without screenshots.

Inputs:

Checks:

Outputs:

speckit.visual-evidence

Purpose: distinguish real visual behavior from unverified sample code.

Inputs:

Checks:

Outputs:

speckit.build-health

Purpose: summarize canonical build health for review.

Inputs:

Outputs:

speckit.template-drift

Purpose: prevent feature work from adding one-off conventions.

Checks whether a diff added:

Outputs:

Failure rule:

speckit.release-pack

Purpose: validate local packages before publishing or consumption by sample projects.

Checks:

Outputs:

Skills should wrap these extensions and FAKE targets, not duplicate their logic.

Testing Strategy

Keep test categories explicit and template-owned:

Category

Purpose

Default target

Pure domain tests

public pure functions

Test

MVU transition tests

Model + Msg -> Model + Effect

Test

Effect interpreter tests

real filesystem/process/window dependencies where safe

Verify

Layout tests

logical bounds, diagnostics, invalidation, snapping

LayoutEvidence

Render planning tests

scene to render commands without GPU where possible

Test

Smoke tests

sample entry points and contract smoke

SampleContractSmoke

Visual evidence tests

screenshots and metadata

Visual

Package tests

packed-library and public-surface validation

PackageSmoke

Performance checks

bounded deterministic micro-scenarios

LayoutEvidence or dedicated target

Expecto implications:

Layout and Rendering Policy

Yoga-backed layout:

Skia rendering:

Documentation Set

Create this documentation structure:

docs/
|-- template-profile.md
|-- architecture.md
|-- build.md
|-- testing.md
|-- speckit.md
|-- dependencies.md
|-- evidence.md
|-- rendering-boundaries.md
|-- layout-boundaries.md
|-- migration.md
`-- adr/
    |-- 0001-template-governance.md
    |-- 0002-fake-build-graph.md
    |-- 0003-nuget-central-package-management.md
    |-- 0004-elmish-view-boundary.md
    |-- 0005-skia-host-edge.md
    `-- 0006-yoga-layout-boundary.md

Docs should explain decisions and regeneration commands. Do not make docs the only source of truth for command ordering; docs should reference FAKE targets.

Risks and Mitigations

Risk

Impact

Mitigation

Template becomes too heavy

New products start slow and users bypass it

Provide Dev, Verify, and Visual targets; make visual/GPU evidence explicit and separable.

dotnet new is used for incremental changes

Existing repos get overwritten or drift

Use dotnet new only for first creation; use FAKE scaffold targets for additions.

Build logic duplicates in CI

Different results locally and remotely

CI calls ./fake.sh build -t Ci; YAML contains environment setup only.

Package versions drift

Repeated updates and inconsistent restores

Use Directory.Packages.props, dependency report, and no-inline-version tests.

Lock files are misunderstood

False confidence for library consumers

Document app-root vs library lock semantics; use locked restore only where intentional.

Visual tests fail due environment

False negatives on machines without GPU/window support

Require explicit skip diagnostics with OS, renderer, GPU mode, and fallback reason.

Agents add one-off scripts

Template loses governance value

Template-drift gate checks scripts, targets, docs, and preset updates.

Yoga.Net leaks into public API

Public contract inherits C# lifecycle/disposal details

Keep Yoga.Net out of .fsi; expose F# data only.

Direct Skia drawing bypasses MVU

Tests cannot reason about behavior

Require declarative view by default and documented custom-render escape hatch.

Surface baselines stay feature-local

Future features patch old readiness folders

Move current baselines to a template-owned path and let feature readiness store evidence copies.

Success Metrics

A template framework milestone is successful when:

Implementation Roadmap

Phase 1: Codify Current Practice

Deliverables:

Exit criteria:

Phase 2: Package Governance

Deliverables:

Exit criteria:

Phase 3: Speckit Hardening

Deliverables:

Exit criteria:

Phase 4: Deterministic Scaffolding

Deliverables:

Exit criteria:

Phase 5: UI Evidence

Deliverables:

Exit criteria:

Phase 6: Local Package Validation and Publishing

Deliverables:

Exit criteria:

First PR Recommendation

The first PR should be intentionally narrow:

  1. Add FAKE bootstrap.
  2. Add Dev, Verify, PackLocal, PackageSurfaceCheck, FsiTranscripts, EvidenceGraph, and EvidenceAudit targets.
  3. Move surface baselines to a stable current path or add a compatibility target that writes both current and feature-readiness copies.
  4. Add docs/reports/build.md and docs/reports/evidence.md.
  5. Update Speckit task template so generated tasks call FAKE targets.

Do not start with .template.config/template.json. Without the build graph, the template can create files but cannot prove that the created project is valid.

Final Position

FS-Skia-UI should become a template framework by making its constraints executable:

The proposal should treat drift as the main enemy. The codebase already has many good conventions; the template framework work is about making those conventions reproducible, testable, and hard to bypass.

Type something to start searching.