Project code patterns and conventions. Auto-loads when implementing, designing, verifying, or reviewing code. Provides detailed pattern definitions with code examples.
Installation
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
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