Logo FS-Skia-UI

Governance deep dive

FS.Skia.UI ships with a governance system whose job is to turn an open-ended change into a deterministic, provable one: given what you edited, it decides which checks you must run and what evidence proves the change is safe to merge. The distinguishing property of this system is that the rules are not prose to be remembered and not loose YAML to be hand-edited — they are compiled F# under build/Governance/**, the FS.Skia.UI.Build library. A mistyped gate or tier in the source of truth is a compile error, not a silent runtime mismatch. This page explains what the system is, why it is built that way, and how to use it day to day; the linked deep-dive pages cover each subsystem in detail. For the higher-level shape of the build front-end, see the governance architecture overview.

The philosophy: rules live in compiled F#, enforced by gates

The system rests on one idea: mechanical policy belongs in code, and compliance is proven by running gates rather than by trusting a description. Three design records anchor this.

The practical consequence is that the things that are easy to get wrong by hand — gate names, tier ordering, the path globs that select rules — are typed. The routing rules in Routing.fs hold RequiredGates: Targets.Target list, where Targets.Target is a closed union (Targets.fsi). There is no way to require a gate that does not exist.

Why it exists

A change to this repository can touch very different kinds of contract: framework internals under src/**, a public package surface (*.fsi), the generated template that consumers receive from dotnet new fs-skia-ui, Spec Kit evidence artifacts, or the governance rules themselves. Each kind of change needs a different amount of proof. Requiring the full validation pipeline on every edit would make routine work so slow that people would skip validation; requiring nothing would let contract-breaking changes through. The governance system exists to make that trade-off automatically and deterministically: a routine framework-internal edit routes to a light inner-loop check, while a consumer-contract change escalates to the broader proof path — and the decision is computed from the actual diff, not chosen by judgement.

Practitioner usage: run Route first

Before validating any change, run the route selector. It reads the working-tree diff and prints the authoritative tier and the minimal list of gates for this change. Run only the gates it prints.

./fake.sh build -t Route

The output is plain text, for example:

developer-class=framework-author
tier=inner-loop
gates=Dev
dogfood-forced=false
matched-rules=(none)

Read it as follows:

To additionally fail when an escalated change is missing a required evidence artifact, add --enforce:

./fake.sh build -t Route --enforce

This names the missing artifact and the tier that requires it, so you know exactly what evidence still has to be produced before the change is mergeable.

One caveat worth knowing. Route reasons over the whole working tree — the union of the branch-vs-main merge-base diff and any uncommitted or untracked changes. That is the correct model for "is this branch safe to merge?", but in a dirty workspace it can escalate because of unrelated in-progress work, not the file you just touched. If a route looks heavier than your edit warrants, check matched-rules against what you actually changed.

A note on safe concurrency: FAKE-backed commands (./fake.sh, fake.cmd, dotnet fake) share repository .fake state and must not run concurrently. Run the gates Route prints sequentially in the order shown.

Map of the deep-dive pages

The governance system has four subsystems, each with its own page:

For the typed API the governance modules expose, see the API reference.

Type something to start searching.