fsharp-signature-files
Skill23 starsChanged 32 days agoMITView on GitHub
Add F# signature files (.fsi) that expose only the API that callers use, and hide type representations so callers must use constructor functions. Covers generating a starting point with --allsigs or --sig, removing helpers from the .fsi, abstract types, attribute and interface mismatches (FS3888, FS0050), signature errors (FS0034, FS0193, FS0238), XML docs, InternalsVisibleTo for tests, and a public-surface check of the built assembly. Use when the user asks for signature files, a smaller public API, encapsulation of an F# library, or hidden constructors.
In this repository16 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- fsharp-async-compositionSkill
- fsharp-binary-compatibilitySkill
- fsharp-env-capabilitiesSkill
- fsharp-failure-pathsSkill
- fsharp-idiom-reviewSkill
- fsharp-iwsam-errorsSkill
- fsharp-library-api-designSkill
- fsharp-native-aotSkill
- fsharp-nullness-interopSkill
- fsharp-performance-typesSkill
- fsharp-serializationSkill
- fsharp-verify-apiSkill
- optimize-fsharp-typecheck-graphSkill
- parse-dont-validateSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 4 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 51
Decide what is truly public: Search the other projects, the tests, and the documented entry points for each member in the .fsi.
- Distinctive: line 57
Do not make it public for the tests.
- Distinctive: line 74
Fix the traps: Read references/TRAPS.md for the diagnostics.
- Very specific: line 83
Put XML doc comments in the .fsi.
- Very specific: line 85
Write each val with the same generic arity and inline flag as the .fs.
- Very specific: line 89
Run public-surface.ps1 again and compare with before.txt.
- Distinctive: line 91
Write a small probe file in a consumer that builds a hidden record or union directly.
- Distinctive: line 96
Run the bundled proof from the skill root:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
F# signature files
A .fsi file is the contract of its .fs file. A value or type that is not in the
.fsi is hidden from every later file and every other assembly. A type that the .fsi
declares with no body (type Email) hides its fields and cases.
The compiler can write a .fsi for you, but that output repeats every public member.
It hides nothing. The work is to remove what callers do not use.
Build time can also improve. Later files type-check against the .fsi, not the
.fs, so graph-based type checking can check them in parallel with the implementation.
Measure this before you claim a speed-up. See
optimize-fsharp-typecheck-graph.
Steps
Record the public surface before the change. Build the library and run:
pwsh ./scripts/public-surface.ps1 -Assembly <bin>/<Lib>.dll -OutFile before.txtGenerate a starting point. Build once with
--allsigs. It writes one.fsinext to each.fs, with doc comments and attributes:dotnet build <Lib>.fsproj -p:OtherFlags=--allsigsBuild the library project file, not the solution. The global property also reaches each project that the library references, and it replaces an
OtherFlagsvalue that the project sets. When the project setsOtherFlags, add--allsigsto that value for one build instead.--sig:<file>writes all signatures into one file, which you must then split by hand. Remove the flag after this build. Delete each.fsithat the build wrote outside the library, and the.fsifiles underobj/for generated files such asAssemblyAttributes.fs.Add each
.fsito the project directly before its.fs:<Compile Include="Email.fsi" /> <Compile Include="Email.fs" />A
.fsiafter its.fsgives FS0238 and FS0248.- Decide what is truly public. Search the other projects, the tests, and the
documented entry points for each member in the
.fsi. Delete eachval, type, and member that no other project uses. Do not mark itprivatein the.fsinstead: the omission from the.fsiis the hiding.- A member that only tests use: write
val internal f: ...in the.fsiand add<InternalsVisibleTo Include="<Tests assembly name>" />to the library project. Do not make it public for the tests. - The generated
.fsialso listsval privatevalues. Delete them.
- A member that only tests use: write
Hide representations where callers must not build values. Replace the record or union body in the
.fsiwith the bare type name and keep the constructor function:/// An email address that contains one '@'. type Email module Email = val create: input: string -> Result<Email, string> val value: email: Email -> stringCallers then get FS0039 for
{ Value = ... }or a union case. Keep a record public when every combination of its field values is valid. For the choice of type and constructor, use parse-dont-validate.- Fix the traps. Read references/TRAPS.md for the
diagnostics. The most important rules:
- Copy each attribute on a type or value to the
.fsi:[<RequireQualifiedAccess>],[<Struct>],[<Literal>]. Do not add an attribute that the.fsdoes not have:[<Sealed>]on a class in the.fsionly gives FS0297. A missingRequireQualifiedAccessgives only warning FS3888. The build passes, and callers can use the cases without the type name. - List each implemented interface:
interface System.IDisposable. Without it, you get warning FS0050, andusein a caller fails with FS0193. - Put XML doc comments in the
.fsi. When a.fsiexists, a doc comment that is only in the.fsis not in the XML documentation file. - Write each
valwith the same generic arity andinlineflag as the.fs.
- Copy each attribute on a type or value to the
- Check the result.
- Build every project and run the tests. Treat FS3888 and FS0050 warnings as errors to fix.
- Run
public-surface.ps1again and compare withbefore.txt. Each removed line must be a member that no caller uses. No line may be added. - Write a small probe file in a consumer that builds a hidden record or union directly. It must fail to compile. Then delete it.
Verify
Run the bundled proof from the skill root:
pwsh ./scripts/run.ps1The runner builds scripts/Sample and checks its public surface, its XML
documentation, and an InternalsVisibleTo test project. It builds
scripts/Generate with --allsigs and checks the generated .fsi.
It then builds each folder in scripts/breaks and checks the diagnostic
codes in its // expect: lines.
These rules come from the F# documentation and are not bundled proofs: a val inline
in a .fsi makes the function body part of the contract, so callers must build again
when the body changes. Use inline in a .fsi only when the function needs it, for
example for statically resolved type parameters.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

