Dédalo code documentation standard
This document defines how to write doc-comments and inline comments in Dédalo's own
TypeScript server code (src/, tools/*/server/, diffusion/api/v1/) and the
JavaScript client (client/dedalo/). It codifies the house style that already exists in the
best-documented files so that documentation is uniform, accurate, and useful to developers.
Scope: Dédalo source only. Do not apply this to third-party libraries — neither the installed dependencies under
node_modules/nor the committed ones undervendor/.
Guiding principles
- Explain why, not just what. The signature already says what. A good doc-block adds intent, contracts, edge cases, and the consequences of getting it wrong.
- Documentation must match the code. Every
@param/@return/@vartype must mirror the real signature. A stale or wrong doc-block is worse than none. - Doc-only edits. Improving documentation never changes behavior. You only add or rewrite comments and doc-blocks — never code tokens, never reordering, never "while I'm here" fixes.
- Signal over noise. Comment the non-obvious. Do not narrate self-evident lines.
- English only. Replace any stray Spanish comments with clear English. (Identifiers and ontology terms stay as they are — only comments are translated.)
- Never delete code. If you find dead/commented-out code, leave it and flag it in the round report. Removal is a separate, explicit decision.
TypeScript
The strict compiler (tsconfig.json) already documents what a symbol is — the types are in the
signature. A TS doc-comment therefore focuses almost entirely on why: intent, contract, edge
cases, and the security/invariant note. Do not restate the type in a @param tag — the type
annotation is the source of truth, and a @param that drifts from it is exactly the stale-doc
failure principle 2 forbids. Formatting is enforced by Biome (biome.json); do not hand-format
around it.
Module header
Every non-trivial module opens with a /** … */ block:
- One-line summary naming the module and what it is for.
- Blank line, then the why / responsibilities — prose and/or bullet lists.
- Where it matters, cite the governing spec section (
spec §4) and any fault-code the behavior defends (SEC-016,AUTH-05) — these tie the code to the audit register. If the module upholds a tripwired invariant, say which one, so the next reader knows the gate exists before they try to "simplify" the code that satisfies it.
A short, declarative module says what it stores and how it behaves:
/**
* component_input_text — short single-line free text. Stores {id,value,lang}
* items in the `string` matrix column; CLASS-translatable (its data items are
* lang-filtered on read).
*/
import type { ComponentModel } from '../types.ts';
A longer engine module states the invariant it upholds and how it relates to its callers. The
WHY THIS EXISTS block is the house convention for a module whose shape is non-obvious:
/**
* Request-scoped effective languages (the current interface + data language).
*
* WHY THIS EXISTS (spec §4): Bun is a long-lived process, so the "current
* language" can NOT be a module-level value — that would bleed one user's
* language choice into every concurrent request. Instead the effective languages
* live in an AsyncLocalStorage scope opened once per RQO at the dispatch
* chokepoint (dispatchRqo) from the caller's session. Leaf resolvers read them
* through the accessors below; outside any scope (unit tests calling resolvers
* directly, background jobs) they fall back to the installation defaults. …
*/
Interface / type doc-comment
- Short description of what the shape represents and when it is populated.
- A trailing
// commenton individual fields for the non-obvious ones — meaning, provenance (which matrix column or wire key it maps to), and any gotcha.
/** Per-request state container. Passed explicitly — never stored globally. */
export interface RequestContext {
/** Unique id for tracing/log correlation. */
readonly requestId: string;
/** Wall-clock start, for latency metrics. */
readonly startedAt: number;
}
Function doc-comment
- What & why prose (one or more lines). State contracts, side effects and fail-closed behavior.
- The parameter and return types live in the signature — describe only what the types cannot say (units, invariants, "null when the locator cannot be resolved", security gates).
- Note when the function throws and under what condition, in prose.
/**
* Route a request. Kept as a plain function (not inline in Bun.serve) so tests
* can call it directly without a socket.
*/
export async function handleRequest(request: Request, context: RequestContext): Promise<Response> {
Constants
Document non-obvious constants with a one-line comment or a short block explaining the value's
meaning and any security/contract significance (e.g. an API_ACTIONS allowlist, a SESSION_COOKIE
name, a KEY_REGEX grammar, config DEDALO_* key mappings).
@param/@returns JSDoc tags — sparingly
A handful of TS files use JSDoc @param {Type} / @returns tags where a public helper benefits
from an IDE hover. This is allowed but not required, and when used the braced type must match
the TS signature exactly. Prefer prose-plus-signature over redundant tags.
JavaScript (the client)
Dédalo JS is browser-side ES modules (no framework). Two type-annotation styles existed in the
codebase; the standard is modern JSDoc with brace types. Migrate legacy @param type name
to @param {Type} name as you touch files.
File header
Preserve and normalize, in this order:
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
/*global SHOW_DEBUG, page_globals */
/*eslint no-undef: "error"*/
// import
import {event_manager} from '../../common/js/event_manager.js'
import {data_manager} from '../../common/js/data_manager.js'
- Line 1 is the mandatory AGPL license magnet line — never remove it.
/*global …*/lists module-scope globals (only if any are used).- For files larger than ~200 lines, add a module header doc-block describing the module's purpose and main exports:
/**
* DATA_MANAGER
* Central client→server request layer: builds API calls, applies retry/timeout,
* and routes responses through the response cache.
*/
Function doc-block
- First line:
FUNCTION_NAME(UPPERCASE) — the established client convention, kept for grep-ability. - What & why prose.
@param {Type} name - description— braces around the type; hyphen before the description.@returns {Type} description— use@returns(not@return).@throws {Error} …when applicable;@examplefor non-obvious call patterns.- Common brace types:
{Object},{string},{boolean},{number},{Array},{Function},{Promise<Object>},{HTMLElement},{Map},{Set},{*}(any).
/**
* IS_FILTER_EMPTY
* Check whether a normalized filter object contains no active query clauses.
* Recurses through nested $and / $or operator groups.
* @param {Object} filter_obj - normalized filter tree ($and/$or → array of clauses)
* @returns {boolean} true when every leaf `q` clause is empty
*/
export const is_filter_empty = function(filter_obj) {
Prototype-assignment modules (search.js, section.js, page.js, …)
- The constructor function gets a minimal header:
/**
* SEARCH
* Search instance: builds and runs the SQO for a section's search UI.
*/
export const search = function() { … }
- The block of
X.prototype.method = …assignments gets one section header doc-block, and the existing inline group labels (// lifecycle,// render, …) are kept:
/**
* COMMON FUNCTIONS
* Extend instance with shared prototype methods from common / render modules.
*/
// prototypes assign
// lifecycle
search.prototype.destroy = common.prototype.destroy
- Individual
prototype.x = …lines are not doc-blocked — the documentation lives at the source method's definition.
Object-literal / namespace modules (ui.js, data_manager.js)
Each method property carries its own function doc-block (same format as above), not just inline comments.
ES6 class modules (event_manager.js)
Use standard JSDoc on the class and each method (@param {Type} name - desc, @returns,
@throws, @example). Keep the narrative description.
Inline comments (TS & JS)
Use the existing // label block style: a label line introduces a logical block, with the
explanation indented beneath it. Comment the non-obvious — why a check exists, what an edge
case protects against — not the obvious.
// Get only the component data. Remove possible dataframe data.
// (TM rows store main + dataframe merged; dataframe entries are
// identified positively via dataframeEntryMatches: type marker first,
// legacy pairing-keys shape as fallback.)
if (isLiteralWithDataframe) {
…
}
- Prefix genuinely critical warnings with
(!):
// (!) For components, always use common.init() — do not call init() directly.
- Same-line trailing comments are fine for short clarifications of a variable's meaning:
self.tipo = options.tipo // structure tipo of the component, e.g. 'dd345'
Anti-patterns to avoid
- Restating the type a TS annotation already gives (a redundant or, worse, drifted
@param). - Copy-pasted doc-comments whose name/params no longer match the symbol.
- Doc-comments attached to commented-out (dead) code.
- Mixed Spanish/English comments.
- Restating the obvious line by line.
- Removing the AGPL header from a client JS file, or changing any code while "documenting".
Verification checklist (per file)
- [ ] Doc-only: stripping comments from before/after yields byte-identical code.
- [ ] Any
@param/@returns(JS) or optional TS JSDoc tag matches the real signature/type. - [ ] Client JS keeps brace types,
@returns, and the AGPL header. - [ ]
tscpasses andbun run lint(Biome) is clean (TS); JS lints clean. - [ ] No new dead code, no Spanish comments.