fosm-rails 0.1.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.
- checksums.yaml +7 -0
- data/AGENTS.md +384 -0
- data/LICENSE +115 -0
- data/README.md +322 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/fosm/rails/application.css +15 -0
- data/app/controllers/fosm/admin/agents_controller.rb +242 -0
- data/app/controllers/fosm/admin/apps_controller.rb +34 -0
- data/app/controllers/fosm/admin/base_controller.rb +15 -0
- data/app/controllers/fosm/admin/dashboard_controller.rb +25 -0
- data/app/controllers/fosm/admin/settings_controller.rb +44 -0
- data/app/controllers/fosm/admin/transitions_controller.rb +22 -0
- data/app/controllers/fosm/admin/webhooks_controller.rb +37 -0
- data/app/controllers/fosm/application_controller.rb +23 -0
- data/app/controllers/fosm/rails/application_controller.rb +6 -0
- data/app/helpers/fosm/application_helper.rb +19 -0
- data/app/helpers/fosm/rails/application_helper.rb +11 -0
- data/app/jobs/fosm/application_job.rb +6 -0
- data/app/jobs/fosm/rails/application_job.rb +6 -0
- data/app/jobs/fosm/webhook_delivery_job.rb +52 -0
- data/app/models/fosm/application_record.rb +6 -0
- data/app/models/fosm/rails/application_record.rb +7 -0
- data/app/models/fosm/transition_log.rb +27 -0
- data/app/models/fosm/webhook_subscription.rb +15 -0
- data/app/views/fosm/admin/agents/chat.html.erb +242 -0
- data/app/views/fosm/admin/agents/show.html.erb +166 -0
- data/app/views/fosm/admin/apps/show.html.erb +114 -0
- data/app/views/fosm/admin/dashboard/index.html.erb +82 -0
- data/app/views/fosm/admin/settings/show.html.erb +63 -0
- data/app/views/fosm/admin/transitions/index.html.erb +65 -0
- data/app/views/fosm/admin/webhooks/index.html.erb +51 -0
- data/app/views/fosm/admin/webhooks/new.html.erb +45 -0
- data/app/views/layouts/fosm/application.html.erb +41 -0
- data/app/views/layouts/fosm/rails/application.html.erb +17 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20240101000001_create_fosm_transition_logs.rb +23 -0
- data/db/migrate/20240101000002_create_fosm_webhook_subscriptions.rb +16 -0
- data/lib/fosm/agent.rb +232 -0
- data/lib/fosm/configuration.rb +50 -0
- data/lib/fosm/engine.rb +133 -0
- data/lib/fosm/errors.rb +31 -0
- data/lib/fosm/lifecycle/definition.rb +103 -0
- data/lib/fosm/lifecycle/event_definition.rb +27 -0
- data/lib/fosm/lifecycle/guard_definition.rb +16 -0
- data/lib/fosm/lifecycle/side_effect_definition.rb +16 -0
- data/lib/fosm/lifecycle/state_definition.rb +18 -0
- data/lib/fosm/lifecycle.rb +173 -0
- data/lib/fosm/rails/engine.rb +9 -0
- data/lib/fosm/rails/version.rb +9 -0
- data/lib/fosm/rails.rb +9 -0
- data/lib/fosm/registry.rb +29 -0
- data/lib/fosm/version.rb +3 -0
- data/lib/fosm-rails.rb +40 -0
- data/lib/generators/fosm/app/app_generator.rb +106 -0
- data/lib/generators/fosm/app/templates/agent.rb.tt +26 -0
- data/lib/generators/fosm/app/templates/controller.rb.tt +56 -0
- data/lib/generators/fosm/app/templates/migration.rb.tt +14 -0
- data/lib/generators/fosm/app/templates/model.rb.tt +31 -0
- data/lib/generators/fosm/app/templates/views/_form.html.erb.tt +24 -0
- data/lib/generators/fosm/app/templates/views/index.html.erb.tt +37 -0
- data/lib/generators/fosm/app/templates/views/new.html.erb.tt +4 -0
- data/lib/generators/fosm/app/templates/views/show.html.erb.tt +57 -0
- data/lib/tasks/fosm/rails_tasks.rake +4 -0
- metadata +139 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: caa495883333e121bf051fcc48f83f16a9182bf0b3ab04257f0c6eb4d6aa107b
|
|
4
|
+
data.tar.gz: 95d917636381c725c1bc35f105775bf7dd262064143d1779bf2eee24a092fb6a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b0a8dcc832dc6a0b459023dc630a224d45d321add78a180d96d0e588b7080e1546809001bbd8e48441e67ca39062fca5ad3fb14e2319d2001e3ad20b395217ba
|
|
7
|
+
data.tar.gz: 73ae82e375097698267f092dbffea06f2472ef36ec4f158213daf3186890d51ec5a53e2f7fb26fa974827f0180bae3bf6bf2e9c62100e7b74828f2bf1f9dafa9
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# AGENTS.md — Understanding and Extending fosm-rails
|
|
2
|
+
|
|
3
|
+
This file is for AI coding agents, contributors, and developers who want to understand the deep design philosophy behind this engine and contribute to it thoughtfully.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What is FOSM?
|
|
8
|
+
|
|
9
|
+
**FOSM** stands for **Finite Object State Machine**.
|
|
10
|
+
|
|
11
|
+
The "finite" part is standard: a countable number of states, with explicit transitions between them. The "object" part is what makes it different: the state machine is **bound to a specific business entity** — an Invoice, a Candidate, a Contract, a Deal. The lifecycle is not an abstract workflow. It *is* the domain object.
|
|
12
|
+
|
|
13
|
+
### The core problem FOSM solves
|
|
14
|
+
|
|
15
|
+
Every business application has objects that move through lifecycles. An Invoice gets drafted, sent, paid or disputed, possibly cancelled. A Candidate gets applied, screened, interviewed, offered, hired or rejected. A Contract gets drafted, reviewed, signed, executed.
|
|
16
|
+
|
|
17
|
+
The conventional CRUD approach models these as a table row with a `status` column — a mutable string with no opinions about what comes before or after. The business rules that govern valid transitions end up scattered across the codebase:
|
|
18
|
+
|
|
19
|
+
- `if`-statements in controllers
|
|
20
|
+
- `before_save` callbacks in models
|
|
21
|
+
- validation logic in service objects
|
|
22
|
+
- guard conditions in Sidekiq jobs
|
|
23
|
+
|
|
24
|
+
None of it is connected. Ask a new developer "what are the rules for invoices?" and the honest answer is: read every file that touches the `Invoice` model, then pray.
|
|
25
|
+
|
|
26
|
+
FOSM declares the rules **as part of what it means to be an Invoice**:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class Invoice < ApplicationRecord
|
|
30
|
+
include Fosm::Lifecycle
|
|
31
|
+
|
|
32
|
+
lifecycle do
|
|
33
|
+
state :draft, initial: true
|
|
34
|
+
state :sent
|
|
35
|
+
state :paid, terminal: true
|
|
36
|
+
state :overdue
|
|
37
|
+
state :cancelled, terminal: true
|
|
38
|
+
|
|
39
|
+
event :send_invoice, from: :draft, to: :sent
|
|
40
|
+
event :pay, from: [:sent, :overdue], to: :paid
|
|
41
|
+
event :mark_overdue, from: :sent, to: :overdue
|
|
42
|
+
event :cancel, from: [:draft, :sent], to: :cancelled
|
|
43
|
+
|
|
44
|
+
guard :has_line_items, on: :send_invoice do |inv|
|
|
45
|
+
inv.line_items.any?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
side_effect :notify_client, on: :send_invoice do |inv, transition|
|
|
49
|
+
InvoiceMailer.send_to_client(inv).deliver_later
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That block is the complete lifecycle specification. There is no path from `draft` to `paid` — the machine won't allow it. There is no path from `paid` to anything — it's terminal. You can see the five states, the four events, the guard condition, and the side effect in twenty lines of code.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## The abstraction: one lifecycle definition, three superpowers
|
|
60
|
+
|
|
61
|
+
When a developer writes a lifecycle block and runs the generator, they get three things without writing any additional code:
|
|
62
|
+
|
|
63
|
+
**1. A CRUD application that enforces the lifecycle**
|
|
64
|
+
The generated controller and views allow users to create records and fire transitions. The UI only offers valid actions — the machine decides what buttons appear. Guards, terminal states, and invalid transitions are enforced at the Rails level.
|
|
65
|
+
|
|
66
|
+
**2. An immutable audit trail**
|
|
67
|
+
Every state change is written to `fosm_transition_logs` with the actor, timestamp, and metadata. No configuration required. The log is tamper-proof — read-only at the database level.
|
|
68
|
+
|
|
69
|
+
**3. A fully-configured AI agent**
|
|
70
|
+
The `Fosm::Agent` base class reads the lifecycle definition at runtime and auto-generates a complete set of Gemlings tools. You don't write a single line of agent code to get a working agent. The agent appears immediately at `/fosm/admin/apps/:slug/agent` after the lifecycle is defined.
|
|
71
|
+
|
|
72
|
+
This is the beauty of the FOSM abstraction: **the lifecycle is the single source of truth** for the CRUD rules, the audit log schema, and the AI agent's capabilities. They cannot drift from each other because they all read from the same definition.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Why FOSM matters now
|
|
77
|
+
|
|
78
|
+
State machines have existed for sixty years. The reason they didn't become the default paradigm for business software is the **specification problem**: you had to enumerate every state, every transition, every guard condition upfront. Business processes are messy, requirements shift, and Agile won because upfront specification is too expensive in a fast-moving world.
|
|
79
|
+
|
|
80
|
+
**AI dissolves this problem.**
|
|
81
|
+
|
|
82
|
+
An LLM already knows what an invoice lifecycle looks like. Tell it "build me an invoicing module" and it produces a reasonable first-draft state machine in seconds — because invoice processing is one of the most documented business processes in human history.
|
|
83
|
+
|
|
84
|
+
But there is a deeper point. When you pair AI with a FOSM-structured codebase, you get **bounded autonomy**:
|
|
85
|
+
|
|
86
|
+
- An AI agent operating within a FOSM system can only do what the state machine allows
|
|
87
|
+
- It cannot skip a step, invent a transition, or operate outside the declared lifecycle
|
|
88
|
+
- The machine is the guardrail — you don't need to trust the AI's judgment, only the state machine
|
|
89
|
+
- When the AI fires `send_invoice!(actor: :agent)`, if the invoice isn't in `draft` state, the machine refuses
|
|
90
|
+
|
|
91
|
+
**FOSM makes AI safe. AI makes FOSM practical. Neither works as well without the other.**
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Design principles of this engine
|
|
96
|
+
|
|
97
|
+
When contributing to fosm-rails, keep these principles in mind. They are not conventions — they are load-bearing.
|
|
98
|
+
|
|
99
|
+
### 1. `fire!` is the only mutation path
|
|
100
|
+
|
|
101
|
+
State must never change via `update(state: "paid")` or direct attribute assignment. The only valid path is `fire!(:event_name, actor:)`. This is what makes the audit trail complete, the guards enforceable, and the AI agents bounded.
|
|
102
|
+
|
|
103
|
+
**Do not** add any method to `Fosm::Lifecycle` that changes state without going through `fire!`.
|
|
104
|
+
|
|
105
|
+
### 2. Guards are pure functions
|
|
106
|
+
|
|
107
|
+
A guard receives the record and returns true or false. It has no side effects. This is critical for the `can_fire?` method — it is called to check availability without triggering anything. If a guard had side effects, `can_fire?` would have side effects, which would break the admin UI, the agent's `available_events_for_*` tool, and any code that inspects state.
|
|
108
|
+
|
|
109
|
+
**Do not** allow guards to modify state or trigger external calls.
|
|
110
|
+
|
|
111
|
+
### 3. Every transition is logged
|
|
112
|
+
|
|
113
|
+
`Fosm::TransitionLog` is immutable and append-only. The `before_update` and `before_destroy` callbacks raise `ActiveRecord::ReadOnlyRecord`. This is intentional — the audit trail must be complete and tamper-proof.
|
|
114
|
+
|
|
115
|
+
**Do not** add `updated_at` to `fosm_transition_logs`. Do not add a soft-delete mechanism.
|
|
116
|
+
|
|
117
|
+
### 4. Terminal states are irreversible by design
|
|
118
|
+
|
|
119
|
+
When a state is declared `terminal: true`, any attempt to fire an event from it raises `Fosm::TerminalState`. This is the architectural equivalent of a physical lock. Business logic that needs to "undo" a terminal state should use a compensating event (e.g., `reopen` that goes from `cancelled` to `draft`), not by removing the terminal constraint.
|
|
120
|
+
|
|
121
|
+
### 5. The lifecycle definition is the documentation
|
|
122
|
+
|
|
123
|
+
The admin explorer renders the lifecycle definition directly from the Ruby code — it doesn't read a separate diagram file or database record. This means the documentation is always accurate because it IS the running code.
|
|
124
|
+
|
|
125
|
+
**Do not** add a separate "description" mechanism that could drift from the actual implementation.
|
|
126
|
+
|
|
127
|
+
### 6. AI agents are bounded, not trusted
|
|
128
|
+
|
|
129
|
+
The `Fosm::Agent` base class generates exactly one Gemlings tool per lifecycle event. The tool calls `fire!` which enforces the machine rules. The AI cannot fire an event that doesn't exist. The AI cannot bypass a guard. The AI cannot modify state directly.
|
|
130
|
+
|
|
131
|
+
When adding new agent capabilities, add new **lifecycle events** (which automatically generate new agent tools), not new raw database tools.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Engine architecture
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
lib/
|
|
139
|
+
fosm/
|
|
140
|
+
lifecycle.rb ← ActiveSupport::Concern — the main DSL mixin
|
|
141
|
+
lifecycle/
|
|
142
|
+
definition.rb ← Holds states/events/guards/side_effects for one model
|
|
143
|
+
state_definition.rb ← Value object: name, initial?, terminal?
|
|
144
|
+
event_definition.rb ← Value object: name, from_states, to_state, guards, side_effects
|
|
145
|
+
guard_definition.rb ← Named callable: (record) → bool
|
|
146
|
+
side_effect_definition.rb ← Named callable: (record, transition) → void
|
|
147
|
+
agent.rb ← Base class: model_class DSL + Gemlings tool generation
|
|
148
|
+
configuration.rb ← Fosm.configure { } block
|
|
149
|
+
registry.rb ← Global slug → model_class map
|
|
150
|
+
errors.rb ← Fosm::InvalidTransition, GuardFailed, etc.
|
|
151
|
+
engine.rb ← Rails::Engine, migration hooks, auto-registration
|
|
152
|
+
fosm-rails.rb ← Entry point
|
|
153
|
+
|
|
154
|
+
app/
|
|
155
|
+
models/fosm/
|
|
156
|
+
transition_log.rb ← Immutable audit trail (shared across all FOSM apps)
|
|
157
|
+
webhook_subscription.rb ← Admin-configured HTTP callbacks
|
|
158
|
+
controllers/fosm/
|
|
159
|
+
application_controller.rb ← Inherits from configured base_controller
|
|
160
|
+
admin/
|
|
161
|
+
base_controller.rb ← Admin auth before_action
|
|
162
|
+
dashboard_controller.rb
|
|
163
|
+
apps_controller.rb
|
|
164
|
+
transitions_controller.rb
|
|
165
|
+
webhooks_controller.rb
|
|
166
|
+
jobs/fosm/
|
|
167
|
+
webhook_delivery_job.rb ← Async HTTP POST with HMAC signing, retries
|
|
168
|
+
|
|
169
|
+
lib/generators/fosm/app/
|
|
170
|
+
app_generator.rb ← rails generate fosm:app
|
|
171
|
+
templates/
|
|
172
|
+
model.rb.tt
|
|
173
|
+
controller.rb.tt
|
|
174
|
+
agent.rb.tt
|
|
175
|
+
migration.rb.tt
|
|
176
|
+
routes.rb.tt
|
|
177
|
+
views/
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## How `fire!` works
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
record.fire!(:send_invoice, actor: current_user)
|
|
186
|
+
|
|
187
|
+
1. Look up the event definition in fosm_lifecycle
|
|
188
|
+
2. Check: does the event exist? → raise UnknownEvent if not
|
|
189
|
+
3. Check: is current state terminal? → raise TerminalState if yes
|
|
190
|
+
4. Check: is the event valid from current state? → raise InvalidTransition if not
|
|
191
|
+
5. Run guards: each guard.call(record) → raise GuardFailed if any return false
|
|
192
|
+
6. Begin database transaction:
|
|
193
|
+
a. UPDATE record SET state = 'sent'
|
|
194
|
+
b. INSERT INTO fosm_transition_logs (...)
|
|
195
|
+
c. Run each side_effect.call(record, transition_data)
|
|
196
|
+
d. COMMIT (or ROLLBACK if any step raises)
|
|
197
|
+
7. Enqueue WebhookDeliveryJob asynchronously (outside transaction)
|
|
198
|
+
8. Return true
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The transaction ensures that if a side effect raises, the state update is rolled back. The webhook job fires outside the transaction so it doesn't delay the response and doesn't roll back if the HTTP call fails.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Gemlings: the required agent dependency
|
|
206
|
+
|
|
207
|
+
`gemlings` is declared as a **required dependency** in `fosm-rails.gemspec` — not optional. This is a deliberate design decision: the agent capability is not a plugin or an afterthought, it is a first-class output of every lifecycle definition.
|
|
208
|
+
|
|
209
|
+
When you add `gem "fosm-rails"` to a project, you get the agent framework automatically. Set `ANTHROPIC_API_KEY` (or another provider key such as `OPENAI_API_KEY` or `GEMINI_API_KEY`) in your environment and the agent is ready to use.
|
|
210
|
+
|
|
211
|
+
Supported LLM providers come from the `ruby_llm` gem that Gemlings depends on. The default model is `anthropic/claude-sonnet-4-20250514`. Override it per-agent:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class Fosm::InvoiceAgent < Fosm::Agent
|
|
215
|
+
model_class Fosm::Invoice
|
|
216
|
+
default_model "openai/gpt-4o"
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Compatibility note
|
|
221
|
+
|
|
222
|
+
The engine's `initializer "fosm.configuration"` includes two runtime patches to `Gemlings::Memory` and `Gemlings::Models::RubyLLMAdapter`. These patches fix Anthropic API incompatibilities in Gemlings' `ToolCallingAgent`:
|
|
223
|
+
|
|
224
|
+
1. **Trailing whitespace** — Anthropic rejects assistant messages whose content ends with whitespace. The patch strips it from all messages in `to_messages`.
|
|
225
|
+
2. **Tool result format** — After a tool_use block, Anthropic requires a structured `tool_result` block in the next user message. Gemlings generates a plain `"Observation: ..."` text message instead. The patch rewrites these into the correct `{ type: "tool_result", tool_use_id: ..., content: ... }` format, and `load_messages` uses `role: :tool` when passing them to `ruby_llm`.
|
|
226
|
+
|
|
227
|
+
These patches are applied once at boot via `prepend` and are invisible to application code. If Gemlings fixes these issues upstream, the patches become no-ops.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## The Admin Agent Explorer
|
|
232
|
+
|
|
233
|
+
For every registered FOSM app, the admin provides two agent-specific pages:
|
|
234
|
+
|
|
235
|
+
**`/fosm/admin/apps/:slug/agent`** — The Tool Catalog
|
|
236
|
+
- Lists all auto-generated tools (read tools and mutate tools) with their descriptions and parameter signatures
|
|
237
|
+
- Provides a **Direct Tool Tester** — invoke any tool from the browser with no LLM involved. Useful for verifying tool behaviour and debugging lifecycle configurations.
|
|
238
|
+
- Shows the **System Prompt** — the exact constraints injected into the LLM, including terminal states and the instruction to always call `available_events_for_*` before firing.
|
|
239
|
+
|
|
240
|
+
**`/fosm/admin/apps/:slug/agent/chat`** — The Agent Chat
|
|
241
|
+
- Multi-turn conversation with the live Gemlings agent
|
|
242
|
+
- Each response shows a collapsible **reasoning trace**: tool calls, observations, and the LLM's thought process
|
|
243
|
+
- "New conversation" clears context and starts fresh
|
|
244
|
+
|
|
245
|
+
The Tool Tester is particularly valuable during development: you can verify that your guards are working correctly, that events are available in the right states, and that your lifecycle behaves as designed — all without writing a test.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## The Gemlings agent tools
|
|
250
|
+
|
|
251
|
+
For a model with this lifecycle:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
lifecycle do
|
|
255
|
+
state :draft, initial: true
|
|
256
|
+
state :sent
|
|
257
|
+
state :paid, terminal: true
|
|
258
|
+
|
|
259
|
+
event :send_invoice, from: :draft, to: :sent
|
|
260
|
+
event :pay, from: :sent, to: :paid
|
|
261
|
+
end
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The following Gemlings tools are auto-generated:
|
|
265
|
+
|
|
266
|
+
| Tool name | What it does |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `list_invoices` | `Invoice.all` (or filtered by `state:`) |
|
|
269
|
+
| `get_invoice` | `Invoice.find(id)` with state + available_events |
|
|
270
|
+
| `available_events_for_invoice` | `record.available_events` |
|
|
271
|
+
| `transition_history_for_invoice` | `TransitionLog.for_record(...)` |
|
|
272
|
+
| `send_invoice_invoice` | `record.fire!(:send_invoice, actor: :agent)` |
|
|
273
|
+
| `pay_invoice` | `record.fire!(:pay, actor: :agent)` |
|
|
274
|
+
|
|
275
|
+
The event tools are named `{event_name}_{model_name}` to avoid ambiguity in multi-model agent workflows.
|
|
276
|
+
|
|
277
|
+
The agent's system prompt includes:
|
|
278
|
+
- The valid states for the model
|
|
279
|
+
- The terminal states
|
|
280
|
+
- The available events
|
|
281
|
+
- Explicit instructions to always call `available_events_for_*` before firing
|
|
282
|
+
- Instructions to accept `{ success: false }` responses without retrying
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## How to add a new FOSM app
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
rails generate fosm:app lead_capture \
|
|
290
|
+
--fields first_name:string last_name:string email:string company:string \
|
|
291
|
+
--states new,qualified,contacted,converted,lost \
|
|
292
|
+
--access authenticate_user!
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Then:
|
|
296
|
+
1. Edit `app/models/fosm/lead_capture.rb` — fill in the events
|
|
297
|
+
2. Edit `app/agents/fosm/lead_capture_agent.rb` — add custom tools if needed
|
|
298
|
+
3. Edit `app/views/fosm/lead_capture/` — customize the UI
|
|
299
|
+
4. `rails db:migrate`
|
|
300
|
+
5. Visit `/fosm/apps/lead_captures`
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## How to extend the admin UI
|
|
305
|
+
|
|
306
|
+
The admin views are plain ERB in `app/views/fosm/admin/`. They use basic HTML with Tailwind-compatible classes. To customize for a specific host app (e.g., to use the app's UI component library), override the views by creating matching paths in the host app:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
app/views/fosm/admin/dashboard/index.html.erb ← overrides the engine view
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Rails view inheritance means host app views take precedence over engine views.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Testing FOSM models
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
# test/models/fosm/invoice_test.rb
|
|
320
|
+
class Fosm::InvoiceTest < ActiveSupport::TestCase
|
|
321
|
+
setup do
|
|
322
|
+
@invoice = Fosm::Invoice.create!(name: "Test", amount: 100, state: "draft")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
test "draft invoice can be sent" do
|
|
326
|
+
assert @invoice.can_send_invoice?
|
|
327
|
+
@invoice.send_invoice!(actor: :test)
|
|
328
|
+
assert @invoice.sent?
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
test "cannot pay a draft invoice directly" do
|
|
332
|
+
assert_raises(Fosm::InvalidTransition) do
|
|
333
|
+
@invoice.pay!(actor: :test)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
test "paid invoice is terminal" do
|
|
338
|
+
@invoice.send_invoice!(actor: :test)
|
|
339
|
+
@invoice.pay!(actor: :test)
|
|
340
|
+
assert_raises(Fosm::TerminalState) do
|
|
341
|
+
@invoice.cancel!(actor: :test)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
test "every transition is logged" do
|
|
346
|
+
@invoice.send_invoice!(actor: :test)
|
|
347
|
+
log = Fosm::TransitionLog.for_record("Fosm::Invoice", @invoice.id).last
|
|
348
|
+
assert_equal "send_invoice", log.event_name
|
|
349
|
+
assert_equal "draft", log.from_state
|
|
350
|
+
assert_equal "sent", log.to_state
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
test "guard blocks sending empty invoice" do
|
|
354
|
+
empty = Fosm::Invoice.create!(name: "Empty", amount: 0, state: "draft")
|
|
355
|
+
assert_raises(Fosm::GuardFailed) do
|
|
356
|
+
empty.send_invoice!(actor: :test)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Contributing guidelines
|
|
365
|
+
|
|
366
|
+
1. **Read the design principles above** before writing any code
|
|
367
|
+
2. **No direct state mutations** — always go through `fire!`
|
|
368
|
+
3. **Keep the lifecycle DSL simple** — resist adding complexity (priorities, concurrent states, history states). FOSM is deliberately simple. If you need those features, look at XState or Statecharts.
|
|
369
|
+
4. **The admin UI is secondary** — the DSL and the transition log are the core. Admin views can be overridden by host apps.
|
|
370
|
+
5. **Test the lifecycle, not the persistence** — unit tests for FOSM models should test state machine behavior, not database queries
|
|
371
|
+
6. **Document new events in lifecycles** — use `guard` and `side_effect` names that are self-documenting
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Key references
|
|
376
|
+
|
|
377
|
+
- **FOSM paper**: [parolkar.com/fosm](https://parolkar.com/fosm)
|
|
378
|
+
- **FOSM book **: [fosm-book.inloop.studio](https://fosm-book.inloop.studio)
|
|
379
|
+
- **Gemlings** (AI agent framework): [github.com/khasinski/gemlings](https://github.com/khasinski/gemlings)
|
|
380
|
+
- **Rails Engine Guide**: [guides.rubyonrails.org/engines.html](https://guides.rubyonrails.org/engines.html)
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
*fosm-rails is an open-source implementation of ideas from Abhishek Parolkar's FOSM research. The goal is to make business software that is auditable, AI-safe, and self-documenting by design.*
|
data/LICENSE
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, Apache 2.0 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-Apache-2.0
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Abhishek Parolkar and INLOOP.STUDIO PTE LTD
|
|
10
|
+
|
|
11
|
+
## Change Date
|
|
12
|
+
|
|
13
|
+
Three years from the release date of each version of the Software and Content.
|
|
14
|
+
|
|
15
|
+
## Terms and Conditions
|
|
16
|
+
|
|
17
|
+
### Licensor ("We")
|
|
18
|
+
|
|
19
|
+
Abhishek Parolkar, INLOOP.STUDIO PTE LTD, and their affiliated entities, the party offering the Software and Content under these Terms and Conditions.
|
|
20
|
+
|
|
21
|
+
### The Software and Content
|
|
22
|
+
|
|
23
|
+
The "Software and Content" refers to each version of the software, documentation, approaches, strategies, methodologies, and all other materials that we make available under these Terms and Conditions, as indicated by our inclusion of these Terms and Conditions with the Software and Content.
|
|
24
|
+
|
|
25
|
+
### License Grant
|
|
26
|
+
|
|
27
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
28
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
29
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
30
|
+
and redistribute the Software and Content for any Permitted Purpose identified below.
|
|
31
|
+
|
|
32
|
+
### Permitted Purpose
|
|
33
|
+
|
|
34
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
35
|
+
means making the Software and Content available to others in a commercial product or
|
|
36
|
+
service that:
|
|
37
|
+
|
|
38
|
+
1. substitutes for the Software and Content;
|
|
39
|
+
|
|
40
|
+
2. substitutes for any other product or service we offer using the Software and Content
|
|
41
|
+
that exists as of the date we make the Software and Content available; or
|
|
42
|
+
|
|
43
|
+
3. offers the same or substantially similar functionality as the Software and Content,
|
|
44
|
+
including making the Software and Content available as a hosted or managed service
|
|
45
|
+
(e.g., software-as-a-service, platform-as-a-service, or any other on-demand service)
|
|
46
|
+
where third parties access the functionality of the Software and Content.
|
|
47
|
+
|
|
48
|
+
Permitted Purposes specifically include using the Software and Content:
|
|
49
|
+
|
|
50
|
+
1. for your internal use and access;
|
|
51
|
+
|
|
52
|
+
2. for non-commercial education;
|
|
53
|
+
|
|
54
|
+
3. for non-commercial research; and
|
|
55
|
+
|
|
56
|
+
4. in connection with professional services that you provide to a licensee
|
|
57
|
+
using the Software and Content in accordance with these Terms and Conditions.
|
|
58
|
+
|
|
59
|
+
### Patents
|
|
60
|
+
|
|
61
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
62
|
+
patents, the license grant above includes a license under our patents. If you
|
|
63
|
+
make a claim against any party that the Software and Content infringes or contributes to
|
|
64
|
+
the infringement of any patent, then your patent license to the Software and Content ends
|
|
65
|
+
immediately.
|
|
66
|
+
|
|
67
|
+
### Redistribution
|
|
68
|
+
|
|
69
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
70
|
+
the Software and Content.
|
|
71
|
+
|
|
72
|
+
If you redistribute any copies, modifications or derivatives of the Software and Content,
|
|
73
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
74
|
+
remove any copyright notices provided in or with the Software and Content.
|
|
75
|
+
|
|
76
|
+
### Disclaimer
|
|
77
|
+
|
|
78
|
+
THE SOFTWARE AND CONTENT IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
79
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
80
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
81
|
+
|
|
82
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
83
|
+
SOFTWARE AND CONTENT, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
84
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
85
|
+
|
|
86
|
+
### Trademarks and Domain Names
|
|
87
|
+
|
|
88
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
89
|
+
the Software and Content, you have no right under these Terms and Conditions to use our
|
|
90
|
+
trademarks, trade names, service marks or product names.
|
|
91
|
+
The name "FOSM-RAILS" is exclusive
|
|
92
|
+
property of Abhishek Parolkar with unlimited license to INLOOP.STUDIO PTE LTD. No right or license is granted
|
|
93
|
+
to use these names or domains in any manner whatsoever without the express written
|
|
94
|
+
permission of Abhishek Parolkar. Any unauthorized use of these names or domains is
|
|
95
|
+
strictly prohibited.
|
|
96
|
+
|
|
97
|
+
## Grant of Future License
|
|
98
|
+
|
|
99
|
+
We hereby irrevocably grant you an additional license to use the Software and Content under
|
|
100
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
101
|
+
the date we make the Software and Content available. On or after that date, you may use the
|
|
102
|
+
Software and Content under the Apache License, Version 2.0, in which case the following
|
|
103
|
+
will apply:
|
|
104
|
+
|
|
105
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
106
|
+
this file except in compliance with the License.
|
|
107
|
+
|
|
108
|
+
You may obtain a copy of the License at
|
|
109
|
+
|
|
110
|
+
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
|
111
|
+
|
|
112
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
113
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
114
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
115
|
+
specific language governing permissions and limitations under the License.
|