Project code patterns and conventions. Auto-loads when implementing, designing, verifying, or reviewing code. Provides detailed pattern definitions with code examples.
Installation
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Project code patterns and conventions. Auto-loads when implementing, designing, verifying, or reviewing code. Provides detailed pattern definitions with code examples.
user-invocable
false
allowed-tools
Read, Glob, Grep
Project Patterns Reference
This skill contains detailed pattern documentation for this project.
See individual pattern files for full details with code examples.
Available patterns:
async-generator-event-stream.md — Every agent turn yields EngineEvent via async function*; persist and forward each event as it arrives — never buffer
engine-session-lifecycle.md — Engine.open(opts) → EngineSession; send(msg) reuses the live SDK conversation; close() in finally; seed with priorTurns only on engine swap/restart
sdk-event-mapping.md — Each adapter has a map*Event(sdkEvent, ctx) in its events.ts that returns EngineEvent | null; Codex returns EngineEvent[] (one SDK event can produce multiple)
tool-dispatch-pipeline.md — Model call → adapter maps to registry.dispatch(name, args) → Zod validation → handler(parsed.data, ToolContext) → ToolResult; all adapters share the same dispatch path
batch-tool-per-item-results.md — Tools that mutate N items in one step return { ok: AND(item.ok), results: ({ok:true, ...id} | {ok:false, ...id, reason})[] } and never abort on per-item failure; sequential await to preserve order; per-item reason is the model's correction signal
episodic-append-ordering.md — Within a turn: recordUserMessage → yield user_message → for await engine events → appendEpisodic immediately per event → yield event; write-failure is non-fatal
mode-tool-scoping.md — mode.toolNames filters ServiceDeps.toolDefinitions in EngineSessionManager.openActive (packages/core/src/services/session/engine-session-manager.ts); toolNames === [] means all tools (backward compat); always keep toolNames and prompt fragment in sync
service-deps-injection.md — ServiceDeps is the single DI container; engineFactory?: fn is the test injection seam for FakeEngine; toolServices: { sympy, sandbox } populated in buildServices
lazy-resolver-thunk.md — For deps that must be late-bound (user-tunable config, runtime swaps, acyclic ordering, narrow lookup-by-id), declare the dep as () => T or (id) => T | null and call it per-use; never capture the result at construction
load-or-throw.md — After .insert/update/delete().run(), call loadOrThrow(() => this.get(...), { entity, op, id, log }) to round-trip — never inline the if-null-throw; uniform error format "<entity> not found after <op>: <id>"
dynamic-where-predicate.md — For Drizzle queries with optional filters, seed a mutable eq[] (named conditions or predicates) with required clauses, .push() the optional ones inside if guards, finalize with .where(and(...arr)); filtering applies before LIMIT — never inline .where().where() chains
indexer-class.md — Background memory writers implement Indexer (id, schedule: "post-turn" | "session-end", async run(ctx)); registered as an array on IndexerOrchestratorImpl, which handles debounce + parallel fan-out + per-indexer error isolation + turnFloor advancement
mode-prompt-fragment-composition.md — A Mode is a list of PromptFragment objects ({ id, position, customizable, template }); composeSystemPrompt sorts mode + additional fragments by a fixed FRAGMENT_ORDER and applies overrides; non-customizable overrides throw — share fragments across modes, don't inline mode-specific content into shared ones
agent-prompt-sidecar.md — Each LLM agent (indexers, rubric/approach graders) ships its system prompt in a sibling <name>-prompt.ts file exporting one NAME_SYSTEM_PROMPT const; keeps the agent file focused on inference plumbing
service-facade-sibling-dir.md — Services > ~400 LoC split into a <name>-service.ts facade + sibling <name>/ directory of pure helpers, registries, prompt sidecars, and sub-services; barrel re-exports keep imports flat — graders, memory, course-create, indexers, session all follow this shape
builder-module-composition.md — 9 build-<domain>-services.ts modules each export an <Domain>Services interface + build<Domain>Services(deps) factory; orchestrator services.ts wires them in dependency order and threads outputs as inputs
ref-cell-bridge.md — When two services need a cyclic runtime dependency, the earlier builder declares a let xxxRef: T | undefined + setXxxRef(fn) setter, A's deps thunk over the ref, and the orchestrator closes the ref after the second service is constructed
kind-adapter-registry.md — Per-variant logic for a discriminated union exposed as buildXxxRegistry(): Record<Union["kind"], Adapter>; TS exhaustiveness forces every new union member to register an adapter; dispatch sites become registry[obj.kind].method(...)
row-to-domain-mapper.md — Per-service function rowToX(row: typeof tableName.$inferSelect): X colocated with the service; all read methods funnel rows through it so JSON-parsing, brand-id wrapping, and Date→Timestamp normalization live in one place
optimistic-dispatch.md — Every UI affordance triggering engine / IPC work uses useOptimisticAction for inline pip state + useActionEscalation for activity-strip fallback after 30 s; never re-implement the state machine inline
use-resource-hook.md — useResource(loader) returns { data, loading, error, refresh, setData }; loads on mount via useEffect; layer mutations on top using setData for optimistic updates; never inline the setLoading/try/catch/finally block
use-resource-aggregation-loader.md — Page-level surfaces with N independent reads pass a useCallback'd Promise.all loader to useResource; the aggregated object becomes data, with one shared loading/error/refresh/setData — never inline setLoading/try/finally per slice
context-hook-pair.md — createContext(null) + Provider + useX() that throws if outside Provider; used for PraxisClient (usePraxisClient) and auth status (useAuthStatus); guards surface missing-provider bugs immediately
activity-rail-producer.md — Long-running services inject ActivityRegistry via ServiceDeps.activity; producers call ctx.activity?.start({ label, metadata? }) → hold ActivityHandle → call handle.update(patch) / handle.finish("done"|"failed"); items surface in <StatusStrip> after their quietPeriodMs threshold (default 800ms for indexers). Never create a blocking modal for background work — use the strip instead.
modal-primitive.md — <Modal onClose={fn}> provides backdrop + ESC + click-outside + ARIA once; 5 modals wrap content inside it; never duplicate the escape handler or backdrop div
editorial-ui-primitives.md — RouteHeader, LibrarySection, EmptyState, LoadingState, ErrorMessage, COPY module, and composes: editorial from global; CSS utility form the editorial design system — use these primitives, never re-implement
tab-body-isolation.md — ALL open <ChatTabBody> instances mount simultaneously; inactive ones use display:none (not unmount) to preserve per-tab message logs and in-flight streams across tab switches
session-tab-open-flow.md — Opening a session always chains session.start → tabs.open (via useTabs().openTab) → navigate; use openSessionInTab helper — never call only session.start without opening a tab
resizable-side-panel-hook.md — Side panels with drag-to-resize + per-device persisted width compose useResizableWidth({ storageKey: "praxis.panel.<id>.width", defaultWidth, minWidth, maxWidth, side }) with <ResizeHandle side="..." {...handleProps} />; the hook owns pointer / keyboard / localStorage; one storage key per panel
hook-decomposition-setitems-callback.md — Complex hooks (useStreamedSend, useIngestion) split into independent sub-hooks each owning one state slice + imperative API; parent's setItems (or other setters) is passed in at call time, not captured at construction, to avoid stale-closure bugs
ipc-channel-convention.md — Channels follow praxis.{domain}.{action}; streaming splits into .start (invoke) / .events.<streamId> (push) / .cancel (signal); subscribe before invoking to avoid race
ipc-envelope-handler.md — Mutating / validating / trust-boundary IPC channels use wrapEnvelope(channel, log, withSchema(zod, fn)) so the wire format is { ok, value | error: { code, message, requestId } }; clients peel with unwrapEnvelope and catch IpcError
server-resolved-student-id.md — IPC handlers needing a studentId resolve it via getStudentId(services) (centralized in packages/desktop/electron/main/student-id.ts); the Zod schema declares no studentId field — never let the renderer pass it
per-domain-channel-module.md — Each cohesive IPC domain lives in its own <domain>-channel.ts exporting registerXxxHandlers(services, [, webContentsGetter, activeAbortControllers], log), builds { handle, on } = createIpcHelpers(log), and is wired into ipc-server.ts as a single call
discriminated-union-dispatch.md — Use type for streamed events (EngineEvent), kind for stored/transmitted domain objects (tool inputs, artifact variants); switch for exhaustive dispatch; z.discriminatedUnion("kind", [...]) for Zod schemas
subscriber-fanout-stream.md — For shared mutable main-process state surfaced to many renderers — service subscribe(listener) (sends snapshot first) → *-channel.ts fanout (built on streaming-ipc-channel-helpers) → client events() → UI hook iterating for await and folding event.kind into a Map → setState
streaming-ipc-channel-helpers.md — registerSubscriberStream (callback-subscribe) and registerGeneratorStream (AsyncIterable) factories in packages/desktop/electron/main/stream-handler.ts own the .start/.events.<id>/.cancel triplet — AbortController lifecycle, WebContents-alive push guard, {kind:"event"|"done"|"error"} envelope, redaction; derive channel names from a single channelBase
notify-listeners-helper.md — notifyListeners(listeners, event, log, component) in packages/core/src/services/db-helpers.ts is the shared listener-loop with per-listener try/catch and uniform ${component}.listener_threw warn; services keep their own Set<Listener> and snapshot/filter semantics, but the fanout step is the helper
ui-test-helper.md — makeFakeClient(overrides?) from __tests__/helpers/fake-client.ts is the single SOT for test PraxisClient stubs; wrap renders in <PraxisClientProvider>; mock @tanstack/react-router with async importOriginal form to preserve non-hook exports
temp-db-test-helper.md — useTempDb(opts?) from tests/helpers/db-setup.ts sets up per-test isolated SQLite + migrations; from per-package tests import via ../../../../tests/helpers/db-setup.js
slow-test-gating.md — Pyodide integration tests use describe.skipIf(!process.env.PRAXIS_RUN_SLOW_TESTS) with { timeout: 120_000 }; fast unit tests mock the runtime
shared-test-fake-factories.md — Port test doubles live as factory functions in tests/helpers/mocks.ts (noopLogger, noopLockService, inMemorySecretStorage, unavailableSecretStorage, noopDocumentScopes, recordingLogger); tests import these instead of inlining literal mocks — new ports added to ServiceDeps warrant a fake here when 3+ tests will need it
electron-ipc-test-harness.md — IPC channel tests stub electron at the module boundary so ipcMain.handle/on capture handlers into a local Map; registerIpcHandlers is imported after the mock (Vitest hoisting); tests invoke handlers.get("praxis.x.y")?.({}, ...args) directly with a minimal fake Services bag
ipc-envelope-test-triad.md — Each handleEnvelope-wrapped channel gets a per-describe block asserting four outcomes — ok:true for valid input, VALIDATION_FAILED for malformed input, INTERNAL (never rejects) for service throws, and no /home/.../*.db leakage in the INTERNAL message; copy the four cases per new channel