Documentation
Engineering conventions
Patterns used for domain computation, schema validation, Effect services, database access, and verification.
Source structure
Use English identifiers, comments, documentation, and commit messages. TypeScript projects use strict checking; public interfaces should preserve explicit types and decode external values instead of propagating unchecked data. Package manifests expose source entry points, and application modules use relative imports appropriate to their build tool.
Desktop features group screens, components, stores, and helpers under apps/desktop/src/features. Route adapters remain in src/pages, while the application shell and client setup live in src/app. Core geometry stays independent from UI state and database access.
Geometry and output units
Core vector and analytic entity geometry uses millimetres and radians. Scene placement, feature controls, and CAM rotary-axis fields have contract-specific angle conventions, including degrees. Follow each schema when adapting between them. UI display conversion belongs at the display boundary. Machining operations additionally use feed, spindle, depth, and clearance parameters with meanings defined by their schemas; a bare number should never acquire an implicit unit from a label alone.
G-code posts own controller-specific output conversion and number formatting. The shared emitter exposes formatNumber, rounds to the requested decimal count, normalizes negative zero, and tracks emitted position. Feed formatting may omit a fractional part for integral values. Use the emitter rather than independently formatting motion words in each caller.
Keep model evidence and approximate display geometry distinct. A tessellated surface is useful for rendering and analysis but does not itself prove an exact B-rep operation succeeded.
Effect services
Services extend Context.Service and expose typed Effect-returning methods. A layer constructs the implementation and supplies its dependencies. apps/sidecar/src/layers.ts is the integration point for repositories, services, handlers, authorization, and runtime configuration.
Resources with a lifetime, such as SQLite handles, are acquired and released inside the owning layer's scope. Use Effect.try or Effect.tryPromise to translate foreign exceptions into meaningful errors. Stable operation names in errors and tracing make failures diagnosable without coupling callers to a storage driver.
Layer composition matters: sibling layers merged together do not automatically provide services to each other. Sidecar workflow step registration is layered after the runner it needs; the AI step is registered where the AI service is available.
Validation and errors
HTTP inputs and outputs are declared in packages/api/src/groups. Validate unknown input at those boundaries. The contract includes plain message errors, success/error envelopes, and some keyed error bodies; preserve the endpoint's declared status and response representation.
HTTP-facing errors use Effect schemas and status annotations. For example, BadRequest, Unauthorized, NotFound, and Conflict are declared in packages/api/src/errors.ts. Internal database failures carry an operation and cause in DatabaseError. Map internal failures at the HTTP boundary rather than expose driver details to clients.
Regenerate clients after a contract change with bun run generate. Check committed output with bun run generate:check. Generated folders are outputs; generation explicitly excludes endpoints that a target cannot represent.
Persistence and configuration
Application repositories use the Database service from @cadcamfun/db and its query(operation, run) helper. Change packages/db/src/schema.ts before generating migrations. Review generated SQL and metadata together; do not run a destructive schema experiment against a user's database.
The sidecar reads configuration in apps/sidecar/src/config.ts and passes it through AppConfigService. Credentials use redacted values where the service requires them. Keep token and provider secret values out of logs, documentation, and troubleshooting output. The native shell supplies its own launch token and storage paths.
The documentation Studio is a separate local tool with its own path selection in packages/web/script/studio-server.ts. Do not assume its database path or login session matches the running desktop.
Verification
bun test packages/core
bun run --filter @cadcamfun/core typecheck
bun run generate:check
# Documentation content and build
bun test packages/web/test
bun run --filter @cadcamfun/web typecheck
bun run --filter @cadcamfun/web build Choose checks that exercise the affected boundary. Pure kernel changes need numerical and geometric regression cases. HTTP changes need handler and schema coverage. Documentation changes need valid routes, anchors, examples, and a successful site build. A full workspace test run is bun run test, which includes packages, apps, and scripts.
Optional fields, nulls, and transport values
An optional field may be omitted. A nullable field may explicitly contain null. They are separate properties: the project contract requires description in returned rows while allowing null, and a create request permits omission of that field. Preserve those distinctions when building clients or patching saved documents.
Transport validation is also distinct from business validation. The project-create payload permits an omitted name at the schema level, but the handler returns 400 with Project name is required if it is missing or empty. A generated type alone cannot establish that an operation will be admitted for an existing resource or current account.
HTTP query parameters are encoded text. Use the client's declared query fields or URL encoding rather than concatenate arbitrary user input into a URL. Encode a key or project identifier as a single path segment, and send bearer tokens in headers rather than in copied links.
The projects API reference shows these fields and status codes; the captured walkthrough demonstrates the handler's validation result.
Maintain executable documentation
Write an example against the exact public surface it names. The Promise SDK and the full contract-based Effect client have different method/input shapes and coverage. A settings-driven generator request is different from a native kernel request, and a generated program is different from a saved toolpath record.
The API reference snapshot is generated from the application contract. After changing an endpoint, regenerate it inside the web workspace with bun run generate:api-reference. Run bun run check:api-reference to compare it with current source. Resource grouping, article text, navigation, and assistant retrieval must still describe that contract faithfully.
When documenting a response captured from an isolated run, identify any normalized IDs, timestamps, or omitted fields. Label request templates as templates. Do not present an invented identifier or an unexecuted schema example as an observed production response.
Effect patterns used throughout
import { Context, Data, Effect, Layer } from "effect"
import { AppConfigService } from "../config.ts"
export class StorageError extends Data.TaggedError("StorageError")<{
readonly operation: string
readonly cause: unknown
}> {}
export class Storage extends Context.Service<Storage, {
readonly read: (key: string) => Effect.Effect<Uint8Array, StorageError>
}>()("cadcamfun/Storage") {
static readonly layer = Layer.effect(Storage, Effect.gen(function* () {
const config = yield* AppConfigService // dependencies come from the context
return Storage.of({
read: (key) => Effect.tryPromise({
try: () => Bun.file(`${config.dataDir}/storage/${key}`).bytes(),
catch: (cause) => new StorageError({ operation: "read", cause }),
}),
})
}))
} - Service keys are namespaced:
"cadcamfun/Database","cadcamfun/AppConfig","cadcamfun/ApiClient". - Errors are
Data.TaggedErrorinside services andSchema.TaggedErrorwith anhttpApiStatusat the HTTP boundary. Handlers map the first to the second. - Resources with a lifetime use
Effect.acquireReleaseso they close when the layer's scope closes. - Named functions use
Effect.fn("area.name")so traces carry a readable span name, for examplecam.runKernelRequest. - Configuration is read once, in
AppConfigService; no other module readsprocess.envin the sidecar.
Naming and layout rules
| Thing | Convention | Example |
|---|---|---|
| Files | kebab-case, one concept per file, test next to it | cam-program.ts, cam-program.test.ts |
| Operation IDs | <group>.<method> | projects.create, health.check |
| Query labels | dotted, resource first | projects.list, toolpaths.update |
| Units | millimetres, degrees, seconds; mm/min for feed, m/min for Vc | feed: 5696 |
| Coordinates | Z up, right-handed; program moves in work coordinates | origin at stock-center top |
| Ids | UUID strings | crypto.randomUUID() |