Logo FS-Skia-UI

Agent Consumer Framework Analysis

Timestamp: 2026-05-28T15:57:01+0200 Updated: 2026-05-28 after the 027-generated-evidence-workflow merge and follow-up package/template pin commits.

This analysis evaluates FS.Skia.UI as a framework whose primary generated consumer is an autonomous agent running Spec Kit, not a human developer typing commands by hand. Under that assumption, the governance harness is not mainly a usability burden. It is part of the framework contract: it gives an agent command entry points, evidence paths, failure classes, and readiness obligations that make completion auditable. The main risks are therefore not "too many commands" or "too much ceremony" by themselves. The risks are slow routing, ambiguous failure ownership, stringly typed public control contracts, and a custom build target interpreter that duplicates parts of FAKE while remaining harder for external tools to understand.

Scope And Evidence

This document is based on local repository inspection and primary external documentation.

Local sources:

External references:

Reframed Consumer Model

If the consumer is a Spec Kit agent, framework quality is measured differently than for a manually operated app starter.

Human-first questions:

Agent-first questions:

This repository already answers several of those questions better than a minimal app template would. The current harness gives agents stable FAKE targets, readiness directories, command logs, generated product validation, focused-gate summaries, evidence graph and audit outputs, and synthetic-evidence disclosure.

The improvement target should therefore be "more deterministic and cheaper for agents" rather than "less governed".

Current Strengths

1. The Runtime Boundary Is Sound

The conceptual runtime split is strong:

Product Model/Msg/update/view
  -> Scene or Control declarations
  -> Viewer host edge
  -> window, input, Skia/Vulkan, screenshots, diagnostics, shutdown

This aligns with Elmish's UI-independent MVU description: immutable model, message type, pure init/update/view, and commands. FS.Skia.UI does not require product logic to own the native window, SKCanvas, GPU handles, or screenshot machinery.

For agents, this is valuable because semantic tests can exercise reducers, view functions, scene descriptions, control diagnostics, and layout evidence without needing a reliable desktop session.

2. The Governance Harness Provides Real Agent Affordances

The FAKE target surface in build.fsx defines named operations that an agent can run and cite:

The target outputs are not only pass/fail values. They also produce logs, readiness Markdown files, JSON outputs, package surfaces, generated product file lists, and focused-gate summaries. This is a strong design for an agentic workflow because it creates durable evidence instead of relying on the final assistant response.

After 027-generated-evidence-workflow, generated EvidenceGraph and EvidenceAudit targets are no longer completion-only placeholders. Generated projects delegate to the authoritative Spec Kit evidence script, record authority=delegated-authoritative, surface the generated project identity, exit code, validation area, diagnostics, and write command reports under readiness/.

Focused gates also have stronger in-repository metadata than the original analysis observed: command, direct prerequisites, log path, readiness path, stale build/restore assumptions, and verdict category are represented in the build workflow and documented in docs/reports/build.md. This improves direct invocation and evidence review, but it is still not a changed-path validation manifest.

3. Unsupported Host Behavior Is Treated As A Contract

The generated app and viewer evidence surfaces distinguish:

That distinction matters for agents. Without it, an agent often treats a CI or desktop-session limitation as a product failure, or worse, relabels a fallback artifact as visual proof.

4. Generated Consumers Validate Packages, Not Source Copies

The generated consumer process uses local NuGet packages from PackLocal. That is the right contract for a framework. It verifies the public package surface a real generated product will consume, not repository internals.

5. Public .fsi Files Make Surface Review Concrete

The paired .fsi style creates a reviewable contract. This is especially important for an agent-driven framework because agents tend to add helper functions opportunistically. The .fsi boundary makes public API expansion visible.

Main Risks

1. Agent Routing Is Too Implicit

The repository documents many gates, but the gate selection rule is not yet a first-class machine-readable contract. An agent can read docs/reports/build.md, but it still has to infer which gates are authoritative for a given change.

Example: a change under src/Controls may need some subset of:

Those requirements are understandable to a maintainer, but an agent needs an explicit mapping from changed paths and feature risk to gates.

Risk: the agent either over-runs expensive broad validation or under-runs the proof needed for final readiness.

2. Broad Validation Has High Latency And Large Failure Surface

Verify depends on many targets, including template validation, generated product validation, dependency checks, guidance checks, drift checks, graph generation, and audit. That is appropriate for final authority, but it is a poor default inner loop.

The problem is not that broad validation exists. The problem is that without a manifested routing contract, broad validation becomes the only obviously safe choice for an agent that wants to avoid missing proof.

Risk: the agent spends most of its time proving unrelated matrix rows, and environment-sensitive failures obscure product defects.

3. Some Generated App Policy Lives In The Product Entrypoint

template/base/src/Product/Program.fs contains the normal product launch path plus many evidence modes:

This is not a human burden in the agent-consumer model, but it is still an architecture risk. The product executable becomes both app and policy harness. That makes the generated product's command-line surface part of governance compatibility.

Risk: evidence policy evolution forces generated app churn and increases the chance that a product command accidentally claims stronger proof than it has.

4. Controls Are Productive But Stringly Typed Internally And Publicly

The Controls surface exposes convenient typed modules such as Button, TextBox, LineChart, and DataGrid, but the underlying contract is largely string based:

type ControlId = string
type ControlKind = string

type Control<'msg> =
    { Kind: ControlKind
      Key: ControlId option
      Attributes: Attr<'msg> list
      Children: Control<'msg> list
      Content: string option
      Accessibility: AccessibilityMetadata option }

and Attr<'msg> =
    { Name: string
      Category: AttrCategory
      Value: AttrValue<'msg> }

Attr.create accepts arbitrary names. Event kinds are strings. Several data paths use UntypedValue of obj. ControlInternals.required maps required attributes from string control kinds to string attribute names. Event binding normalization maps string attribute names such as onClick and onChanged to event names such as click and changed.

This design is flexible and simple to extend. It is also weakly typed at the point where agents most need compile-time guardrails.

Observed examples:

Risks:

5. Build Graph Duplicates FAKE Target Infrastructure

The current build.fsx implements a custom target system:

FAKE already provides target definition, dependency operators, target listing, single-target execution, and parallel traversal support through its target module. The repository's custom layer buys something useful: a pure transition model that can be tested and a single place to emit structured reports. But it also means external FAKE conventions are bypassed.

Risks:

This is not automatically wrong. It is a deliberate tradeoff: testable build workflow algebra versus idiomatic FAKE integration. The current implementation leans heavily toward the custom algebra.

Recommendations

Recommendation 1: Add An Agent Validation Manifest

Add a machine-readable validation contract, for example:

validation.contract.yml

The manifest should map path patterns, capability ids, and feature risk levels to required gates, expected artifacts, timeout class, and failure ownership.

Sketch:

version: 1
default:
  inner_loop: [Dev]
  final: [EvidenceGraph, EvidenceAudit, Verify]

rules:
  - id: controls-public-api
    paths:
      - src/Controls/**/*.fsi
      - src/Controls/**/*.fs
    gates:
      focused:
        - ControlsCatalogCheck
        - ControlsInteractionCheck
        - ControlsRenderingCheck
      surface:
        - PackageSurfaceCheck
        - FsiTranscripts
      generated_consumer:
        - GeneratedProductCheck
    artifacts:
      - readiness/control-catalog.md
      - readiness/interaction-tests.md
      - readiness/layout-rendering.md
    failure_owner: product

  - id: template-owned-change
    paths:
      - template/**
      - .template.config/**
      - template/profiles/**
    gates:
      focused:
        - TemplateCheck
        - GeneratedProductCheck
        - TemplateDrift
    failure_owner: template

  - id: speckit-evidence-workflow
    paths:
      - .specify/extensions/evidence/**
      - specs/**/tasks.md
      - specs/**/tasks.deps.yml
    gates:
      focused:
        - EvidenceGraph
        - EvidenceAudit
        - GeneratedGuidanceCheck
    failure_owner: governance

Agent benefit:

Recommendation 2: Introduce Validation Tiers

Keep the existing target names, but formalize tier semantics.

Suggested tiers:

Tier

Purpose

Example targets

inner-loop

fast product correctness

Dev, targeted tests

focused-authority

authoritative proof for one changed concern

ControlsRenderingCheck, TemplateCheck, GeneratedProductCheck

agent-ready

minimum feature-complete proof selected by manifest

path-derived focused gates plus EvidenceGraph and EvidenceAudit

maintainer-verify

broad repository confidence

Verify

automation-final

non-interactive final authority

Ci

The new tier worth adding is AgentReady. It should not be a static clone of Verify. It should read the validation manifest and run only required focused gates plus evidence graph/audit.

Recommendation 3: Produce One Consolidated Agent Verdict

Every focused and broad validation path should converge into one machine-readable file, for example:

readiness/agent-verdict.json

Minimum fields:

{
  "status": "passed|failed|unsupported|degraded",
  "authority": "non-authoritative|focused-authoritative|broad-authoritative",
  "target": "ControlsRenderingCheck",
  "changed_rule_ids": ["controls-public-api"],
  "required_gates": ["ControlsRenderingCheck", "PackageSurfaceCheck"],
  "completed_gates": ["ControlsRenderingCheck"],
  "missing_gates": ["PackageSurfaceCheck"],
  "failure_owner": "product|environment|template|governance|prerequisite",
  "next_command": "./fake.sh build -t PackageSurfaceCheck",
  "artifacts": ["readiness/layout-rendering.md"],
  "diagnostics": []
}

This should supplement, not replace, the existing Markdown evidence. Agents need a compact routing artifact, while reviewers still benefit from readable reports.

Recommendation 4: Keep Evidence Commands, But Move Policy Out Of Product Main

Generated products need evidence commands. They should not have to own all evidence policy in Program.fs.

Recommended direction:

Preferred generated shape:

src/Product/Program.fs          normal app launch and thin arg dispatch
src/Product/Evidence.fs         product evidence adapters
tests/Product.Tests             semantic checks
build.fsx                       orchestration and report contract

This keeps generated product code testable while reducing policy duplication inside the app entrypoint.

Recommendation 5: Gradually Type The Controls Contract

Do not remove the flexible Control and Attr representation immediately. It is useful for generic rendering, diagnostics, catalogs, and generated controls. Instead, add typed front doors while preserving the existing representation as the lowered form.

5.1 Add Typed Control Kinds

Replace public ControlKind = string usage at creation sites with a union or single-case wrapper plus known values.

Option A: discriminated union:

type ControlKind =
    | TextBlock
    | Label
    | Button
    | TextBox
    | DataGrid
    | Chart of ChartKind
    | Custom of string

Option B: opaque wrapper:

type ControlKind = private ControlKind of string

module ControlKind =
    val button: ControlKind
    val textBox: ControlKind
    val custom: string -> ControlKind
    val value: ControlKind -> string

Option B is less disruptive because it preserves custom extensibility and string output for catalogs.

5.2 Add Typed Event Kinds

Introduce:

type ControlEventKind =
    | Click
    | Changed
    | Selected
    | TextCommitted
    | CustomEvent of string

Then expose:

module Attr =
    val onEvent: ControlEventKind -> 'msg -> Attr<'msg>
    val onEventWith: ControlEventKind -> (ControlEvent -> 'msg) -> Attr<'msg>

Keep Attr.on and Attr.onWith as compatibility APIs, but have typed modules use ControlEventKind.

5.3 Replace Common UntypedValue Paths

The highest-value typed attributes are chart and DataGrid data:

type AttrValue<'msg> =
    | TextValue of string
    | BoolValue of bool
    | FloatValue of float
    | StringListValue of string list
    | ChartSeriesValue of ChartSeries list
    | ChartPointValue of ChartPoint list
    | DataGridColumnsValue of DataGridColumn list
    | DataGridRowsValue of DataGridRow list
    | VisibleRangeValue of VisibleRange
    | MessageValue of 'msg
    | EventValue of (ControlEvent -> 'msg)
    | CustomValue of obj

Then keep CustomValue only for genuine extension cases.

5.4 Add A Control Schema Registry

Today required attributes are in ControlInternals.required as string matches. Move this into a schema table:

type ControlAttributeRequirement =
    { Name: AttributeName
      Category: AttrCategory
      Required: bool }

type ControlSchema =
    { Kind: ControlKind
      RequiredAttributes: ControlAttributeRequirement list
      SupportedEvents: ControlEventKind list
      AccessibilityRole: AccessibilityRole }

Agent benefit:

5.5 Keep The Lowered Form

The renderer can still consume:

Control<'msg>
Attr<'msg>
AttrValue<'msg>

The goal is not to make the renderer generic over many typed control records. The goal is to give agents typed constructors that lower into the existing representation.

Recommendation 6: Use Native FAKE Targets

The chosen build-graph direction is native FAKE target registration with pure planning functions retained for testability. The repository should stop treating the current custom traversal as a long-term target interpreter and move target ownership back to FAKE.

Register targets through FAKE:

Target.create "Dev" (fun _ -> runEffects (planTarget "Dev"))
Target.create "Verify" (fun _ -> runEffects (planTarget "Verify"))

open Fake.Core.TargetOperators

"Restore" ==> "Build" ==> "Test" ==> "Dev"
"EvidenceGraph" ==> "EvidenceAudit"

Keep pure functions:

val planTarget: BuildModel -> TargetId -> BuildEffect list
val targetMetadata: TargetId -> TargetMetadata

Add explicit machine-readable metadata alongside native registration:

Expected benefits:

Migration constraints:

Recommendation 7: Add Gate Cost And Authority Metadata

Agents need to reason about cost and authority. Add metadata such as:

targets:
  Dev:
    tier: inner-loop
    cost: low
    authority: non-authoritative-final
    expected_duration: short
    failure_owner_default: product

  GeneratedProductCheck:
    tier: focused-authority
    cost: high
    authority: generated-consumer-authoritative
    expected_duration: long
    failure_owner_default: template

  Verify:
    tier: maintainer-verify
    cost: very-high
    authority: broad-authoritative
    expected_duration: long
    failure_owner_default: product-or-environment

This is more useful to an agent than prose alone.

Recommendation 8: Make Environment Failures First-Class In The Verdict

The current docs already distinguish environment-failure. Extend that into every focused gate and generated product row.

Important fields:

Agent benefit:

Proposed Implementation Sequence

Phase 1: Native FAKE Registration And Metadata

Register existing targets with native FAKE Target.create definitions while keeping pure planning functions for effect tests. Add validation.contract.yml and target metadata. Teach existing GeneratedGuidanceCheck or a new focused gate to validate:

Command names and wrappers remain stable in this phase.

Phase 2: AgentReady Target

Add:

./fake.sh build -t AgentReady

AgentReady should:

  1. read the validation contract
  2. compute changed-path rules from git diff or active feature metadata
  3. run required focused gates
  4. run EvidenceGraph
  5. run EvidenceAudit
  6. write readiness/agent-verdict.json

If changed-path detection is unavailable, it should degrade explicitly and name the broad fallback command.

Phase 3: Typed Controls Front Door

Add typed wrappers without removing compatibility APIs:

Update public module constructors first:

Keep Attr.create for advanced/custom cases, but move generated template code to typed constructors.

Phase 4: Remove Custom Traversal

After native FAKE registration has covered the target graph, remove or reduce the custom targetDependencies and runWithDependencies traversal. Generate docs from target metadata or validate metadata against FAKE registrations so the documented graph and runnable graph cannot drift.

Recommendation Summary

The current architecture is directionally right for a Spec Kit agent. The framework should not discard the harness. Instead, it should make the harness more machine-routable.

Highest-value changes:

  1. Add a validation manifest that maps paths and capabilities to required gates.
  2. Add AgentReady as a manifest-driven middle tier between focused gates and full Verify.
  3. Emit one compact agent-verdict.json for routing, authority, and next action.
  4. Move generated evidence policy out of the product entrypoint where possible.
  5. Add typed Controls front doors while keeping the current lowered representation.
  6. Move the build graph to native FAKE targets while preserving pure planning functions for tests.

These changes preserve the core advantage of FS.Skia.UI for agent consumers: the framework can prove its own generated products. The improvement is to make that proof cheaper, more deterministic, and less dependent on implicit maintainer knowledge.

Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
namespace Microsoft.FSharp.Control
type 'T option = Option<'T>
type 'T list = List<'T>
type bool = System.Boolean
Multiple items
val float: value: 'T -> float (requires member op_Explicit)

--------------------
type float = System.Double

--------------------
type float<'Measure> = float
type obj = System.Object

Type something to start searching.