Skip to content

Functions ​

One function = one named concept ​

  • The name is a standard mathematical, geometric or business term: dot, cross, orientation, filletSetback, regularPolygonVertices, canConnect.
  • A high-level function reads like the formula: a sequence of calls to named functions. Reading the names alone tells "this serves that, which serves that".
  • Top-level functions are function declarations (func-style): hoisted, so a module reads top-down, from the formula to its steps (no-use-before-define allows functions).
  • Arrow functions are reserved for inline callbacks.

A function never modifies its arguments ​

It takes values and returns a result. Enforced by:

  • no-param-reassign (properties included) — everywhere;
  • functional/immutable-data, functional/prefer-immutable-types (ReadonlyDeep parameters), functional/type-declaration-immutability — in the core;
  • model types are readonly: the compiler rejects any mutation, even in the shell.

Exception by nature: a procedure receiving a DOM element may call its methods (append, setAttribute). See ADR-0014.

Explicit inputs (ADR-0016) ​

In src/: no default parameter, no optional parameter. Defaults belong to the model (a symbol's parameters declare them), not to function signatures. A behavior variant is a distinct named function, or an explicit field of a named parameter object.

Kind @kind (ADR-0011) ​

Every top-level function carries exactly one @kind. A function mixing two kinds must be split.

@kindDefinitionSide effects
mathpure numeric computation (scalars, vectors)no
geometryoperation with a geometric meaning on geometric objectsno
domainbusiness rule (symbol, port, parameter, pipe…)no
formatconversion between representations (model ↔ SVG, JSON)no
procedureaction with a side effect (DOM, state, events)yes

Why a JSDoc tag and not a decorator: a TypeScript decorator only applies to classes and their members, never to a free function (REF-TS-DECORATORS). The tag plays the same role, and ESLint reads it.

Limits per kind (local/kind-limits) ​

Values: eslint/settings/kinds.ts.

@kindCode linesStatementsComplexityDepth
math10431
geometry20852
domain20852
format20841
procedure401531
  • Lines: lines holding code (blank and comment lines excluded), signature included.
  • Complexity: McCabe, decision points + 1 (if, ?:, &&, ||, ??, case, loops, catch), callbacks included.
  • Every kind: 3 parameters at most (@typescript-eslint/max-params) — beyond, one named object.

Long procedures: Composed Method ​

A business procedure may be long (40 lines), never complicated (complexity 3, depth 1). It is written as a list of named steps at the same level of abstraction (REF-BECK-SBPP):

ts
/**
 * Places a pipe between two ports and records the command for undo.
 *
 * @kind procedure
 * ...
 */
export function addPipe(editor: Editor, from: PortReference, to: PortReference): void {
  const route = orthogonalRoute(editor.view, from, to);
  const pipe = pipeFromRoute(route);
  const command = addPipeCommand(pipe);

  executeCommand(editor, command);
  renderView(editor.canvas, editor.view);
}

orthogonalRoute, pipeFromRoute, addPipeCommand are pure domain functions; executeCommand and renderView are procedures. (Illustrative example: these functions do not exist yet.)

Every decision (choosing a route, validating) lives in a tested pure function; the procedure only chains. If a procedure exceeds its limits after two splits, stop and propose a split to the Product Owner (AGENTS.md, Stop and ask when). There is deliberately no annotation lifting the limit.

Forbidden ​

  • any, ! (non-null assertion), as (except as const): an exception requires // eslint-disable-next-line rule -- justification.
  • Classes, this, enum (use a union of literals), for…in, get/set accessors, dynamic import().
  • Magic numbers: only -1, 0, 1, 2 are allowed inline; everything else is a named, documented constant.