agentleFS
TheAngryByrdTheAngryByrd/fsharp-skills/skills/fsharp-signature-files/SKILL.md

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.

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
  1. Very specific:

    Decide what is truly public: Search the other projects, the tests, and the documented entry points for each member in the .fsi.

    line 51
  2. Distinctive:

    Do not make it public for the tests.

    line 57
  3. Distinctive:

    Fix the traps: Read references/TRAPS.md for the diagnostics.

    line 74
  4. Very specific:

    Put XML doc comments in the .fsi.

    line 83
  5. Very specific:

    Write each val with the same generic arity and inline flag as the .fs.

    line 85
  6. Very specific:

    Run public-surface.ps1 again and compare with before.txt.

    line 89
  7. Distinctive:

    Write a small probe file in a consumer that builds a hidden record or union directly.

    line 91
  8. Distinctive:

    Run the bundled proof from the skill root:

    line 96

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

  1. Record the public surface before the change. Build the library and run:

    pwsh ./scripts/public-surface.ps1 -Assembly <bin>/<Lib>.dll -OutFile before.txt
  2. Generate a starting point. Build once with --allsigs. It writes one .fsi next to each .fs, with doc comments and attributes:

    dotnet build <Lib>.fsproj -p:OtherFlags=--allsigs

    Build the library project file, not the solution. The global property also reaches each project that the library references, and it replaces an OtherFlags value that the project sets. When the project sets OtherFlags, add --allsigs to 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 .fsi that the build wrote outside the library, and the .fsi files under obj/ for generated files such as AssemblyAttributes.fs.

  3. Add each .fsi to the project directly before its .fs:

    <Compile Include="Email.fsi" />
    <Compile Include="Email.fs" />

    A .fsi after its .fs gives FS0238 and FS0248.

  4. Decide what is truly public. Search the other projects, the tests, and the documented entry points for each member in the .fsi. Delete each val, type, and member that no other project uses. Do not mark it private in the .fs instead: the omission from the .fsi is the hiding.
    • A member that only tests use: write val internal f: ... in the .fsi and add <InternalsVisibleTo Include="<Tests assembly name>" /> to the library project. Do not make it public for the tests.
    • The generated .fsi also lists val private values. Delete them.
  5. Hide representations where callers must not build values. Replace the record or union body in the .fsi with 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 -> string

    Callers 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.

  6. 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 .fs does not have: [<Sealed>] on a class in the .fsi only gives FS0297. A missing RequireQualifiedAccess gives 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, and use in a caller fails with FS0193.
    • Put XML doc comments in the .fsi. When a .fsi exists, a doc comment that is only in the .fs is not in the XML documentation file.
    • Write each val with the same generic arity and inline flag as the .fs.
  7. Check the result.
    • Build every project and run the tests. Treat FS3888 and FS0050 warnings as errors to fix.
    • Run public-surface.ps1 again and compare with before.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.ps1

The 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.