Last active
October 27, 2025 10:37
-
-
Save ciscoheat/f76cb9541c9c71022fa8b69aece8afb6 to your computer and use it in GitHub Desktop.
llms.txt for DCI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| applyTo: "**/*.ts" | |
| --- | |
| # Instructions for Writing Code with the DCI Paradigm | |
| > **DCI: Data, Context, Interaction** | |
| > DCI separates _what the system is_ (data) from _what the system does_ (behavior), bridging human mental models and code. | |
| > Follow these instructions when generating code in the DCI style. | |
| ## 1. Core DCI Architecture | |
| - DCI code is organized around three projections: | |
| ### Data ("What the system _is_") | |
| - Domain objects, with simple properties. | |
| - Pure data structures that represent the state of the system. | |
| - Classes or types that do NOT contain interaction logic relevant to the current use case. | |
| ### Context ("What the system _does_") | |
| - Encapsulates a _use case_ based on a mental model. | |
| - Orchestrates interactions between Data objects by assigning them **Roles** at runtime. | |
| - The (public) properties of these Data objects form the **Role Contracts**, a partial interface for accessing the role-playing object. | |
| - A Context encapsulates one complete use case or user story, with all variantions expressed in the Context. | |
| ### Interaction ("How the system does it") | |
| - Specifies _how_ objects collaborate inside a Context - via **RoleMethods**. | |
| - RoleMethods define the behavior of objects playing specific Roles. | |
| - IMPORTANT: Only the Role's own RoleMethods can access its Role Contract (the underlying object properties) directly. | |
| - Internal (private) RoleMethods are callable _only_ by RoleMethods in the same Role. | |
| - Interactions should favor "ask, don't tell" (objects request services, not micromanage). | |
| - The starting point for a Role interaction (a flow of messages through the Context) is called a **System Operation**. | |
| - In a true DCI runtime, RoleMethods are attached _dynamically_ to the objects playing the Roles, and only exist during Context execution, but this is language- and implementation-specific. | |
| ## 2. DCI Principles & Key Concepts | |
| ### Mental Model Alignment | |
| - DCI code should map closely to how users think about the domain. | |
| - The RoleMethods should express what the user wants the role-playing objects to do, based on their properties. | |
| ### Roles | |
| - Role = An identifier for an object in a Context; not a reusable type. | |
| - Objects can play a Role if they fulfill the Role's contract (literal type). | |
| - Roles are _not wrappers_; object identity MUST be preserved. | |
| ### Object Identity | |
| - Real objects, not proxies or wrappers, play Roles to maintain their identity. | |
| ### Separation of Concerns | |
| - Domain knowledge (Data) evolves slowly; use case logic (Context/Interaction) changes rapidly. | |
| - Keep these separate for maintainability. | |
| ### Readability | |
| - Gather use case logic in one place (the Context). | |
| - Use comments and TypeScript types to clarify contracts and intent. | |
| ### Runtime Focus | |
| - DCI describes system behavior _at runtime_, not just compile-time structure. | |
| ### Agile Support | |
| - DCI supports practices like iterative development, clear mental models, and adaptation to change. | |
| ### When not to use DCI | |
| - DCI is best suited for use cases where there are two or more interacting actors. | |
| - For simple operations like CRUD, or purely functional data transformation, do not use DCI. | |
| - If a use case tends to contain only one Role or be specific enough not to express a genericity of its Role interfaces, do not use DCI. | |
| ## 3. DCI Analogies | |
| ### Movie Script | |
| - The Context is the script; objects are actors; Roles are character parts. | |
| - Objects (actors) can play different Roles in different Contexts (scenes). | |
| ### Train System | |
| - Instead of modeling trains or stations individually, DCI models their patterns of interaction (e.g., station visits). | |
| ### Automated factory | |
| - When producing different products, the factory (Context) assigns machines (objects) to different Roles based on the product being made. | |
| - The product is passed between the machines (through Role Method arguments), each using its Role Contract to modify or use it, until the goal of the Context has been achieved. | |
| ### Extending the Context | |
| - Extending the Context (adding variations to the use case) should be like rewiring cables (RoleMethod calls), not changing domain objects. The simple data playing the Context Roles should be able to play a part in many different scenarios through their interfaces. | |
| ## 4. DCI Code Generation Workflow | |
| 1. **Start with the Use Case** | |
| - What does the user want to _do_? | |
| - Define this as a Context. | |
| - If a mental model or use case is supplied, use it as a foundation for the Roles and RoleMethods. | |
| 2. **Identify Roles** | |
| - What objects collaborate for this use case? | |
| - Roles should be played by OBJECTS, not primitive types. | |
| - Primitive types passed to the Context, like configuration options, are better expressed as a settings object, played by a `Context` role. | |
| - Additional Context state, usually transient, can also be added as properties to the `Context` role. | |
| - Name the Roles meaningfully (e.g., `SourceAccount`, `Messages`). | |
| 3. **Define Role Contracts** | |
| - What properties/methods must an object have for its Role? | |
| - Define clear contracts that specify the interface needed for each Role. | |
| - Use the language's type system to express these contracts explicitly. | |
| 4. **Implement RoleMethods** | |
| - Write interaction logic inside the Context. | |
| - Group RoleMethods by Role for clarity. | |
| - RoleMethods should be kept together. No mixing of RoleMethods or other instructions between RoleMethods belonging to the same role. | |
| - Most of the time, RoleMethods should "chain" together the System Operation in progress, meaning that at the end of a RoleMethod, another RoleMethod of a Role is called, passing on relevant data as arguments, further fulfilling the purpose of the Context according to the mental model. | |
| - This chaining makes it easy to "rewire" the context later if requirements change, or new functionality is added. | |
| 5. **Focus on Interaction** | |
| - RoleMethods should coordinate ("ask") with other Roles, not dictate ("tell"). | |
| - When data is acquired or created within a RoleMethod, for example through a Role Contract method call, if needed by other Roles it should be passed to other RoleMethods, expressing the interaction and collaboration of the Roles - true object-orientation. | |
| - Return values should be avoided if possible (think message-passing that ultimately modifies state), but is not prohibited, for example an occasional boolean check. Readability is the goal, not enforcing rules that complicates the code. | |
| 6. **Keep Data Pure** | |
| - Domain objects (classes/types) must NOT contain Context-specific logic. | |
| 7. **Preserve Object Identity** | |
| - Role wrappers can lead to subtle bugs with strict equality checks, so NEVER wrap objects for Role assignment - always use direct references. | |
| 8. **Role-binding** | |
| - All Roles _must_ be bound either during the Context initialization, or in a single `rebind` function that reassigns _all_ Roles. | |
| - If one or more Roles must change during the Context execution, prefer reinstantiating the Context again, or use the `rebind` function to avoid recursion for example. | |
| - Roles _can_ be bound to null, but is unusual and a good reason must exist for that. | |
| 9. **Nested Contexts** | |
| - If a RoleMethod's logic represents a _reusable_, _distinct_ use case, consider implementing it as a separate Context. | |
| - This keeps Contexts focused and manageable. | |
| - Calling such a Context within another is called "nesting" Contexts. | |
| - Follow the rules about when not to use DCI to determine whether to use nested Contexts. | |
| 10. **Documentation** | |
| - Document Contexts clearly with their purpose and use case. | |
| - Clarify Role Contracts with appropriate type annotations or comments. | |
| ## 5. TypeScript-Specific Implementation | |
| ### Context Implementation | |
| - A Context is a function annotated with `@DCI-context`. | |
| - Contexts and their RoleMethods can be async functions when needed. | |
| ### Role Contracts | |
| - Use literal TypeScript types as Role Contracts, so the code can be understood without deeper type knowledge. | |
| - Example: `const Form: { action: string } = event.target;` | |
| - If the types are well-known, like the JavaScript Web APIs, you can reference them directly (e.g., `Page`, `HTMLElement`). | |
| - Example: `const Page: Page = await Browser.newPage();` | |
| - If an object is passed to the Context function, a Role can be defined from it directly in the function parameter. | |
| ### RoleMethod Naming | |
| - In TypeScript, RoleMethods are functions within the Context scope, named `Role_method()`. | |
| - Example: `Speaker_proclaim()`, `World_note()` | |
| - Internal (private) RoleMethods, callable only by RoleMethods in the same Role, use a double underscore: `Role__method()`. | |
| ### Role Organization | |
| - Use `//#region Role /////` and `//#endregion` comments to group RoleMethods by Role. | |
| - This enables easy folding/unfolding of Roles in the editor (Ctrl+K Ctrl+8 to fold all). | |
| ### Type Annotations | |
| - Use `@DCI-context` JSDoc tag to mark Context functions. | |
| - Clarify Role Contracts with explicit TypeScript types inline. | |
| - Prefer inline literal types over separate type declarations for Role Contracts. | |
| ## 6. TypeScript DCI Examples | |
| ### AJAX form submit with dynamic error display | |
| - Notable as a "one off" operation, nothing is returned from the Context function. | |
| - Also demonstrates the basics of Context error handling by using a single try/catch around the System Operation part, to avoid errors leaking outside the RoleMethods. | |
| ```ts | |
| /** | |
| * Submit a form and show error messages from the response. | |
| * @DCI-context | |
| */ | |
| async function SubmitForm(e: SubmitEvent) { | |
| if (!(e.target instanceof HTMLFormElement)) throw new Error("No form found."); | |
| //#region Form Role //////////////////// | |
| const Form: { action: string } = e.target; | |
| async function Form_submit() { | |
| // Role contract: Form.action | |
| const response = await fetch(Form.action, { | |
| method: "POST", | |
| body: new FormData(Form as HTMLFormElement), | |
| }); | |
| const data = await response.json(); | |
| for (const error of data.errors ?? []) Messages_show(error); // Role interaction | |
| } | |
| //#endregion | |
| //#region Messages Role //////////////////// | |
| const Messages: Iterable<{ | |
| dataset: DOMStringMap; | |
| style: CSSStyleDeclaration; | |
| }> = e.target.querySelectorAll<HTMLElement>("[data-form-message]"); | |
| async function Messages_hide() { | |
| Messages__set("none"); | |
| await Form_submit(); // Role interaction | |
| } | |
| function Messages_show(name: string) { | |
| Messages__set("unset", name); | |
| } | |
| function Messages__set(display: string, name = "") { | |
| for (const msg of Messages) { | |
| if (name && msg.dataset.formMessage != name) continue; | |
| msg.style.display = display; | |
| } | |
| } | |
| //#endregion | |
| try { | |
| console.log("Submit"); | |
| e.preventDefault(); | |
| await Messages_hide(); // System operation | |
| console.log("Done"); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| } | |
| ``` | |
| ### Session validation for SvelteKit and Drizzle | |
| - Another "one off" operation, where ultimately the Request is modified to have valid or invalid session data. | |
| - The System Operation (initial RoleMethod call) is started right away, as all Roles are defined in the Context arguments. | |
| ```ts | |
| /** | |
| * Sets locals.user and locals.session on success, otherwise null. | |
| * @DCI-context | |
| */ | |
| export async function ValidateSession( | |
| Request: RequestEvent, | |
| Session = db, | |
| INVALIDATE = false | |
| ): Promise<void> { | |
| await Request_getTokenFromCookie(); | |
| //#region Request ////////////////////////////// | |
| async function Request_getTokenFromCookie() { | |
| const token = Request.cookies.get(COOKIE_NAME); | |
| if (!token) Request_clearSession(); | |
| else await Session_findByToken(token); | |
| } | |
| function Request_setSession(token: string, session: Session, user: User) { | |
| Object.freeze(user); | |
| Object.freeze(session); | |
| Request.cookies.set(COOKIE_NAME, token, { | |
| httpOnly: true, | |
| sameSite: "lax", | |
| expires: session.expiresAt, | |
| path: "/", | |
| }); | |
| Request.locals.user = user; | |
| Request.locals.session = session; | |
| } | |
| function Request_clearSession(): void { | |
| Request.cookies.set(COOKIE_NAME, "", { | |
| httpOnly: true, | |
| sameSite: "lax", | |
| maxAge: 0, | |
| path: "/", | |
| }); | |
| Request.locals.user = undefined; | |
| Request.locals.session = undefined; | |
| } | |
| //#region Session //////////////////////////////////////// | |
| async function Session_findByToken(token: string) { | |
| const sessionId = encodeHexLowerCase( | |
| sha256(new TextEncoder().encode(token)) | |
| ); | |
| const [result] = await Session.select({ | |
| user: userTable, | |
| session: sessionTable, | |
| }) | |
| .from(sessionTable) | |
| .innerJoin(userTable, eq(sessionTable.userId, userTable.id)) | |
| .where(eq(sessionTable.id, sessionId)) | |
| .limit(1); | |
| if (!result) Request_clearSession(); | |
| else await Session_checkExpiryDate(token, result.session, result.user); | |
| } | |
| async function Session_checkExpiryDate( | |
| token: string, | |
| session: Session, | |
| user: User | |
| ) { | |
| if (INVALIDATE || Date.now() >= session.expiresAt.getTime()) { | |
| await Session.delete(sessionTable).where(eq(sessionTable.id, session.id)); | |
| Request_clearSession(); | |
| } else { | |
| await Session_refreshExpiryDate(token, session, user); | |
| } | |
| } | |
| async function Session_refreshExpiryDate( | |
| token: string, | |
| session: Session, | |
| user: User | |
| ) { | |
| if ( | |
| Date.now() >= | |
| session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * (EXPIRY_DAYS / 2) | |
| ) { | |
| session.expiresAt = new Date( | |
| Date.now() + 1000 * 60 * 60 * 24 * EXPIRY_DAYS | |
| ); | |
| await db | |
| .update(sessionTable) | |
| .set({ | |
| expiresAt: session.expiresAt, | |
| }) | |
| .where(eq(sessionTable.id, session.id)); | |
| } | |
| Request_setSession(token, session, user); | |
| } | |
| } | |
| ``` | |
| ### A book borrowing machine at a public library | |
| - Notable as it returns an object from the Context, similar to a class with public methods. | |
| ```ts | |
| import { Display, type ScreenState } from "$lib/assets/screen/screenStates"; | |
| import { title } from "$lib/data/libraryItem"; | |
| import { cards, library, loans } from "$lib/library"; | |
| import { hash } from "$lib/utils"; | |
| import { BorrowItem } from "./borrowItem"; | |
| /** | |
| * @DCI-context | |
| * Ctrl+K Ctrl+8 folds all Roles. | |
| */ | |
| export function LibraryMachine( | |
| Screen: { | |
| display: (state: ScreenState) => void; | |
| currentState: () => ScreenState; | |
| }, | |
| Printer: { | |
| print: (line: string) => void; | |
| } | |
| ) { | |
| //#region Borrower ///// | |
| let Borrower: { | |
| "@id": string; | |
| "@type": "Person"; | |
| items: { id: string; title: string; expires: Date }[]; | |
| }; | |
| function Borrower_isLoggedIn() { | |
| // A getter is ok if it is descriptive beyond "get" and returns a boolean | |
| return !!Borrower["@id"]; | |
| } | |
| function Borrower_login(user: Pick<typeof Borrower, "@id" | "@type">) { | |
| rebind(user["@id"]); | |
| Screen_displayItems(Borrower.items); | |
| } | |
| /** | |
| * @param forced Whether the logout was forced by the user (e.g. card removed) | |
| */ | |
| function Borrower_logout(forced: boolean, printItems: boolean) { | |
| // Need to print before rebinding, as it will clear the items | |
| if (printItems) Printer_printReceipt(Borrower.items); | |
| if (Borrower_isLoggedIn()) rebind(undefined); | |
| Screen_displayThankYou(forced); | |
| } | |
| function Borrower_borrowItem(itemId: string | undefined) { | |
| // TODO: Built-in security (assertions) for required login | |
| if (!Borrower_isLoggedIn() || !itemId) return; | |
| if (Borrower.items.find((item) => item.id === itemId)) return; | |
| // Call nested context | |
| const loan = BorrowItem(library, Borrower, { "@id": itemId }, loans); | |
| // TODO: Error handling (logging) for expected errors | |
| if (loan instanceof Error) return Screen_displayError(loan); | |
| Borrower.items.push({ | |
| id: loan.object["@id"], | |
| title: title(loan.object), | |
| expires: loan.endTime, | |
| }); | |
| Screen_displayItems(Borrower.items); | |
| } | |
| //#endregion | |
| //#region CardReader ///// | |
| const CardReader: { currentId: string; attempts: number } = { | |
| currentId: "", | |
| attempts: 0, | |
| }; | |
| function CardReader_cardScanned(id: string | undefined) { | |
| if (CardReader.currentId == id) return; | |
| if (!id) { | |
| // Card removed or missing | |
| if (CardReader.currentId) Borrower_logout(true, false); | |
| } else { | |
| // Card scanned | |
| if (!Borrower_isLoggedIn()) { | |
| // New card | |
| Screen_displayEnterPIN(0); | |
| } | |
| } | |
| CardReader.currentId = id ?? ""; | |
| } | |
| function CardReader_resetAttempts() { | |
| CardReader.attempts = 0; | |
| } | |
| function CardReader_validatePIN(pin: string[]) { | |
| Library_validateCard(CardReader.currentId, pin); | |
| } | |
| function CardReader_PINfailed() { | |
| // TODO: Force remove card after 3 failed attempts | |
| Screen_displayEnterPIN(++CardReader.attempts); | |
| } | |
| //#endregion | |
| //#region Library ///// | |
| const Library = { | |
| cards, | |
| }; | |
| function Library_validateCard(cardId: string, pin: string[]) { | |
| const card = Library.cards.find((card) => card["@id"] === cardId); | |
| if (card && card.identifier === hash(pin.join(""))) { | |
| Borrower_login(card._owner); | |
| } else { | |
| CardReader_PINfailed(); | |
| } | |
| } | |
| //#endregion | |
| //#region Screen ///// | |
| function Screen_displayWelcome() { | |
| Screen.display({ display: Display.Welcome }); | |
| } | |
| function Screen_displayEnterPIN(attempts: number) { | |
| Screen.display({ display: Display.EnterPIN, attempts }); | |
| } | |
| function Screen_displayItems(items: { title: string; expires: Date }[]) { | |
| Screen.display({ display: Display.Items, items }); | |
| } | |
| function Screen_displayThankYou(forced: boolean) { | |
| if (forced && Screen.currentState().display === Display.ThankYou) { | |
| Screen_displayWelcome(); | |
| } else { | |
| Screen.display({ display: Display.ThankYou }); | |
| if (forced) Screen__displayNext({ display: Display.Welcome }); | |
| } | |
| } | |
| function Screen_displayError(error: Error) { | |
| // Log out user | |
| rebind(undefined); | |
| Screen.display({ display: Display.Error, error }); | |
| Screen__displayNext({ display: Display.Welcome }, 10000); | |
| } | |
| function Screen__displayNext(nextState: ScreenState, delay = 5000) { | |
| const currentState = Screen.currentState(); | |
| setTimeout(() => { | |
| if (currentState === Screen.currentState()) Screen.display(nextState); | |
| }, delay); | |
| } | |
| //#endregion | |
| //#region Printer ///// | |
| async function Printer_printReceipt( | |
| items: { title: string; expires: Date }[] | |
| ) { | |
| if (items.length) { | |
| await Printer__printLine(new Date().toISOString().slice(0, 10)); | |
| await Printer__printLine(""); | |
| for (const item of items) { | |
| await Printer__printLine(item.title); | |
| await Printer__printLine( | |
| "Return on " + item.expires.toISOString().slice(0, 10) | |
| ); | |
| await Printer__printLine(""); | |
| } | |
| } | |
| } | |
| async function Printer__printLine(line: string) { | |
| Printer.print(line); | |
| await new Promise((resolve) => setTimeout(resolve, 100)); | |
| } | |
| //#endregion | |
| /** | |
| * Reset the Context state, rebind to a new user or undefined (not logged in). | |
| */ | |
| function rebind(userId: string | undefined) { | |
| Borrower = { "@id": userId ?? "", "@type": "Person", items: [] }; | |
| CardReader_resetAttempts(); | |
| } | |
| { | |
| // Context start | |
| rebind(undefined); | |
| Screen_displayWelcome(); | |
| return { | |
| cardScanned(id: string | undefined) { | |
| CardReader_cardScanned(id); | |
| }, | |
| itemScanned(id: string | undefined) { | |
| Borrower_borrowItem(id); | |
| }, | |
| pinEntered(pin: string[]) { | |
| CardReader_validatePIN(pin); | |
| }, | |
| finish(printReceipt: boolean) { | |
| Borrower_logout(false, printReceipt); | |
| }, | |
| }; | |
| } | |
| } | |
| ``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment