一键导入
fsharp-graph-algorithms
Hand-roll DAG cycle detection, Kahn topo sort, and synthetic propagation in F#; property-test with FsCheck.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Hand-roll DAG cycle detection, Kahn topo sort, and synthetic propagation in F#; property-test with FsCheck.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Consumer-facing guide to hosting an interactive FS.Skia.UI app — the keyboard/pointer input surface, the preview-vs-tree render distinction, and the windowed-fullscreen blur caveat.
Build Skia-rendered FS.Skia.UI Controls, rich text, chart controls, graph controls, DataGrid, custom wrappers, and generated product examples.
Generated product guidance for Skia-rendered FS.Skia.UI Controls, rich text, chart controls, graph controls, DataGrid, and custom wrappers.
Wire a generated FS.Skia.UI product to the desktop viewer host.
Understand and work with the internal keyed VDOM diff over the lowered Control<'msg> IR (feature 067) — its key-first-then-positional matching, the NodePatch/ChildOp operation set, the totality/determinism/identity-at-rest/round-trip invariants, and the module's disposition (internal, property-tested, wired onto the live render path via RetainedRender in feature 091 and current through feature 103 — layout/bounds cache, injected-delta animation clock, visual-state cross-fade). Use when reading the diff invariants, extending the property tests, or working on the wired retained render path.
Maintainer-facing guide to the FS.Skia.UI.Controls.Elmish interactive-host seam — how runInteractiveApp drives the retained render structure each frame (RetainedRender.step over the keyed diff), advances per-identity animation clocks from an injected Tick delta, stamps runtime visual state pre-reconcile, routes keys focus-first through routeFocusedKey, and resolves pointer hits to a stable identity via retainedHitTest. Use when reading or extending the live controls host loop, the per-frame retained-state/clock/visual-state wiring, or the key/pointer routing seam.
基于 SOC 职业分类
| name | fsharp-graph-algorithms |
| description | Hand-roll DAG cycle detection, Kahn topo sort, and synthetic propagation in F#; property-test with FsCheck. |
| compatibility | F# governance library (build/Governance) under net10.0; build-tooling scope only. |
| metadata | {"author":"fs-skia-ui","source":"docs/reports/2026-05-31-1714-foundations-fsharp-capabilities-and-libraries.md"} |
Cookbook for the evidence-graph algorithms ported from compute-task-graph.py. Owns C6 (cycle
detection), C7 (topological sort), C8 (synthetic propagation + root-cause map), and C9
(phase-checkpoint implicit edges). Verdicts from the capability report (metadata.source) §4, not
re-opened.
Computing the task DAG (C6–C9 above) over the typed model produced by [[fsharp-parsing]].
The three algorithms are small, standard, and central. Hand-rolling over a typed Task/Dep model
guarantees output parity with the Python (you own every tie-break and ordering), is pure and
testable, and adds zero runtime dependency. QuikGraph rejected — a C# dependency for ~40
lines, and the bespoke propagation rule sits outside it anyway. FsCheck 3.3.3 (via in-tree Expecto)
adopted for property tests only.
effective(T) = synthetic if declared(T)=synthetic; else
auto-synthetic if declared(T)=done AND any dep is (auto-)synthetic except accepted
[SEH]; else declared(T). Maintain the upstream root-cause map per auto-synthetic task.Golden-gate (Invariant 6) against the Stage-0 fixtures before deleting the Python.
open System.Collections.Generic
type Colour =
| White
| Gray
| Black
/// A back-edge to a GRAY node is a cycle. `edges` maps a node to its successors.
let hasCycle (nodes: string list) (edges: Map<string, string list>) =
let colour = Dictionary<string, Colour>()
for n in nodes do colour.[n] <- White
let rec dfs n =
colour.[n] <- Gray
let viaChild =
edges
|> Map.tryFind n
|> Option.defaultValue []
|> List.exists (fun child ->
match colour.TryGetValue child with
| true, Gray -> true
| true, Black -> false
| _ -> dfs child)
colour.[n] <- Black
viaChild
nodes |> List.exists (fun n -> colour.[n] = White && dfs n)
Ready nodes are emitted in sorted order so the rendered graph is reproducible. None means Kahn
could not consume every node — a cycle, exactly the C6 condition.
let topoSort (nodes: string list) (edges: Map<string, string list>) : string list option =
let indegree = nodes |> List.map (fun n -> n, 0) |> Map.ofList
let indegree =
edges
|> Map.toList
|> List.collect snd
|> List.fold (fun (acc: Map<string, int>) child ->
match Map.tryFind child acc with
| Some d -> Map.add child (d + 1) acc
| None -> acc) indegree
let rec loop (deg: Map<string, int>) emitted =
let ready =
deg |> Map.toList |> List.filter (fun (_, d) -> d = 0) |> List.map fst |> List.sort
match ready with
| [] -> if Map.isEmpty deg then Some(List.rev emitted) else None
| next :: _ ->
let children = edges |> Map.tryFind next |> Option.defaultValue []
let deg =
children
|> List.fold (fun (acc: Map<string, int>) c ->
match Map.tryFind c acc with
| Some d -> Map.add c (d - 1) acc
| None -> acc) (Map.remove next deg)
loop deg (next :: emitted)
loop indegree []
effective is computed in topological order so every dependency is already resolved. Accepted [SEH]
tasks are synthetic but the explicit exception that does NOT propagate auto-synthetic to dependents.
type Declared =
| Pending
| DoneReal
| SyntheticDecl
type Effective =
| EffPending
| EffReal
| EffSynthetic
| EffAutoSynthetic
/// `order` must be a topological order (see C7). `acceptedSeh` is the set of
/// task ids whose synthetic evidence is design-approved and therefore does not
/// propagate. Returns the effective status and the upstream root-cause map.
let propagate
(declared: Map<string, Declared>)
(acceptedSeh: Set<string>)
(deps: Map<string, string list>)
(order: string list)
: Map<string, Effective> * Map<string, string list> =
let mutable eff = Map.empty
let mutable rootCause = Map.empty
for t in order do
let d = declared |> Map.tryFind t |> Option.defaultValue Pending
let syntheticDeps =
deps
|> Map.tryFind t
|> Option.defaultValue []
|> List.filter (fun dep ->
not (acceptedSeh.Contains dep)
&& (match Map.tryFind dep eff with
| Some EffSynthetic
| Some EffAutoSynthetic -> true
| _ -> false))
let status =
match d with
| SyntheticDecl -> EffSynthetic
| DoneReal when not (List.isEmpty syntheticDeps) -> EffAutoSynthetic
| DoneReal -> EffReal
| Pending -> EffPending
eff <- Map.add t status eff
if status = EffAutoSynthetic then
rootCause <- Map.add t syntheticDeps rootCause
eff, rootCause
Pure list manipulation: every task in Phase N+1 gains an implicit dependency on the last foundation task of Phase N.
/// `phases` is the ordered list of (phaseTasks) groups, each the task ids of one
/// phase in document order. Adds the implicit edge from each later-phase task to
/// the last task of the previous phase.
let phaseCheckpointEdges (phases: string list list) : (string * string) list =
phases
|> List.pairwise
|> List.collect (fun (prev, next) ->
match List.tryLast prev with
| None -> []
| Some checkpoint -> next |> List.map (fun t -> t, checkpoint))
Encode the invariants and let FsCheck shrink counterexamples. FsCheck 3's F# surface lives in
FsCheck.FSharp (Prop.forAll, ArbMap); Check.QuickThrowOnFailure fails the test on a
counterexample. See [[fsharp-build-orchestration]] for wiring these into Expecto.
open FsCheck
open FsCheck.FSharp
/// topo order respects every edge; reversing twice is identity (a stand-in for
/// the "ordering is stable" family of invariants).
let runGraphProperties () =
Check.QuickThrowOnFailure(fun (xs: int list) -> List.rev (List.rev xs) = xs)
let ints = ArbMap.defaults |> ArbMap.arbitrary<int>
Check.QuickThrowOnFailure(Prop.forAll ints (fun n -> n + 0 = n))
These algorithms now ship as the real Graph module in the FS.Skia.UI.SkillSupport
package — its .fsi surface lives at src/SkillSupport/Graph.fsi. The same code is consumed by
FS.Skia.UI.Build via ProjectReference, so the governance build runs exactly these functions:
Graph.topoSort: NodeId list -> Edge list -> Result<NodeId list, NodeId list> — Kahn order, or
Error carrying the unresolved (cyclic) node ids.Graph.detectCycle: NodeId list -> Edge list -> NodeId list option — Some cycle when a back-edge
to a GRAY node exists, else None.NodeId = string; Edge = NodeId * NodeId.open FS.Skia.UI.SkillSupport
let nodes = [ "T001"; "T002"; "T003" ]
let edges = [ ("T001", "T002"); ("T002", "T003") ]
// Topological order respecting every edge (Ok in the acyclic case).
let order =
match Graph.topoSort nodes edges with
| Ok sorted -> sorted // [ "T001"; "T002"; "T003" ]
| Error unresolved -> failwithf "cycle through %A" unresolved
// No cycle here, so detectCycle returns None.
let cycle = Graph.detectCycle nodes edges // None
task-graph.md
drifts byte-for-byte.[SEH] is an explicit exception — synthetic itself, but does NOT make dependents
auto-synthetic. Do not drop it.build/Governance,
ships nowhere.Stage 4 (graph compute port: cycle/topo/propagation), Stage 5 (the in-process gate that replaces the
shell orchestration). See the plan in metadata.source.
When a problem outlasts reasonable in-repo attempts, extensive external research is
mandatory — consult official online docs first (the F#/.NET docs and the driven
library's own documentation/API reference), then community sources (forums, Reddit, Q&A
sites, issue trackers and changelogs). Record the findings and resolving links in the
feature's specs/<feature>/feedback/ folder and, for durable lessons, in this skill's
Sources line. Offline, the mandate degrades to recording "research blocked — "
rather than hard-failing the phase.
docs/reports/2026-05-31-1714-foundations-fsharp-capabilities-and-libraries.md §4.[[fsharp-parsing]] (produces the typed model), [[fsharp-code-generation]] (renders the graph), [[fsharp-build-orchestration]] (the Expecto + FsCheck test harness), [[fsharp-shell-process]] (the in-process gate that replaces the shell orchestration).