vigiles 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.claude/settings.json +46 -0
  2. package/.claude/settings.local.json +8 -0
  3. package/.claude-plugin/hooks/post-edit.sh +34 -0
  4. package/.claude-plugin/hooks/pre-edit.sh +40 -0
  5. package/.claude-plugin/hooks/session-start.sh +38 -0
  6. package/.claude-plugin/marketplace.json +14 -0
  7. package/.claude-plugin/plugin.json +47 -0
  8. package/.github/workflows/ci.yml +81 -0
  9. package/.prettierignore +1 -0
  10. package/.vigiles/generated.d.ts +205 -0
  11. package/CLAUDE.md +95 -0
  12. package/CLAUDE.md.spec.ts +142 -0
  13. package/CONTRIBUTING.md +121 -0
  14. package/LICENSE +21 -0
  15. package/README.md +377 -0
  16. package/action.yml +25 -0
  17. package/dist/action.d.ts +7 -0
  18. package/dist/action.d.ts.map +1 -0
  19. package/dist/action.js +180 -0
  20. package/dist/action.js.map +1 -0
  21. package/dist/cli.d.ts +12 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +1267 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/cli.test.d.ts +2 -0
  26. package/dist/cli.test.d.ts.map +1 -0
  27. package/dist/cli.test.js +650 -0
  28. package/dist/cli.test.js.map +1 -0
  29. package/dist/compile.d.ts +101 -0
  30. package/dist/compile.d.ts.map +1 -0
  31. package/dist/compile.js +503 -0
  32. package/dist/compile.js.map +1 -0
  33. package/dist/evolve.d.ts +132 -0
  34. package/dist/evolve.d.ts.map +1 -0
  35. package/dist/evolve.js +599 -0
  36. package/dist/evolve.js.map +1 -0
  37. package/dist/freshness.d.ts +67 -0
  38. package/dist/freshness.d.ts.map +1 -0
  39. package/dist/freshness.js +244 -0
  40. package/dist/freshness.js.map +1 -0
  41. package/dist/freshness.test.d.ts +2 -0
  42. package/dist/freshness.test.d.ts.map +1 -0
  43. package/dist/freshness.test.js +356 -0
  44. package/dist/freshness.test.js.map +1 -0
  45. package/dist/generate-types.d.ts +34 -0
  46. package/dist/generate-types.d.ts.map +1 -0
  47. package/dist/generate-types.js +381 -0
  48. package/dist/generate-types.js.map +1 -0
  49. package/dist/inline.d.ts +58 -0
  50. package/dist/inline.d.ts.map +1 -0
  51. package/dist/inline.js +142 -0
  52. package/dist/inline.js.map +1 -0
  53. package/dist/inline.test.d.ts +5 -0
  54. package/dist/inline.test.d.ts.map +1 -0
  55. package/dist/inline.test.js +152 -0
  56. package/dist/inline.test.js.map +1 -0
  57. package/dist/linters.d.ts +38 -0
  58. package/dist/linters.d.ts.map +1 -0
  59. package/dist/linters.js +588 -0
  60. package/dist/linters.js.map +1 -0
  61. package/dist/proofs.d.ts +272 -0
  62. package/dist/proofs.d.ts.map +1 -0
  63. package/dist/proofs.js +622 -0
  64. package/dist/proofs.js.map +1 -0
  65. package/dist/proofs.test.d.ts +9 -0
  66. package/dist/proofs.test.d.ts.map +1 -0
  67. package/dist/proofs.test.js +952 -0
  68. package/dist/proofs.test.js.map +1 -0
  69. package/dist/spec.d.ts +258 -0
  70. package/dist/spec.d.ts.map +1 -0
  71. package/dist/spec.js +113 -0
  72. package/dist/spec.js.map +1 -0
  73. package/dist/spec.test.d.ts +2 -0
  74. package/dist/spec.test.d.ts.map +1 -0
  75. package/dist/spec.test.js +1222 -0
  76. package/dist/spec.test.js.map +1 -0
  77. package/dist/types.d.ts +101 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +3 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/validate.d.ts +10 -0
  82. package/dist/validate.d.ts.map +1 -0
  83. package/dist/validate.js +286 -0
  84. package/dist/validate.js.map +1 -0
  85. package/dist/validate.test.d.ts +2 -0
  86. package/dist/validate.test.d.ts.map +1 -0
  87. package/dist/validate.test.js +531 -0
  88. package/dist/validate.test.js.map +1 -0
  89. package/docs/agent-setup.md +85 -0
  90. package/docs/agent-workflows.md +103 -0
  91. package/docs/comparison.md +71 -0
  92. package/docs/freshness.md +124 -0
  93. package/docs/inline-mode.md +119 -0
  94. package/docs/linter-support.md +166 -0
  95. package/docs/spec-format.md +194 -0
  96. package/eslint.config.mjs +79 -0
  97. package/examples/CLAUDE.md +54 -0
  98. package/examples/CLAUDE.md.spec.ts +65 -0
  99. package/examples/SKILL.md +50 -0
  100. package/examples/SKILL.md.spec.ts +57 -0
  101. package/fixtures/example-project/CLAUDE.md +11 -0
  102. package/fixtures/example-project/package.json +9 -0
  103. package/fixtures/example-project/src/index.ts +3 -0
  104. package/fixtures/example-project/src/utils.test.ts +2 -0
  105. package/fixtures/example-project/src/utils.ts +3 -0
  106. package/logo.png +0 -0
  107. package/package.json +42 -0
  108. package/research/adoption-strategy.md +111 -0
  109. package/research/agent-integration.md +145 -0
  110. package/research/ai-code-quality.md +197 -0
  111. package/research/code-search-for-agents.md +313 -0
  112. package/research/competitive-landscape.md +163 -0
  113. package/research/doc-freshness.md +516 -0
  114. package/research/executable-specs.md +368 -0
  115. package/research/feature-ideas.md +464 -0
  116. package/research/formal-proofs-for-agents.md +338 -0
  117. package/research/fp-for-agent-harness.md +150 -0
  118. package/research/fp-for-deterministic-ai.md +131 -0
  119. package/research/self-evolving-specs.md +298 -0
  120. package/schemas/claude-md-strict.yml +18 -0
  121. package/schemas/claude-md.yml +6 -0
  122. package/schemas/skill-strict.yml +12 -0
  123. package/schemas/skill.yml +5 -0
  124. package/skills/audit-feedback-loop/SKILL.md +76 -0
  125. package/skills/edit-spec/SKILL.md +131 -0
  126. package/skills/enforce-rules-format/SKILL.md +71 -0
  127. package/skills/generate-logo/SKILL.md +102 -0
  128. package/skills/generate-rule/SKILL.md +90 -0
  129. package/skills/linter-docs/clippy.md +241 -0
  130. package/skills/linter-docs/eslint.md +384 -0
  131. package/skills/linter-docs/pylint.md +288 -0
  132. package/skills/linter-docs/rubocop.md +277 -0
  133. package/skills/linter-docs/ruff.md +187 -0
  134. package/skills/linter-docs/stylelint.md +247 -0
  135. package/skills/migrate-to-spec/SKILL.md +124 -0
  136. package/skills/pr-to-lint-rule/SKILL.md +97 -0
  137. package/skills/strengthen/SKILL.md +168 -0
  138. package/src/action.ts +214 -0
  139. package/src/cli.test.ts +914 -0
  140. package/src/cli.ts +1631 -0
  141. package/src/compile.ts +691 -0
  142. package/src/evolve.ts +781 -0
  143. package/src/freshness.test.ts +449 -0
  144. package/src/freshness.ts +299 -0
  145. package/src/generate-types.ts +448 -0
  146. package/src/inline.test.ts +206 -0
  147. package/src/inline.ts +164 -0
  148. package/src/linters.ts +739 -0
  149. package/src/proofs.test.ts +1314 -0
  150. package/src/proofs.ts +849 -0
  151. package/src/spec.test.ts +1471 -0
  152. package/src/spec.ts +427 -0
  153. package/src/types.ts +117 -0
  154. package/src/validate.test.ts +701 -0
  155. package/src/validate.ts +381 -0
  156. package/tsconfig.json +23 -0
@@ -0,0 +1,338 @@
1
+ # Formal Proofs for AI Agents
2
+
3
+ Scope: should vigiles reach past its current deterministic proofs (monotonicity,
4
+ NCD, Merkle, fixed-point, property tests in `src/proofs.ts`) into real formal
5
+ verification, and if so, how?
6
+
7
+ Bottom line up front: yes, but narrowly. Dafny is the only system where
8
+ "LLM writes code + spec + proof" is remotely credible today, and even then only
9
+ for small functions. Ship a `dafny()` enforce target first. Treat full
10
+ evolution-engine proof gating as a research preview, not a product.
11
+
12
+ ---
13
+
14
+ ## 1. Landscape
15
+
16
+ **Lean 4.** Dependent-type proof assistant, successor to Lean 3. Primary use is
17
+ formalized mathematics (mathlib has ~1.8M lines of formalized math) and
18
+ verification research. Production software use is negligible. Most
19
+ LLM-theorem-proving papers target Lean because miniF2F and putnamBench are Lean
20
+ benchmarks and mathlib is huge, clean training data. LLM-friendliness: highest
21
+ of any system by volume of tooling, but proofs are long, tactic-heavy, and
22
+ brittle.
23
+
24
+ **Coq / Rocq** (renamed 2024). The grand-daddy: CompCert (verified C compiler,
25
+ AbsInt ships it to Airbus), seL4 microkernel proofs, Iris separation logic.
26
+ Real production use, real Fortune-500 deployment via CompCert. LLM tooling
27
+ (Proverbot9001, CoqGym, Copra) is older and slower-moving than Lean's; the
28
+ pivot to the Rocq name also fractured the ecosystem briefly.
29
+
30
+ **Dafny.** Imperative/OO language with pre/postconditions and a built-in SMT
31
+ backend (Z3). Written by Rustan Leino at MSR, now at AWS. Used in production at
32
+ AWS: the ESDK (Encryption SDK), the Authenticated Encryption library, parts of
33
+ S3's ShardStore, and Dafny-to-Rust compilation for cryptographic components.
34
+ Dafny is the pragmatic choice: proof burden is 3-20 lines per function, not
35
+ hundreds. LLM-friendliness is very good; Microsoft's own "Laurel" and "Clover"
36
+ work specifically targets Dafny proof synthesis with LLMs.
37
+
38
+ **F\*.** Dependent types + SMT, developed at MSR and Inria. Powers Project
39
+ Everest: HACL\*, EverCrypt, Vale — these ship inside Firefox's NSS, the Linux
40
+ kernel crypto path, WireGuard, mbedTLS, and ZincCrypto. Real production use,
41
+ but learning curve is brutal. LLM tooling is thin; almost no papers target F\*
42
+ specifically.
43
+
44
+ **TLA+.** Leslie Lamport's specification language for concurrent and
45
+ distributed systems. AWS uses it heavily — DynamoDB, S3, EBS designs were
46
+ verified in TLA+ and Amazon has published about it. It does not verify code;
47
+ it verifies designs and finds protocol bugs via model checking (TLC) or proof
48
+ (TLAPS). LLM-friendliness is moderate — there are TLA+ generation papers but
49
+ it's a small community.
50
+
51
+ **Liquid Haskell.** Refinement types bolted onto GHC via SMT. Academic
52
+ darling, small industrial footprint (Galois, Awake Security historically).
53
+ Lightweight annotations, but depends on Haskell. LLM tooling: near zero.
54
+
55
+ **Verus.** Verification for Rust, developed at MSR/CMU. Pre/postconditions,
56
+ SMT-backed, targeting systems code. Still young (first stable release 2023)
57
+ but Microsoft has a concerted push, including a verified storage engine
58
+ (StorageNode in Verus) and verified parts of Hyper-V. LLM-friendliness: a few
59
+ 2024-25 papers (AutoVerus, SAFE from UCSD) exist, results are preliminary.
60
+
61
+ **Kani.** Model checker for Rust from AWS, based on CBMC. Bounded verification,
62
+ not full proof — you prove properties hold for all inputs up to bound N. Very
63
+ practical and low-friction, and AWS uses it on Firecracker, s2n-quic, and
64
+ aws-nitro-enclaves. LLM-friendliness: no dedicated tooling but the assertion
65
+ style is easy to generate.
66
+
67
+ ---
68
+
69
+ ## 2. LLM-Assisted Theorem Proving: 2024-2026 State of the Art
70
+
71
+ The field has moved fast on olympiad-style math and almost not at all on
72
+ software verification. Honest pass rates below.
73
+
74
+ **LeanDojo (2023, updated 2024).** CMU infrastructure paper: extracted mathlib
75
+ as (state, tactic) pairs, released ReProver retrieval-augmented model. Baseline
76
+ of ~51% on miniF2F-test for small open models. Still the standard harness
77
+ everyone builds on.
78
+
79
+ **Lean Copilot (2024).** Song et al., runs local LLMs as Lean tactics
80
+ (`suggest_tactics`, `search_proof`). It's a human-in-the-loop authoring tool,
81
+ not an autonomous prover. Useful, not transformative.
82
+
83
+ **DeepSeek-Prover V1.5 (2024), V2 (2025).** Expert iteration over generated
84
+ proofs. V1.5 reached ~63% miniF2F-test, V2 pushed to ~88% with RL and
85
+ subgoal decomposition. State of the art for open models on olympiad math.
86
+
87
+ **Goedel-Prover (2025, Princeton).** Trained on ~1.6M auto-formalized
88
+ statements. ~64% miniF2F pass@32, ~7% on putnamBench pass@512. putnamBench is
89
+ brutally hard — even the best closed models are in the single digits to low
90
+ teens.
91
+
92
+ **AlphaProof (DeepMind, July 2024).** Silver-medal IMO 2024 performance (4/6
93
+ problems), Lean-based. Not released. Demonstrated that RL + massive compute
94
+ on Lean can reach elite human level on contest math. Zero transfer demonstrated
95
+ to real software.
96
+
97
+ **Baldur (Meta/Google 2023, cited through 2025).** Whole-proof generation for
98
+ Isabelle. ~41% on PISA benchmark with repair loop. Interesting because it
99
+ generates whole proofs rather than tactic-by-tactic — similar ergonomics to
100
+ how you'd want an agent to work.
101
+
102
+ **Copra (2024).** GPT-4-based in-context proof agent for Lean and Coq. Uses
103
+ error messages and retrieval. ~30% on miniF2F with GPT-4. Honest and modest.
104
+
105
+ **The benchmark gap.** miniF2F and putnamBench are olympiad math. For actual
106
+ software verification the closest thing is DafnyBench (Poesia et al., 2024),
107
+ ~750 Dafny problems drawn from textbooks and real code. GPT-4 solves ~68%.
108
+ There is no equivalent large-scale Lean-for-software or Verus benchmark. This
109
+ matters: 88% on miniF2F tells you nothing about whether an LLM can verify your
110
+ sort function.
111
+
112
+ **Summary.** On olympiad math, top systems are at 60-88% and the curve is
113
+ steep. On real software verification, we have one benchmark (DafnyBench), one
114
+ system that clearly works (GPT-4 + Dafny), and lots of arXiv preprints with
115
+ <50% pass rates. Anyone claiming "LLMs write verified software" in 2026 is
116
+ selling a demo.
117
+
118
+ ---
119
+
120
+ ## 3. Dafny vs Lean 4 for Wiring to Claude
121
+
122
+ This is the core decision. Both can be driven by an LLM agent. They are not
123
+ equivalent.
124
+
125
+ | Dimension | Dafny | Lean 4 |
126
+ | ------------------------- | ----------------------------------------- | ------------------------------------ |
127
+ | Target user | Programmers | Mathematicians, PL researchers |
128
+ | Proof style | Declarative pre/post + SMT auto-discharge | Interactive tactic proofs |
129
+ | Typical burden | 3-20 lines per function | 50-500 lines per theorem |
130
+ | What you verify | Imperative/OO code | Arbitrary propositions |
131
+ | Backend | Z3 (automatic) | Kernel (manual) |
132
+ | Tooling maturity for code | Production (AWS) | Research |
133
+ | LLM pass rate in-domain | ~68% (DafnyBench, GPT-4) | ~30-88% (miniF2F, math only) |
134
+ | Production users | AWS (ESDK, S3 ShardStore) | None at code level; mathlib for math |
135
+ | Error messages | SMT counterexamples, often cryptic | Type errors, very precise |
136
+ | Install footprint | ~50 MB, single binary | ~500 MB, elan toolchain |
137
+
138
+ **The honest comparison.** Dafny is for verifying that a function satisfies a
139
+ spec. Lean 4 is for proving theorems. If you want to say "this sort function
140
+ returns a permutation that is sorted," Dafny does it in 5 lines. Lean 4 does it
141
+ in 80 lines plus lemmas. If you want to say "this elliptic curve satisfies the
142
+ Hasse bound," only Lean 4 can do that, and an LLM will not succeed unassisted.
143
+
144
+ **For vigiles' audience** — programmers who write `.spec.ts` to guide coding
145
+ agents — Dafny wins on every axis except theoretical expressiveness. The
146
+ audience overlap between vigiles users and people who can read a Lean proof is
147
+ near zero. The overlap between vigiles users and people who would accept a
148
+ Dafny precondition is much larger, because Dafny looks like code.
149
+
150
+ **LLM tooling.** Dafny's LLM story (Laurel, Clover, DafnyBench, plus Copilot
151
+ working decently on small Dafny files) is behind Lean's in raw research volume
152
+ but ahead in software-verification credibility. Lean's ecosystem is optimized
153
+ for formalizing existing math, not synthesizing new code-level specs.
154
+
155
+ **Recommendation: Dafny first, Lean as an experimental target later.**
156
+
157
+ ---
158
+
159
+ ## 4. Integration Patterns: Wiring a Prover to Claude
160
+
161
+ Four options, brief pros and cons. These are not exclusive.
162
+
163
+ ### 4.1 Subagent calls prover via CLI
164
+
165
+ Claude dispatches a subagent with the prompt "run `dafny verify foo.dfy` and
166
+ fix errors until green." Subagent iterates until pass or budget exhausted.
167
+
168
+ Pros: zero infrastructure, uses existing Task tool, fits Claude Code today.
169
+ Cons: no caching, no shared proof state, subagent re-reads the whole file every
170
+ loop, burns tokens fast on complex proofs.
171
+
172
+ ### 4.2 MCP server for Dafny/Lean
173
+
174
+ Stand up an MCP server exposing `verify`, `suggest_tactic`, `get_goal_state`,
175
+ `check_proof`. Claude calls these as tools.
176
+
177
+ Pros: structured, stateful, interactive proofs become tractable, sharable
178
+ across agents. Cons: building a good MCP server for an interactive prover is
179
+ real work — Lean-Dojo took a team, and Dafny's LSP is less interactive. This
180
+ is the right long-term answer and the expensive one.
181
+
182
+ ### 4.3 PostToolUse hook running prover on changed files
183
+
184
+ Edit a `.dfy` file, hook fires `dafny verify`, output goes back into Claude's
185
+ context on failure.
186
+
187
+ Pros: mechanical, always on, cannot be skipped, fits vigiles' existing
188
+ "enforce this externally" philosophy. Cons: feedback is one-shot per edit, not
189
+ a conversation; large files re-verify from scratch.
190
+
191
+ ### 4.4 `vigiles verify` CLI command on spec-annotated blocks
192
+
193
+ New CLI verb that scans the spec for `verify()` rules, extracts the referenced
194
+ proof obligations, runs the chosen prover, and produces an audit report. Same
195
+ shape as `vigiles check`.
196
+
197
+ Pros: matches vigiles' current audit-at-commit-time model; deterministic; easy
198
+ to integrate with CI; does not require Claude at all. Cons: not interactive —
199
+ if verification fails, the user/agent has to loop manually.
200
+
201
+ **Recommendation: start with 4.3 + 4.4.** Both are mechanical, both fit
202
+ vigiles' existing architecture, neither requires Lean-Dojo-grade engineering.
203
+ 4.2 (MCP server) is the right v2 if users actually engage. 4.1 (subagent) is
204
+ already possible without any vigiles change.
205
+
206
+ ---
207
+
208
+ ## 5. What Vigiles Should Ship First
209
+
210
+ Three proposals, ordered by ambition. Ship the low one, prototype the medium,
211
+ do not build the high one yet.
212
+
213
+ ### 5.1 Low: `dafny()` enforce target
214
+
215
+ ```ts
216
+ enforce("dafny:ESDK/encrypt.dfy#EncryptIsInverseOfDecrypt");
217
+ ```
218
+
219
+ Semantics: at `vigiles check` time, parse the reference, locate the `.dfy`
220
+ file, run `dafny verify` on it, confirm the named lemma/method exists and
221
+ verifies. Fits the existing stale-reference pattern in `src/linters.ts` — if
222
+ the lemma is renamed or deleted, check fails.
223
+
224
+ Effort: 1-2 weeks. Requires Dafny installed; follows the same model as the
225
+ existing pylint/clippy/rubocop checks. This is a natural extension of
226
+ vigiles' linter cross-referencing moat into the proof world.
227
+
228
+ Production-grade: yes, for projects that already use Dafny. Demo-grade for
229
+ anyone else. That's fine — vigiles never forced anyone to adopt pylint either.
230
+
231
+ ### 5.2 Medium: `verify()` rule builder that emits a proof obligation file
232
+
233
+ ```ts
234
+ verify({
235
+ name: "MonotonicityProof",
236
+ target: "src/proofs.ts#monotonicityLattice",
237
+ obligations: [
238
+ "forall s1 s2 :: merge(s1, s2) >= s1",
239
+ "forall s1 s2 :: merge(s1, s2) >= s2",
240
+ "forall s1 s2 :: merge(s1, s2) == merge(s2, s1)",
241
+ ],
242
+ backend: "dafny", // or "lean4"
243
+ });
244
+ ```
245
+
246
+ Semantics: compiling the spec emits a `.dfy` skeleton with the obligations as
247
+ method postconditions. Running `vigiles verify` tries to discharge them — with
248
+ Dafny, often automatically; with Lean 4, the user (or an agent) fills in the
249
+ proof. Either way vigiles records pass/fail alongside its existing proof
250
+ record.
251
+
252
+ Effort: 4-8 weeks. The hard part is the TS-to-Dafny type bridge. Stay very
253
+ narrow — support int, bool, string, sequences, sets, and let users opt in per
254
+ function. Do not try to verify arbitrary TypeScript.
255
+
256
+ Production-grade: demo-grade initially. Becomes production-grade only for the
257
+ subset of code users are willing to hand-translate or re-write in Dafny. Pitch
258
+ it honestly: "for the 5% of your code that must be correct."
259
+
260
+ ### 5.3 High: Proof-gated evolution engine
261
+
262
+ Today `src/evolve.ts` lets an LLM propose mutations to the spec, runs property
263
+ tests, and commits passing mutations to a Merkle history. The ambitious version
264
+ would require every mutation to also ship a proof obligation discharge —
265
+ "the new rule implies the old rule, verified by Dafny" — before the Merkle
266
+ commit is accepted.
267
+
268
+ Effort: 3-6 months minimum, mostly research. The fundamental problem is that
269
+ rule semantics are natural-language prose filtered through `enforce`, `check`,
270
+ and `guidance`. There is no formal meaning of "the new rule implies the old
271
+ rule" until someone defines one. You would need a specification logic for
272
+ instruction files. That is a PhD project, not a sprint.
273
+
274
+ Production-grade: no. Demo-grade at best, for a long time. Do not promise it.
275
+ It is worth keeping as a research direction because it would be a genuine
276
+ first — "the first self-evolving agent spec system with formally verified
277
+ mutations" — but the honest engineering answer is "we are not there yet."
278
+
279
+ ---
280
+
281
+ ## Closing Honesty Check
282
+
283
+ - LLMs solve 60-88% of olympiad-math problems in Lean. This does not transfer
284
+ to arbitrary software verification.
285
+ - The one benchmark we have for code-level proof synthesis (DafnyBench) sits at
286
+ ~68% for the best closed models. That means for every three verified
287
+ functions, one is wrong.
288
+ - No production system today lets an LLM "write code + spec + proof" for
289
+ general-purpose software. AWS's Dafny deployments are human-written with
290
+ machine-checked proofs. That is a different workflow than what an agent
291
+ would do.
292
+ - Vigiles' existing deterministic proofs (monotonicity lattice, NCD, Merkle,
293
+ fixed-point, property tests) are already unusually rigorous for this product
294
+ category. Adding a Dafny enforce target is a cheap, honest extension.
295
+ Anything beyond that is marketing ahead of capability.
296
+
297
+ Recommendation: ship 5.1. Prototype 5.2 behind a flag. Write a blog post about
298
+ 5.3 but do not build it.
299
+
300
+ ---
301
+
302
+ ## Additional Ideas (post-session)
303
+
304
+ ### 5.4. Dafny-in-comments — inline formal specs
305
+
306
+ Mirror the inline enforce pattern but for formal contracts:
307
+ `<!-- vigiles:verify dafny "ensures result > 0" -->` next to a function.
308
+ vigiles extracts the contract, wraps the referenced function in a `.dfy`
309
+ file with the contract as a postcondition, runs `dafny verify`. The
310
+ developer writes zero Dafny — they write a one-line natural-language
311
+ contract in a markdown comment, and the tool does the wiring. If the
312
+ proof fails, the error goes back to the agent as a diagnostic. Same
313
+ adoption shape as inline enforce: one comment, zero new files, zero build
314
+ step. The agent can even generate the contract from the function's
315
+ docstring.
316
+
317
+ ### 5.5. Property-test → Dafny bridge
318
+
319
+ fast-check properties in the spec (from `propertyTest()` in proofs.ts)
320
+ are already "lightweight formal methods" — they test an invariant over
321
+ random inputs. Bridge to Dafny: when a property covers a pure function,
322
+ offer to generate a Dafny contract from the property's invariant. The
323
+ property test IS the spec; Dafny just machine-checks it exhaustively
324
+ instead of sampling. The bridge is mechanical: `property("positive",
325
+ (n) => f(n) > 0)` → `ensures f(n) > 0`. The hard part is translating
326
+ the function body to Dafny, which is where the LLM earns its keep.
327
+
328
+ ### 5.6. Proof receipts in the Merkle chain
329
+
330
+ When a Dafny/Lean verification passes during evolution, include the
331
+ proof receipt in the Merkle history node alongside the existing
332
+ monotonicity/NCD/bloom receipts. The chain becomes a certificate of
333
+ correctness: "this spec change was not just fitness-positive and
334
+ monotonically valid, it was FORMALLY VERIFIED by Z3/Lean kernel."
335
+ Consumers can distinguish "verified by deterministic proofs" from
336
+ "verified by formal prover" at the receipt level. The `verify()`
337
+ method already checks receipt hashes, so formal receipts get
338
+ tamper-evidence for free.
@@ -0,0 +1,150 @@
1
+ # FP for the Agent Harness: Railway, Effects, and Skills
2
+
3
+ ## Framing
4
+
5
+ The previous FP doc (`fp-for-deterministic-ai.md`) was about the code agents **produce** — pure zones, Result types, exhaustive pattern matches in TypeScript. This doc is about the different layer: applying FP structure to the **agent harness itself** — Claude Code's skills, hooks, tool-use loop, session state. The same techniques, aimed inward instead of outward.
6
+
7
+ The starting point is the half-joke from earlier: "what about railway programming structure for skills lol". It is not a joke. The Claude Code harness already has all the ingredients for an effect system and nobody has wired them up: skills are functions, hooks are effect handlers, tool calls are suspendable effects, the session transcript is event-sourced state. Structuring them with Railway / algebraic-effect idioms would make agent behavior **composable, inspectable, and replayable** — all three things that agents today are bad at.
8
+
9
+ Claude Code is not going to adopt Effect.ts as an implementation detail. But vigiles can ship the contracts, conventions, and verification that act **as if** skills and hooks were Railway pipelines — and get most of the benefit at the spec layer without changing the harness.
10
+
11
+ ## What the harness already looks like
12
+
13
+ A Claude Code turn decomposes into something like:
14
+
15
+ ```
16
+ Input (user message + transcript)
17
+
18
+ SessionStart hooks (can inject context)
19
+
20
+ Model turn → tool call
21
+
22
+ PreToolUse hooks (can block / rewrite)
23
+
24
+ Tool execution
25
+
26
+ PostToolUse hooks (can transform result / error)
27
+
28
+ Model continues with result
29
+
30
+ Stop hooks / output
31
+ ```
32
+
33
+ Each arrow is a place a function gets called with a typed input and returns a typed output (possibly augmented with a side channel of diagnostics). That is a Railway. The harness already **is** a pipeline; the opportunity is to make the pipeline shape explicit so users can reason about it, skills can be composed, and failure paths collect instead of drop.
34
+
35
+ ## Ten ideas for FP applied to the harness
36
+
37
+ ### 1. Railway-typed skills — NOT BUILT
38
+
39
+ Model a skill as `Skill<Ctx> = (Ctx) => Effect<Ctx, Diagnostic>` — it takes the current session context, returns either the next context or a list of diagnostics. Two skills compose with `.andThen`:
40
+
41
+ ```ts
42
+ const reviewAndFix = review.andThen(runTests).andThen(fixFailures);
43
+ ```
44
+
45
+ In Scott Wlaschin's formulation this is the two-track railway: the success track carries Ctx forward, the failure track carries Diagnostic short-circuited to the end. vigiles's contribution: a **spec-level declaration** of skill shape (`skill("review", { input: ..., output: ... })`) that compiles to a SKILL.md with the pipeline documented, and an audit check that each declared skill step actually exists. Users write skills any way they like; vigiles enforces the shape at the edges.
46
+
47
+ ### 2. Validation applicative for rule audits — PARTIALLY SHIPPED (audit collects all diagnostics across stages, but no formal Validation type)
48
+
49
+ Today `vigiles audit` collects problems across stages (stale refs, dead enforcements, duplicates, coverage gaps) but each stage is written imperatively and short-circuits on its own. Switch to the Validation pattern: every check returns `Validation<OK, Diagnostic[]>` and the whole audit is `checks.map(run).sequence()`, collecting **all** failures across **all** checks in one pass. Contrast with Either monad: Either short-circuits at the first error. For audits you want the opposite — run everything, collect everything. This is a one-library change (neverthrow's `Result.combineWithAllErrors`) and makes audit output dramatically more useful per invocation.
50
+
51
+ ### 3. Hooks as algebraic effect handlers — NOT BUILT
52
+
53
+ Reframe Claude Code hooks as handlers for algebraic effects. Tool use in the agent becomes `perform EditFile(path, content)` — a suspension, not a side effect. The harness interprets it by running registered handlers in order (PreToolUse handlers first, then the real tool, then PostToolUse). Each handler is a pure function `Request -> Handled | Rewrite | Continue`. This is what Effect.ts `provide` does. vigiles cannot change the harness, but it **can** ship a spec-time model of hooks with the same semantics, and a linter that catches hook ordering bugs. The payoff is: you can reason about hook stacks the way you reason about middleware, not the way you reason about shell scripts.
54
+
55
+ ### 4. Skill combinators: retry, fallback, parallel, race — NOT BUILT
56
+
57
+ Once skills are typed as `Skill<Ctx>`, combinators fall out:
58
+
59
+ - `retry(n, skill)` — run up to n times, return first success
60
+ - `fallback(a, b)` — if `a` fails, try `b` with the same input
61
+ - `parallel([...skills])` — fan-out/fan-in where all branches must succeed
62
+ - `race([...skills])` — first to return a Result wins, others canceled
63
+ - `timeout(ms, skill)` — bound wall-clock
64
+ - `tap(log, skill)` — pre/post instrumentation without touching the skill body
65
+
66
+ These are exactly the combinators `Effect.retry`, `Effect.orElse`, `Effect.all`, `Effect.race`, `Effect.timeout`, `Effect.tap` ship today for arbitrary computations. Porting them to skills gives users composable retry/fallback without hand-rolling control flow in bash. vigiles ships them as spec builders that compile to prose explaining the combinator in the target SKILL.md.
67
+
68
+ ### 5. Event-sourced session state — PARTIALLY SHIPPED (Merkle history is event-sourced with append-only chain + verify(); no fork/replay exposed)
69
+
70
+ Treat the session transcript as an **event log**: each user message, model response, tool call, hook fire is an event. Session state at any point is `events.reduce(step, initial)`. Two properties fall out for free:
71
+
72
+ - **Replay**: re-run `reduce` from event N to reconstruct state at turn N. Debugging a weird agent decision becomes point-in-time inspection.
73
+ - **Fork**: `events.slice(0, n).concat(newEvent)` forks the session at turn n with a different branch. Lets you A/B a prompt change without losing the prefix.
74
+
75
+ This is what Redux/Elm do and what Effect's `Fiber` model does internally. The harness already persists the transcript on disk; vigiles's role is a **spec-level invariant** that every rule produces deterministic output from the same input prefix — so replays match. An audit check: "replay session X from transcript, compare final state to recorded — diverge = bug."
76
+
77
+ ### 6. Lenses for settings.json — NOT BUILT
78
+
79
+ `settings.json` is the agent's config: hooks, tool permissions, env. Today editing it is read-JSON, mutate, write. That loses atomicity and composition. Optics-ts / monocle-ts lenses let you express edits as `over(settingsLens.hooks.preToolUse, prepend(newHook))(settings)` — pure function, fully composable, trivially reversible. vigiles can ship a lens-based settings editor that underlies `vigiles init --install-hooks`, guaranteeing that concurrent edits from different commands don't stomp each other. The generalization: Claude Code config is a product type, and product types are what lenses are for.
80
+
81
+ ### 7. Skill type signatures in the spec — NOT BUILT
82
+
83
+ `skill("review-pr", { input: PrNumber, output: ReviewReport, may: [ReadRepo, PostComment] })`. The `may` clause is an **effect annotation**: this skill is allowed to read the repo and post comments, nothing else. Compiles to a permissions section in SKILL.md that Claude Code can parse at invocation time, and a PreToolUse hook that blocks anything outside the declared effect set. This is how Koka / Eff / Frank handle effect rows at the type level; porting it down to skill metadata means the harness can enforce effect bounds **per skill** instead of per-session globally.
84
+
85
+ ### 8. Kleisli composition and `>=>` — NOT BUILT
86
+
87
+ If skills are `A -> Effect<B>`, the operator to compose them is **Kleisli composition** (`>=>` in Haskell, `>>` in PureScript): `(f >=> g) = \a -> f(a).andThen(g)`. This is just function composition in the Effect category. Why it matters for vigiles: if the spec lets you write `pipeline("ship-pr", review >=> fix >=> push)`, the compiler can statically check that the output type of `review` matches the input type of `fix`. Type-safe multi-skill workflows. Today these workflows are written as bash or as prose instructions that the agent interprets; Kleisli composition makes them **compile-time verifiable graphs**.
88
+
89
+ ### 9. Reader monad for shared context — NOT BUILT
90
+
91
+ Every hook and skill gets the same `{cwd, env, transcript, user, project}` passed in. That is the Reader monad: `Reader<Env, A> = (Env) -> A`. The payoff: `Reader.ask` lets a deeply nested combinator access the environment without threading it through every intermediate function, and `local(f, reader)` lets a combinator run its body with a **locally modified** environment without mutating the real one. For hooks this means you can write a hook that scopes `cwd` to a subdirectory for its inner work and transparently restores it afterward. vigiles can declare Reader-shaped context in the spec and verify every hook/skill's signature conforms.
92
+
93
+ ### 10. Pure replay harness for hook testing — NOT BUILT
94
+
95
+ Today, testing a hook means setting up a real Claude Code session, triggering the tool, and reading the output. That is slow and flaky. If hooks are pure functions of `(Request, Env) -> Response`, you can **unit test** them with `fast-check` — generate random requests, assert invariants like "PreToolUse never returns both Allow and Rewrite" or "PostToolUse output is either the original result or a Result value, never undefined." vigiles ships a `vigiles test-hooks` subcommand that loads `settings.json`, extracts each hook as a pure function, and runs property tests against it. Catches the silent-matcher-ignore and trailing-wildcard anti-patterns from the agent-integration doc with **100% coverage** because the property test tries every shape.
96
+
97
+ ## Why Railway, specifically, for skills
98
+
99
+ Scott Wlaschin's railway pattern has one killer property: **errors and happy path have the same shape.** Both are `Result<T, E>`. Both flow through the same pipeline. The skill author writes code as if nothing can fail; the combinators handle the failure plumbing. Agents are spectacularly bad at writing correct error handling — they `try { ... } catch (e) { console.log(e); }` and move on. Railway removes the temptation entirely: there is no `try` to write because there is no `throw`. The skill returns `Result.err(Diagnostic.MissingFile(path))` and the caller either keeps going or bails at the boundary.
100
+
101
+ Concretely for a skill like "review a PR":
102
+
103
+ ```ts
104
+ const reviewPr: Skill<PrCtx> = (ctx) =>
105
+ fetchPr(ctx.prNumber)
106
+ .andThen(loadDiff)
107
+ .andThen(checkStyle)
108
+ .andThen(runAudit)
109
+ .andThen(postComment)
110
+ .mapErr((diagnostic) => ({
111
+ ...ctx,
112
+ diagnostics: [...ctx.diagnostics, diagnostic],
113
+ }));
114
+ ```
115
+
116
+ Every step can fail. The failure type is the same for all of them. The success path reads top-to-bottom with no `if` branches. A model looking at this pipeline can extend it (`checkSecrets.andThen(postComment)`) by adding one line. A model looking at the try/catch equivalent has to find the right `catch` block, decide if the new step needs its own, and often forgets. Railway is not just nice for humans — it is **easier for agents to modify correctly** than nested try/catch, which is the whole point.
117
+
118
+ ## What vigiles actually ships
119
+
120
+ None of the above asks Claude Code's harness to change. vigiles's leverage is the **spec layer**:
121
+
122
+ - `skill(name, { input, output, may, steps })` builder in `src/spec.ts`
123
+ - Compiles to a SKILL.md documenting the pipeline, effect set, and failure modes
124
+ - Audit check that every step referenced actually exists as a file or subskill
125
+ - Optional: a tiny runtime wrapper (`@vigiles/skill`) that provides the neverthrow-based `Skill<Ctx>` type and combinators, so users who want to adopt the pattern in their own tooling can import a shared definition
126
+ - Hook validation subcommand (`vigiles audit --hooks`) that treats hooks as pure functions and property-tests them
127
+
128
+ The runtime wrapper is optional. The spec layer is the thing. Everything else is documentation of the pattern with a compile-time check that the documentation matches reality.
129
+
130
+ ### 11. Adversarial differential replay — NOT BUILT
131
+
132
+ Record the Merkle chain from a session (every mutation + proof receipt). Replay the same sequence through a different LLM (or different temperature) and compare: which mutations does each accept/reject? Divergence = the mutation is model-sensitive, which means the proof suite isn't strict enough (the deterministic gate should have caught it). This is differential testing applied to agent behavior, and the Merkle chain is already the exact replay log it needs.
133
+
134
+ ### 12. Audit stage as a composable functor — NOT BUILT
135
+
136
+ Each audit stage today is an imperative function that mutates `silent` state and accumulates counters. Make each stage a pure `(AuditReport) → AuditReport` transformation. Stages compose via plain function composition. The `silent`/`loud` threading becomes a Reader effect; the counter accumulation is just field merges. Users could define custom audit pipelines in their spec: `auditStages: [verifyHashes, inlineRules, coverage]` — pick what runs, skip what doesn't apply.
137
+
138
+ ### 13. Hook contract testing via fast-check — NOT BUILT
139
+
140
+ Treat each Claude Code hook (PreToolUse, PostToolUse, SessionStart) as a pure function of `(Request, Env) → Response` and property-test it with fast-check: generate random tool-use requests, assert invariants like "PreToolUse never returns both Allow and Block" or "PostToolUse output is always valid JSON." The existing hook scripts are bash, so the "function" is actually `echo $INPUT | bash hook.sh | read $OUTPUT` — property testing over that subprocess boundary catches the silent-matcher-ignore and trailing-wildcard anti-patterns from the agent-integration doc with machine coverage instead of manual audit.
141
+
142
+ ## Priority
143
+
144
+ 1. **`skill()` builder with typed input/output** — the smallest change, unblocks everything else. Write the builder, compile it to the existing SKILL.md format, audit for missing referenced files.
145
+ 2. **Validation applicative for audit** — one-library refactor, immediate UX win ("audit reports 7 issues" instead of "audit failed at issue 1").
146
+ 3. **Skill combinators as spec builders** — `retry()`, `fallback()`, `parallel()` that compile to prose. No runtime required, just better SKILL.md output.
147
+ 4. **Hook property testing** — depends on having a spec model of hooks first, then layering fast-check.
148
+ 5. **Effect annotations + lens-based settings editor** — the sharpest tools but the most design work. Ship last.
149
+
150
+ The short version: the previous FP doc was about TS code patterns for LLM output. This one is about applying the same shapes **one level up** to the agent runtime. Railway for skills is not a joke — it is the only way to compose agent pipelines that both humans and models can edit safely.
@@ -0,0 +1,131 @@
1
+ # Functional Programming Techniques for Deterministic AI Code
2
+
3
+ ## Framing
4
+
5
+ LLMs are nondeterministic by construction. Temperature 0 reduces variance but does not eliminate it. The practical question is not "how do we make the model deterministic" but "how do we **shape the code the model writes** so that nondeterminism is harmless." Functional programming has spent four decades answering this exact question for a different reason (human reasoning about concurrent/parallel/distributed code), and its tools transplant cleanly.
6
+
7
+ The thesis: **pure functions with total signatures and exhaustive pattern matches are the substrate on which agents fail loudest and recover fastest.** A pure function that returns the wrong value fails a property test. A function that throws produces a stack trace in production. Agents are much better at fixing the first one.
8
+
9
+ This doc surveys the FP techniques that have shown up in the 2025–2026 literature on AI code quality, notes the specific tooling available in the TS/JS ecosystem, and proposes 10 vigiles features that operationalize them.
10
+
11
+ ## Why FP is a good fit for LLM output
12
+
13
+ Three properties make FP uniquely useful here:
14
+
15
+ 1. **Totality.** A pure total function is a contract: "for every input in type A, I produce exactly one value in type B." Agents that write code under that contract cannot silently skip edge cases — unhandled cases become type errors. `ts-pattern`'s exhaustiveness check is the clearest example: the compiler refuses to build until every branch is handled.
16
+
17
+ 2. **Referential transparency.** `f(x) === f(x)` for all x. This is what makes property-based testing work: you can generate 10,000 random inputs and assert an invariant without worrying about order or state. arxiv 2506.18315 ("Property-Generated Solver") showed a **23–37% pass@1 improvement** on code tasks when the harness generated properties first and let the model iterate against failing counter-examples. That only works on pure code.
18
+
19
+ 3. **Composition.** Small building blocks with typed connectors force the agent into a local reasoning window. It is much easier for a model to write `pipe(x, validate, normalize, save)` correctly than it is to write a 200-line function that weaves the same steps through mutable state. Railway-oriented programming (Scott Wlaschin) is the cleanest articulation: every step is `A -> Result<B, E>`, errors short-circuit, success flows down the happy path.
20
+
21
+ ## The counter-evidence
22
+
23
+ arxiv 2601.02060 ("FPEval") benchmarked code generation on **purely functional languages** (Haskell, OCaml, Elm) and found LLMs perform **15–40% worse** than on Python/TS. The reason is training-distribution: there is orders of magnitude more imperative code in the training set. So pure FP languages are not the answer.
24
+
25
+ The answer is **FP-style code in mainstream languages**, specifically TypeScript. The training set is huge, the type system is expressive enough, and the ecosystem has a cluster of libraries (Effect, neverthrow, ts-pattern, fast-check, Zod) that make FP idioms first-class without forcing a paradigm shift. vigiles can bundle those libraries into enforce-preset bundles and get the benefits without the performance hit.
26
+
27
+ ## Ecosystem survey (TypeScript, 2025)
28
+
29
+ | Tool | What it gives you | Relevance |
30
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------- |
31
+ | `eslint-plugin-functional` | `no-throw-statements`, `immutable-data`, `no-let`, `no-loop-statements`, `prefer-readonly-type` | Direct `enforce()` targets |
32
+ | `neverthrow` | `Result<T, E>` + `ResultAsync` + `.map`/`.andThen`/`.mapErr` | Railway-style error handling |
33
+ | `Effect.ts` | Full effect system: `Effect<R, E, A>`, managed concurrency, resources, retries | Heavier but complete |
34
+ | `fp-ts` / `ts-results` | Classical Either/Option/Task | Smaller footprint than Effect |
35
+ | `ts-pattern` | Exhaustive pattern matching via `P.infer`, `.exhaustive()` | Totality on tagged unions |
36
+ | `monocle-ts` / `optics-ts` | Lenses, prisms, traversals | Immutable deep updates |
37
+ | `zod` / `valibot` / `@effect/schema` | Parse-don't-validate at boundaries | Purity zones need these |
38
+ | `fast-check` | Property-based testing, shrinking | Matches 2506.18315 technique |
39
+ | `@typescript-eslint/switch-exhaustiveness-check` | Compile-time exhaustive switch | Cheap totality win |
40
+ | CodeQL (Datalog) / Flux / Liquid Haskell | Refinement / dependent types | Out of scope for us but worth knowing |
41
+
42
+ ## How this interacts with vigiles
43
+
44
+ vigiles is a spec-to-markdown compiler with a linter-cross-reference engine. It does not write code. But it decides **which rules the agent is told to follow**, and it verifies those rules against real linter config. That makes it the natural home for a curated set of FP presets: instead of forcing every team to discover `neverthrow` and configure `eslint-plugin-functional` from scratch, vigiles can ship a `pureZone()` builder that encodes the whole bundle.
45
+
46
+ Crucially, this is not reimplementing a linter. Every rule still runs in ESLint / TypeScript / ts-pattern. vigiles just decides **where** they apply, tells the agent in prose, and verifies the wiring.
47
+
48
+ ## Ten vigiles ideas
49
+
50
+ ### 1. FP Determinism Preset — DROPPED (just an eslint-plugin-functional wrapper, no moat)
51
+
52
+ A named bundle: `fpDeterminism()` returns an array of `enforce()` calls covering the minimum viable FP-for-agents rule set. Target:
53
+
54
+ - `functional/no-throw-statements`
55
+ - `functional/no-let`
56
+ - `functional/immutable-data`
57
+ - `functional/no-loop-statements` (allows `map`/`filter`/`reduce`)
58
+ - `@typescript-eslint/switch-exhaustiveness-check`
59
+ - `@typescript-eslint/no-floating-promises`
60
+
61
+ Users write `...fpDeterminism()` in a spec section and inherit the entire preset. Upgrading the preset upgrades every downstream spec at once.
62
+
63
+ ### 2. Pure zones — NOT BUILT
64
+
65
+ Builder: `pureZone("src/core/**", { allow: ["date-fns"] })`. Compiles to a section in the target markdown saying "inside `src/core/**`, no I/O, no throws, no globals, no Date.now / Math.random / fetch / fs." Backed by `eslint-plugin-functional` + a focused ESLint override for that glob. The agent sees "you are in a pure zone" in context; the linter enforces it for real. Two-layer defense: prose for reasoning, lint for deterministic rejection.
66
+
67
+ ### 3. Rule combinator API — NOT BUILT
68
+
69
+ Expose `Rule<A, B>` as a public plugin type so users can compose their own rules: `pipe(spec.rules, strengthen("no-console"), restrictGlob("src/core/**"))`. Internally this is the same data the compiler already works with; externally it turns vigiles into a spec-programming library rather than a config file. Enables `fpDeterminism()` (#1) and every other preset to be built out of the same primitives users can use.
70
+
71
+ ### 4. PBT coverage check — NOT BUILT
72
+
73
+ Audit-time assertion: for every file matching `src/**/*.ts` that exports a pure function (no `Promise`, no parameter of type `unknown`, no `void` return), require a colocated `*.property.test.ts`. Dovetails directly with arxiv 2506.18315 — properties catch the failure modes tests miss, and agents iterate on property failures faster than they iterate on hand-written tests. `fast-check` already handles the runtime. vigiles just makes coverage visible.
74
+
75
+ ### 5. Content-addressed compile cache — NOT BUILT
76
+
77
+ Hash every spec by its AST + transitive imports; cache the compiled markdown by that hash. Re-running `vigiles compile` on an unchanged spec becomes a no-op. Bazel-style memoization. The reason this belongs in an FP doc: it only works because the compiler is itself a pure function from spec AST to markdown. The more we lean on FP internally, the cheaper incremental work gets.
78
+
79
+ ### 6. Refinement at boundaries via schema libraries — NOT BUILT
80
+
81
+ New `enforce()` target category: `zod/strict-object`, `valibot/parse-not-safeParse`, `@effect/schema/decodeUnknown`. vigiles verifies the schema library is in the project and the rule is enabled. The spec then tells the agent "all external input must be parsed via Zod before it enters a pure zone." Parse-don't-validate is the single most effective technique for containing AI-generated input handling code — it converts runtime surprises into boundary errors.
82
+
83
+ ### 7. Exhaustiveness-checking preset — NOT BUILT
84
+
85
+ One-liner: `enforce("@typescript-eslint/switch-exhaustiveness-check")` + `enforce("ts-pattern/exhaustive")`. Ships as a standalone mini-preset because it is by far the highest leverage single rule: it turns every `switch` on a discriminated union into a type-level guarantee. LLMs drop cases constantly; this catches every single one at compile time, not review time.
86
+
87
+ ### 8. Result/Either error handling preset — NOT BUILT
88
+
89
+ Banned: `throw`, `try/catch` outside top-level handlers, `.catch(() => null)`. Required: return types shaped `Result<T, E>` via neverthrow (or Effect.ts if the project already uses it). vigiles verifies the project has neverthrow in `package.json` and that the relevant eslint-plugin-functional rules are on. Agent sees "errors are values; use `.mapErr` to transform them." This is the single largest departure from standard TS agent output, so the preset needs to be loud in the compiled markdown — but the payoff is that error paths become testable.
90
+
91
+ ### 9. Immutable-by-default preset — NOT BUILT
92
+
93
+ Bundle: `no-let`, `immutable-data`, `no-loop-statements`, `prefer-readonly-type`, plus a `check()`-equivalent that greps for `.push(`, `.splice(`, `Object.assign`, and direct array index assignment. Not every project can adopt this wholesale, which is why it ships as a separate preset applied via `pureZone()`. Inside a pure zone, the agent is forced into `[...xs, y]` and `{...obj, k: v}` — which it handles fine.
94
+
95
+ ### 10. Lens recommendation detector — NOT BUILT
96
+
97
+ Audit-time scan: detect the pattern `{...a, b: {...a.b, c: {...a.b.c, d: value}}}` (spread nesting ≥ 3) and suggest switching to `monocle-ts` or `optics-ts`. Pure diagnostic — vigiles does not rewrite the code. But it does catch the single ugliest FP anti-pattern LLMs produce when asked to "update this field immutably": nested spreads that nobody can read and nobody tests. Flagging them directs the agent toward the lens library the project presumably already has.
98
+
99
+ ### 11. Immutability as a security property — NOT BUILT
100
+
101
+ The 15+ defensive-copy fixes from PR #16 proved that shared mutable references are not just a style concern — they're **bypass vectors** in proof-gated systems. Every live reference the engine returned (rules, history nodes, receipts, allowWeaken Set) was a surface where a caller could mutate state without running proofs. Ship this insight as a rule: `enforce("functional/immutable-data")` in evolve.ts and proofs.ts themselves, documented with the rationale "immutability is tamper prevention, not taste." Dogfood vigiles on vigiles.
102
+
103
+ ### 12. Budget-aware fitness with a cliff — NOT BUILT
104
+
105
+ The current fitness formula (`coverage × (1 - redundancy) × (1 - budgetPressure)`) penalizes ANY token growth linearly, which makes `acceptNeutral` nearly useless: adding a rule always increases tokens, always decreases fitness, always gets rejected. A better formula: `coverage × (1 - redundancy) × min(1, maxTokens / tokens)` — score is flat at 1.0 until you exceed the token budget, then drops off a cliff. This lets useful additions pass while still enforcing the hard cap. Simple change, fixes a real design bug discovered during the `proposeAll` test rewrite.
106
+
107
+ ### 13. Deterministic seed for property tests in CI — NOT BUILT
108
+
109
+ `propertyTest()` in proofs.ts accepts a `seed` option, but nothing in the CI pipeline uses it. Ship a convention: `vigiles audit --pbt-seed=$GITHUB_SHA` so property-based tests are deterministic per commit (reproducible failures) but vary across commits (explore the search space over time). Combines the benefits of deterministic CI (no flakes) with the coverage of randomized testing (different inputs each push).
110
+
111
+ ## Railway-oriented programming for skills
112
+
113
+ Tangent worth noting: the same Result-pipe pattern applies to vigiles's own internal surface. If every audit step, every compile step, every linter-verification step is typed as `(Input) => Result<Output, Diagnostic[]>`, the whole compiler is one `pipe()` chain from spec AST to final output. Errors collect instead of throwing; the top-level command decides whether to short-circuit or aggregate. This is how Effect.ts pipelines are structured, and it would make the compiler's own behavior under partial failure **provably** correct instead of "seems to work in tests." A good reason to adopt neverthrow internally even before we ship it as a preset.
114
+
115
+ ## What we are NOT doing
116
+
117
+ - Not adopting Haskell. FPEval says no. TypeScript FP presets are the sweet spot.
118
+ - Not shipping our own `Result` type. neverthrow is a single small dependency and is already the community default.
119
+ - Not rewriting the compiler in Effect.ts. Too much churn. neverthrow + pipe is enough.
120
+ - Not building a refinement-type system. Liquid TypeScript does not exist yet and Flux is Rust-only; Zod at boundaries is 90% of the value for 5% of the cost.
121
+
122
+ ## Priority
123
+
124
+ 1. **#1 fpDeterminism preset** + **#7 exhaustiveness preset** — single day of work, zero new dependencies beyond what users opt into, immediate win
125
+ 2. **#2 pureZone + #6 schema boundaries** — the core pair; these define where the other presets apply
126
+ 3. **#4 PBT coverage check** — wires into audit, no new subsystem, matches the strongest academic signal
127
+ 4. **#3 rule combinator API** — enables plugins, unlocks user-contributed presets, 1–2 days of refactor
128
+ 5. **#8 Result preset + #9 immutable preset + #10 lens detector** — powerful but opinionated; ship after the first four land so adopters have the machinery to opt in surgically
129
+ 6. **#5 content-addressed cache** — pure performance work; ship when compile times become a complaint
130
+
131
+ The TL;DR is: we do not need to change the compiler to get the benefit of FP for AI code. We need to ship curated rule bundles, verify them against real linters (which vigiles already does), and let `pureZone()` and `fpDeterminism()` carry the reasoning load for users.