Building an application with ChatGPT
Can AI build a proper libmodulor application with as few instructions as possible ?
A simple prompt
The goal of this experiment is to be as concise as possible so the prompts are voluntarily very light.
Based on the docs here : https://libmodulor.c100k.eu. Create a UseCase for an insurance product, in order for the customer to fill in a car damage report.
After a few seconds it generated non-sense.
There were snippets of code, but they were completely wrong and unrelated to the way libmodulor
works.
Tweaking the prompt
This is wrong. Follow strictly all the pages of the documentation to understand how to build a UseCase, its input type, output type, etc.
Instead of replying straight away, it started "Thinking". And after having "Thought for 1m 24s", I have to admit that I was :obama_not_bad:
.
The thinking process
Thinking longer for a better answer
Browsing and analyzing documentation for strict UseCase formatting
https://libmodulor.c100k.eu/
I need to review the full documentation to understand how to build a UseCase. This means reading several guides, like "Create a UseCase," "Create a Data Type," and "Create a Policy." Since the user wants a UseCase for an insurance product related to car damage reports, I need to make sure it follows the proper
Navigating site documentation for UseCase creation
https://libmodulor.c100k.eu/docs/guides/create-use-case
Creating UseCase for insurance product and data types
https://libmodulor.c100k.eu/docs/guides/create-data-type
Reading policies and guidelines
https://libmodulor.c100k.eu/docs/guides/create-policy
Examining references for Data Types and Policies
https://libmodulor.c100k.eu/docs/references/data-types
Searching for relevant types like DateTime and Geolocation
https://libmodulor.c100k.eu/docs/references/data-types
Searching for DateTime and product structure details
https://libmodulor.c100k.eu/docs/guides/create-product
Creating app to view manifest and ucReg
https://libmodulor.c100k.eu/docs/guides/create-app
To create the UseCase, I'll need to define data types specific to car damage: CarPlate, Vehicle, DamageSeverity, Photos (File), Geolocation, DateISO8601 for date, and FreeTextLong for descriptions. I'll also need to consider outputs, such as AggregateOPI for persisted aggregates. I'll check the "Targets" reference to clarify the output structure for AggregateOPI0.
Opening reference for Targets and AggregateOPI details
https://libmodulor.c100k.eu/docs/references/targets
I think I might need to use AggregateOPI types for this. I'll look up details on AggregateOPI to better understand its structure and if it fits with what I need for this task. I'll check references or resources about it to move forward.
Searching for AggregateOPI0 definition and references
https://stackoverflow.com/questions/20167411/how-to-compile-a-kernel-module-for-raspberry-pi
https://www.opi.com/collections/nail-top-and-base-coats
https://github.com/allenzren/open-pi-zero
https://www.amazon.com/OPI-GELement-Quality-Manicure-Formula/dp/B0F8PP78T4
https://raspberrypi.stackexchange.com/questions/147637/kernel-in-use-does-not-match-headers-after-manual-update
https://nailmaxx.com/collections/opi-top-base?srsltid=AfmBOopnjvOQVJfT3NqT5pr5iNNmbwlvoWpdKbMpE0JAjCJGVUpmkvba
https://www.reddit.com/r/raspberry_pi/comments/1c7lzuw/streaming_video_with_raspberry_pi_zero_2_w_camera/
https://www.ulta.com/p/gelement-base-coat-pimprod2053134?sku=2641437
https://leanprover-community.github.io/mathlib_docs/algebra/module/pi.html
https://www.physicalintelligence.company/blog/openpi
https://www.youtube.com/watch?v=3PVckXTb3Ns
https://www.raspberrypi.com/documentation/pico-sdk/networking.html
https://www.beautycarechoices.com/opi/gelement-base-coat-1
https://problog.readthedocs.io/en/latest/prolog.html
https://www.beyondpolish.com/collections/opi-gelcolor?srsltid=AfmBOooQh6neoF7RDnwj3GyknKhbTwLPHs7St-re4COBpBIQsmOqYIAZ
https://forum.armbian.com/topic/52352-dkms-exec-format-error/
https://www.universalcompanies.com/products/opi-super-base-base-coat-0-5-fl-oz?srsltid=AfmBOorxdEz2GHALHQq_xf2598iCveQVlxgaRfuQmFSXIS6JtjljFnlG
The search for "AggregateOPI" returned unrelated results, probably because of the common term "OPI." From previously accessed documentation, it looks like I don’t need to go deeper with AggregateOPI. For the use case, it needs to conform to libmodulor’s UCInput with specific data types. I'll craft the UCD TS file, including imports and types such as TFile, TGeolocation, and TAddress. Additionally, I'll explore definitions of AggregateOPI0 to create the Output types like DamageReportOPI0. Time to search for "AggregateOPI0" on the site!
It seems like "BuyAssetOPI0 extends AggregateOPI0" was mentioned in the documentation, but maybe extending AggregateOPI0 isn't necessary if we use UCOPIBase or AggregateOPI0 directly from libmodulor. The user wants strict adherence to documentation, so I'll base the UseCase structure on examples. I'll create a full TypeScript UCD file, including interfaces for input/output and the io fields. For data types, things like LicensePlate might require custom creation, since there isn't an existing type for it. Keeping things simple, I could use FreeTextShort or String for this field.
The documentation suggests we prefer final data types for semantic meaning, so I should create new ones for "LicensePlate" and "DamageSeverity" (maybe as Enum). I'll define TLicensePlate as a TString with regex for typical plate patterns. I'll also need a policy class for contract validity checks. The user wants a UseCase for a car damage report—so I’ll craft the code files: TLicensePlate, the UC policy file (ContractHolderUCPolicy), and the full UCD file (CarDamageReportUCD.ts), following the documentation structure.
I need to properly cite the documentation pages that guide the UseCase structure. Key points to reference include defining the UCInputFieldValue types, declaring fields in UCD.io.i.fields, output in parts._0, and data type creation. I’ll also mention the policy structure and use of data types like TFile or TGeolocation. I’ll also include the usage guidelines, but I won't add UI elements unless absolutely needed. Citations will be integrated without overcrowding the response.
I need to focus on following the documentation strictly. I'll format the code to match the "libmodulor" structure, using UCInput and UCOPIBase types, as well as classes like TFile and TGeolocation. I’ll also check examples from the "Create a use case" guide for correct imports and interface styles.
I'll be careful not to ask any clarifying questions, as the user requested strict adherence. This means I'll provide a full set of files—a UCD file, DataType files, a Policy file, and a manifest snippet, complete with citations and mappings between data fields and types.
I need to ensure I'm citing key sections, like data type creation and policy structure, from the documentation. I'll follow the libmodulor patterns for naming and structure, like defining the UCD file, referencing data types like TFile and TGeolocation, and creating custom data types such as TDamageSeverity.
I’ll create the input fields in the UCD file using these types and decide how to handle any date-time-related fields, possibly using TDateISO8601 or FreeTextShort for timing. I'll review the online documentation to finalize these.
For datetime, I’ll stick with DateISO8601 for the date and use FreeTextShort for the time. A custom data type extending TString for full ISO datetime might also be more robust. I'll use existing data types like TFile for photos, TGeolocation for location, and TAddress for addresses. For arrays, I’ll need to check if constraints for cardinality are mentioned in the docs.
Designing custom data types for strict format adherence
https://libmodulor.c100k.eu/docs/guides/create-use-case
There isn't explicit cardinality in the example, but I’m thinking the "Sept. Tip" article might clarify it. For now, I'll assume that photos can be treated as an array. I'll need to find out how to declare arrays properly, and I want to check out the "cardinality" article to confirm this approach.
Searching for cardinality article and array declaration
https://libmodulor.c100k.eu/articles/sept-tip-cardinality-leads-to-all-directions
The article clarified cardinality usage, so I'll cite it for the field explanation. For the code, I plan to create three files: one for data types (e.g., TDateTimeISO and TLicensePlate), one for policy (IsContractHolderUCPolicy), and one for the UCD (SubmitCarDamageReportUCD). Data types will follow the pattern shown on the "Create a data type" page, using regex and extending TString.
For the TDamageSeverity data type, I'll extend TString with a format that includes enumerated values like minor, moderate, and severe. I'll follow the examples from the documentation by overriding the example() and tName() methods. For the policy, the IsContractHolderUCPolicy class will implement UCPolicy, verifying the user’s ID according to the documentation.
In the example, the docs use types directly, like ISIN, instead of class types. I'll follow this pattern by exporting a type alias for DateTimeISO = string and creating the class TDateTimeISO by extending TString<TDateTimeISO, 'DateTimeISO'>. For the photos field, I'll use TFile with valid mime types and possibly a cardinality range of 0–10. The array for other parties will be an optional array of PersonName.
I could use the EmbeddedObject data type for structured JSON like OtherParties. But it might be clearer to define my own type OtherParty with fields such as name and contact, making it more manageable. I'll use TEmbeddedObject for inputs and limit fields like contractId, incidentLocation, and description to keep things simple.
It looks like I need to define a series of components, like CarDamageReportOPI0, extending UCOPIBase, and include necessary fields like id, submittedAt, and status. I'll stick to the examples from the documentation, ensuring key fields, policies (IsContractHolderUCPolicy), and server handling are implemented with TypeScript definitions. I’ll also be mindful of imports and ensure everything is concise with necessary references to the documentation.
I’m building out definitions for the fields in the car damage report. For things like photos, these should be handled using UCInputFieldValue<File>, not arrays directly. The cardinality will handle how many, so I’ll mark photos optional with a minimum cardinality of 0. Everything else about the code looks good to keep clean with proper TS interface matching.
Done
You're right — my previous answer was too generic. I read the libmodulor docs and rebuilt the UseCase strictly following the library conventions (input types as UCInputFieldValue<...>, io declarations, cardinality rules, data-type best practices, policy shape, etc.).
Below I give you:
a short reasoning + the docs I used (so you can verify), and
1. ready-to-drop TypeScript snippets: data types, policy, UCD and a manifest snippet.
2. They follow the exact patterns shown in the libmodulor docs (IO mapping, client/server lifecycle, cardinality, etc.).
The Code
// src/apps/Insurance/src/lib/dataTypes/DateTimeAndPlate.ts
import { TString, TStringConstraints, TName } from 'libmodulor';
/* --- DateTime ISO (strict ISO 8601 datetime) --- */
export type DateTimeISO = string;
export class TDateTimeISO extends TString<DateTimeISO, 'DateTimeISO'> {
public static readonly FORMAT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
constructor(constraints?: TStringConstraints) {
super({
...constraints,
format: { f: 'DateTimeISO', regexp: TDateTimeISO.FORMAT },
});
}
public override tName(): TName {
return 'DateTimeISO';
}
public override example(): DateTimeISO {
return new Date().toISOString();
}
}
/* --- License plate (simple, adjust regex to your locale) --- */
export type LicensePlate = string;
export class TLicensePlate extends TString<LicensePlate, 'LicensePlate'> {
public static readonly FORMAT = /^[A-Z0-9\-\s]{2,12}$/i;
constructor(constraints?: TStringConstraints) {
super({
...constraints,
format: { f: 'LicensePlate', regexp: TLicensePlate.FORMAT },
});
}
public override tName(): TName {
return 'LicensePlate';
}
public override example(): LicensePlate {
return 'AB-123-CD';
}
}
/* --- Damage severity (enum-like) --- */
export type DamageSeverity = 'minor' | 'moderate' | 'severe';
export class TDamageSeverity extends TString<DamageSeverity, 'DamageSeverity'> {
constructor() {
// many TString implementations accept an "enumeration" or "format" constraint;
// adjust to your project's TStringConstraints shape if needed.
super({
enumeration: { allowed: ['minor', 'moderate', 'severe'] },
} as unknown as TStringConstraints);
}
public override tName(): TName {
return 'DamageSeverity';
}
public override example(): DamageSeverity {
return 'minor';
}
}
// src/apps/Insurance/src/lib/policies/IsContractHolderUCPolicy.ts
import { injectable, inject } from 'tsyringe'; // or your DI lib used by your project
import {
UCPolicy,
UCPolicyInput,
UCPolicyOutput,
defaultUCPolicyOutput,
UCInput,
UCOPIBase,
} from 'libmodulor';
/* ContractStore is an application service that knows contracts */
export interface ContractStore {
get(contractId: string): Promise<{ id: string; ownerId: string; status: string } | null>;
}
@injectable()
export class IsContractHolderUCPolicy<
I extends UCInput | undefined = undefined,
OPI0 extends UCOPIBase | undefined = undefined,
OPI1 extends UCOPIBase | undefined = undefined,
> implements UCPolicy<I, OPI0, OPI1> {
constructor(@inject('ContractStore') private contractStore: ContractStore) {}
// If you want the use case to appear in the UI even before auth, set true/false here.
public async canBeExecutedPreAuth(): Promise<boolean> {
return false;
}
public async exec({
uc,
}: UCPolicyInput<I, OPI0, OPI1>): Promise<UCPolicyOutput> {
const out = defaultUCPolicyOutput();
// contractId is passed in the input fields; follow your UCInput shape to read it
const contractId = (uc.input?.contractId?.value as string) ?? null;
if (!contractId) {
return out;
}
const contract = await this.contractStore.get(contractId);
if (!contract) {
return out;
}
// allow only if the current authenticated user owns the contract and it's active
out.allowed = contract.ownerId === uc.auth.user.id && contract.status === 'active';
return out;
}
}
// src/apps/Insurance/src/ucds/SubmitCarDamageReportUCD.ts
import { injectable, inject } from 'tsyringe';
import {
UCDef,
UCInput,
UCOPIBase,
UCInputFieldValue,
UCOPIVal,
UCMain,
UCMainInput,
UCOutput,
UCOutputBuilder,
UCOutputOrNothing,
UCManager,
UCTransporter,
UCExecMode,
// built-in data types
TExternalServiceId,
TFile,
TGeolocation,
TAddress,
TFreeTextLong,
TFreeTextShort,
} from 'libmodulor';
import { DateTimeISO, TDateTimeISO, LicensePlate, TLicensePlate, DamageSeverity, TDamageSeverity } from '../lib/dataTypes/DateTimeAndPlate';
import { IsContractHolderUCPolicy } from '../lib/policies/IsContractHolderUCPolicy';
import { Manifest } from '../manifest';
/* === INPUT interface ===
Must extend UCInput and use UCInputFieldValue<DataType> for each field.
(As described in the "Create a use case" guide.) */
export interface SubmitCarDamageReportInput extends UCInput {
contractId: UCInputFieldValue<ExternalServiceId>;
incidentDateTime: UCInputFieldValue<DateTimeISO>;
incidentLocation: UCInputFieldValue<Geolocation>;
incidentAddress?: UCInputFieldValue<Address>; // optional (cardinality min:0)
licensePlate?: UCInputFieldValue<LicensePlate>; // optional
otherPartyLicensePlate?: UCInputFieldValue<LicensePlate>; // optional
severity: UCInputFieldValue<DamageSeverity>;
description: UCInputFieldValue<FreeTextLong>;
photos?: UCInputFieldValue<File>; // cardinality is defined in the UCD.io section (0..N)
}
/* === OUTPUT (OPI0) ===
Must extend UCOPIBase (or a provided AggregateOPI0 if you prefer).
*/
export interface SubmitCarDamageReportOPI0 extends UCOPIBase {
id: UCOPIVal<ExternalServiceId>;
submittedAt: UCOPIVal<DateTimeISO>;
status: UCOPIVal<string>; // e.g. 'submitted'
}
/* === client main ===
simple client that forwards to the server target (pattern from docs) */
@injectable()
class SubmitCarDamageReportClientMain implements UCMain<SubmitCarDamageReportInput, SubmitCarDamageReportOPI0> {
constructor(@inject('UCTransporter') private ucTransporter: UCTransporter) {}
public async exec({
uc,
}: UCMainInput<SubmitCarDamageReportInput, SubmitCarDamageReportOPI0>): Promise<UCOutputOrNothing<SubmitCarDamageReportOPI0>> {
return this.ucTransporter.send(uc);
}
}
/* === server main ===
Typical server side: persist the UC (ucManager.persist) and return an OPI.
The example in the docs shows the same pattern. */
@injectable()
class SubmitCarDamageReportServerMain implements UCMain<SubmitCarDamageReportInput, SubmitCarDamageReportOPI0> {
constructor(@inject('UCManager') private ucManager: UCManager) {}
public async exec({
uc,
}: UCMainInput<SubmitCarDamageReportInput, SubmitCarDamageReportOPI0>): Promise<UCOutput<SubmitCarDamageReportOPI0>> {
// >=> Persist the report (pattern from docs: ucManager.persist(uc))
const { aggregateId } = await this.ucManager.persist(uc);
const submittedAt = new Date().toISOString();
const status: SubmitCarDamageReportOPI0['status'] = 'submitted';
return new UCOutputBuilder<SubmitCarDamageReportOPI0>()
.add({
id: aggregateId,
submittedAt,
status,
})
.get();
}
}
/* === The UCDef ===
- io.i.fields must mirror the Input interface (types are instances of data-type classes).
- use cardinality to declare optional/multi-valued fields (see cardinality docs).
*/
export const SubmitCarDamageReportUCD: UCDef<SubmitCarDamageReportInput, SubmitCarDamageReportOPI0> = {
io: {
i: {
fields: {
contractId: {
type: new TExternalServiceId(),
},
incidentDateTime: {
type: new TDateTimeISO(),
},
incidentLocation: {
type: new TGeolocation(),
},
incidentAddress: {
cardinality: { min: 0 }, // optional
type: new TAddress(),
},
licensePlate: {
cardinality: { min: 0 },
type: new TLicensePlate(),
},
otherPartyLicensePlate: {
cardinality: { min: 0 },
type: new TLicensePlate(),
},
severity: {
type: new TDamageSeverity(),
},
description: {
type: new TFreeTextLong(),
},
photos: {
cardinality: { min: 0, max: 10 }, // 0..10 photos allowed (see cardinality docs)
type: new TFile({
type: {
allowed: ['image/jpeg', 'image/png'],
},
}),
},
},
},
o: {
parts: {
_0: {
fields: {
id: {
type: new TExternalServiceId(),
},
submittedAt: {
type: new TDateTimeISO(),
},
status: {
type: new TFreeTextShort(),
},
},
},
},
},
},
lifecycle: {
client: {
main: SubmitCarDamageReportClientMain,
policy: IsContractHolderUCPolicy,
},
server: {
execMode: UCExecMode.USER, // optional: run as user
main: SubmitCarDamageReportServerMain,
policy: IsContractHolderUCPolicy,
},
},
metadata: Manifest.ucReg.SubmitCarDamageReport,
};
// src/apps/Insurance/src/manifest.ts
import type { AppManifest } from 'libmodulor';
export const Manifest: AppManifest = {
languageCodes: ['en'],
name: 'Insurance',
ucReg: {
SubmitCarDamageReport: {
action: 'Create',
icon: 'car',
name: 'SubmitCarDamageReport',
sensitive: false,
},
},
};
Some Takeaways
First, it's clear that examples in the documentation helped a lot to achieve a better result. So I guess, instead of long theoretical explanations, examples are better. Maybe it's the same for humans after all.
We can clearly see that it got a little bit lost with OPI
which actually stands for OutputPartItem
.
This abbreviation was maybe a bad choice. Or at least, I need to document it better.
But the interesting part is that it realized that it was getting lost (cf. the 17 links list) and finally found its way via an example.
Some of those links are very funny by the way.
Besides, it's funny how it kept repeating "I need to adhere to the docs", "I need to adhere to the docs" to itself. As if the position of an "instruction" within the context is important, so repeating it would put it back close to the "current position".
What about the code ?
It's actually not that bad, and even pretty good as it got the essence of what libmodulor
is. I haven't integrated it yet but I can see that it won't pass basic TypeScript transpiling though :
- There is no such thing as
enumeration
inTString
- It assumed
tsyringe
was used for DI (Dependency Injection) whilelibmodulor
usesinversify
- It considered input fields as potentially undefined
incidentAddress?:
which is fine, but it's already handled inUCInputFieldValue
so it's redundant - It used
new Date()
inSubmitCarDamageReportServerMain
=> BOOOH !
That being said, it needs only small fixes to work.
And the most interesting part, is that once it transpiles, it can benefit from everything provided by libmodulor
: auto documentation, auto testing, etc.
And most importantly, I'd just have to expose the use case on one of the built-in targets (server, cli, web, mobile, mcp, etc.) and it will just work.
Provided we also include an implementation of the ContractStore
it rightfully introduced.
All in all, that was a fun experiment. I'm pretty sure that with a slight improvement in the documentation, it can figure everything out by itself.
Maybe the specialized AIs (Cursor, Claude Code, Devin, Codex, etc.) would be better at this ?