Skip to content

Engineering Wiki Commenting Guide

Ed Mozley edited this page Oct 13, 2026 · 1 revision

πŸ“ Commenting for the Engineering wiki - Developer Guide

How to write comments that the Engineering wiki scanner can read, so every file, function, parameter, table and API endpoint in FreeITSM gets a page that explains it. The scanner has no AI: it is a script that reads comments of a fixed shape. Write to that shape and a comment does two jobs at once - it reads naturally in VS Code or Notepad++, and it becomes documentation under System -> Engineering wiki.

The same conventions live inside the app as the Handbook page Writing comments for the wiki (engineering-wiki/handbook/comments.md). This page is the fuller version, with the reasons and the mistakes that trip the scanner up.

Nothing here is compulsory. A function with no comment still gets a page - its name, its parameters, where it is defined and who calls it. The comments add the meaning. But the goal, met in 3.6.0, is that every file, function and parameter has one: the scanner reports anything missing, and a blank "What it does" cell is a gap.


1. 🧭 What turns into what

You write The wiki shows
The first /** ... */ block in a file The file's summary (first sentence) and description (the rest)
A /** ... */ block directly above a function or class The function's summary in every list, and its own page
@param lines The Parameters table - one row per parameter
@return Returns: under the signature
@reads / @writes Which tables the function touches, and why
@input / @output (API endpoints) The endpoint's request and response
@wiki Page-Name A Developer guide link box at the top of the page
@see path A link to another file's page
// SQL: ... above a query That query's purpose, beside it in the Database table
A comment containing TRAP: A warning on the page, and a line on the all-traps page
// ... on the same line as a parameter That parameter's meaning

2. πŸ“„ The file comment

The first /** ... */ block in the file, straight after <?php, describes the whole file. Its first sentence is the summary shown in every list, so make it say what the file is:

<?php
/**
 * Leases (3.5.0) - Assets -> Leases.
 *
 * A LEASE is the agreement (lessor, agreement number, cost centre, optional
 * contract, notice period). It has SCHEDULES - a group of kit with its own dates
 * and cost, because one agreement can cover 10 PCs from one date and 10 monitors
 * from another. Each schedule has ASSETS, and each asset on a schedule has an
 * OUTCOME (Assets -> Settings -> Leasing).
 *
 * @wiki Leases
 */

Good first sentences: "API: save a ticket's category.", "The Kanban board for Tasks - drag, drop and inline edit.", "Snoozing a ticket - 'I can't do anything with this until Thursday.'"

For an API endpoint, add the conversation:

<?php
/**
 * API: one Engineering wiki page, rendered.
 * GET ?page=handbook/start
 * Returns: { success, page, title, html, toc, modified, section, generated }
 *
 * @input  page  the page path from the wiki root, without ".md"
 * @output the page rendered to HTML (raw HTML in the file is always escaped)
 */

The scanner works out the rest of an endpoint by itself: its URL, the check that protects it (administrators only, a module, a capability - followed through any shared bootstrap it includes), the $_GET/$_POST fields it reads, the JavaScript that calls it, and the tables behind it.

One file comment, then a gap. The first doc block is always the file's. If a function follows it directly with no comment of its own, the same text is reused as that function's description. Give the function its own block to stop that.


3. πŸ”§ The function comment

A /** ... */ block directly above the function - above any modifiers, with nothing but whitespace between. The first sentence is the summary; the paragraphs after it are the explanation; the tags go last.

/**
 * Change one asset on a schedule: outcome, its own dates, notes.
 *
 * Moving it to a CLOSED outcome stamps today as the outcome date if none is given.
 * An asset can only go back on lease if it is not out on another one meanwhile.
 *
 * @param int   $id  the lease_assets row to change
 * @param array $in  the fields to change: outcome_id, outcome_date, start_date, end_date, notes
 * @writes lease_assets(outcome_id, start_date, end_date, outcome_date, notes)
 */
function leaseAssetUpdate(PDO $conn, ActorContext $ctx, int $id, array $in): void

Rules of thumb:

  • Start with a verb or a noun phrase - "Returns...", "Saves...", "The..." - never "This function".
  • Say what it does and why, if the why is not obvious. One sentence is plenty for a small function.
  • Mention side effects worth knowing: it writes table X, sends an email, fires a workflow event, queues a webhook.
  • Write from the code, not the name. A function called getUser that also creates the user when missing should say so.
  • British English, plain hyphens, no "we".

Tags, one per line

Tag Shape Example
@param @param type $name meaning @param ?int $tenantId the company; null means the Default company
@return @return type meaning @return ?array the active lease row, or null
@reads @reads table(cols) - why @reads lease_assets(outcome_id) - which machines are still out
@writes @writes table(cols) - why @writes assets(lease_start, lease_expiry) - kept in step with the lease
@see a repo path @see includes/leases.php
@wiki a GitHub wiki page name or URL @wiki Projects-Internals-4-Plan-and-Timeline

A tag that runs onto a second line is fine - indent the continuation:

 * @param array $check  the check definition from the card file:
 *                      array{type:string, table?:string, where?:string}

Parameters: say what the value IS

 * @param string $kind     'task' | 'ticket' - what the reminder is about
 * @param ?string $since   a UTC 'Y-m-d H:i:s' cut-off; null = everything
 * @param array $row       the ticket row as loaded by TicketsService::loadTicket()
 * @param bool $dryRun     true = work out the changes and report them, write nothing

Units, allowed values, what null or '' means, and the shape of an array are what the next developer needs. A parameter whose name already says it all still gets a line - "the ticket's id" is fine.

A comment on the parameter's own line also counts - useful in long constructors:

public function __construct(
    public int $actorId,
    public ?array $companyScope = null,   // null = all companies, else the allowed tenant ids
    public string $source = 'api',        // 'ui' | 'api'
) {}

Standard parameters - no need to repeat them

A few names mean the same thing everywhere, so they are explained once on the Handbook page Standard parameters and the wiki shows that meaning, marked standard, wherever a function says nothing about them:

Parameter Meaning
PDO $conn The database connection.
int $analystId The analyst doing this - normally the one signed in.
ActorContext $ctx Who is acting, which companies they may reach, ui or api, their language.
?int $tenantId A company id; null = the Default company.
int $projectId / $ticketId / $taskId / $assetId That record's id.
array $apiKey / $params / $body (REST API, api/v1/) The authenticating key; the ids from the URL path; the decoded JSON body.

If a function uses one of these names for something else - say so with its own @param line. An optional ?array $apiKey where null skips the company check, or an $analystId that is the person a task is assigned to rather than the one acting, must be described, or the wiki will show the standard meaning and mislead.


4. πŸ—„οΈ SQL

The scanner finds every SQL statement in a string and records which tables it reads and which it writes, with the columns an INSERT or UPDATE sets. Those land on the table's own page (Written by / Read by) and on the file's Database table. What it cannot know is the purpose. Give it one with a // SQL: comment just above the query, or at the end of the line that holds it:

// SQL: every lease row for this asset that is still out on lease
$s = $conn->prepare(
    "SELECT la.id FROM lease_assets la
       LEFT JOIN lease_outcomes lo ON lo.id = la.outcome_id
      WHERE la.asset_id = ? AND (la.outcome_id IS NULL OR lo.kind = 'active')"
);

The Read by list on lease_assets and lease_outcomes then shows that line, its function and the purpose:

File In Purpose
includes/leases.php:<line> leaseAssetUpdate every lease row for this asset that is still out on lease

For writes, the columns come straight from the statement:

// SQL: hand the machine back - the outcome and its date
$conn->prepare("UPDATE lease_assets SET outcome_id = ?, outcome_date = ?, updated_datetime = UTC_TIMESTAMP() WHERE id = ?")

becomes update lease_assets (outcome_id, outcome_date, updated_datetime).

Use @reads / @writes on the function for the why across all its queries. When a tag names a table the function's SQL never mentions, the page says so in a caution box - a quick way to spot a comment that has gone stale.

Tables and columns

Table pages take their descriptions from the comments in includes/db_verify_schema.php - a comment above a table describes the table, and a comment above a column or after it on the same line describes the column:

// An asset on a schedule; its own start/end when they differ from the schedule's.
'lease_assets' => [
    'id'          => 'INT NOT NULL AUTO_INCREMENT',
    'outcome_id'  => 'INT NULL',            // lease_outcomes.id; NULL = still on lease
    'end_date'    => 'DATE NULL',           // only when it differs from the schedule's
],

A // --- Section --- banner is a heading, not a description. When adding a table or column, add database/freeitsm.sql and this array as usual, and give the new entry its comment.


5. πŸͺ€ Traps

A comment containing TRAP: marks something that will bite the next person - the reason a line looks odd, or the mistake it exists to prevent. Every trap is collected onto its file's page, its function's page and the all-traps page. A // trap that runs over several lines is taken whole:

// TRAP: a returned asset keeps lease_expiry as the record of what happened, so
// anything that alerts on lease_expiry must skip these, or a machine handed
// back in March is "due back" for ever.

Say what goes wrong and why, so the warning makes sense to somebody who never saw the bug. See Code traps for the wider convention.


6. 🟨 JavaScript

The same shapes work in .js files. The scanner recognises every way the codebase writes a function:

/** Opens the ticket in the reading pane, loading it if it is not already. */
function openTicket(id) { ... }

// Merge the analyst's display zone into an Intl options object.
window.tzOpts = function (extra) { ... };

/** Rounds a duration to the nearest quarter hour. */
const roundQuarter = (mins) => Math.round(mins / 15) * 15;

const api = {
    /** Saves the board's column order. */
    saveOrder: function (ids) { ... },
};

class Board {
    /** Moves a card to another column and saves it. */
    move(cardId, columnId) { ... }
}

A /** */ block or a run of // lines directly above the function is its description. JavaScript parameters are listed on the file page from the signature; there is no Parameters table for them.

The scanner also links JavaScript to the API: any string containing api/.../x.php, or BASE + 'x.php' where BASE is set to an api/ folder in the same file, adds the file to that endpoint's Called from list.


7. ❌ What trips the scanner up

Mistake What happens Fix
A blank line, or another comment, between the doc block and the function The block is not the function's Keep the doc block directly above
Two doc blocks stacked above a function Only the last one counts; the first is orphaned Merge them, or move the first to where it belongs
*/ inside a /** */ block (e.g. "a plain / / block") The comment ends early - often a parse error Reword; never write */ in comment text
@param text with no meaning (@param PDO $conn) Treated as no description Write the meaning, or leave it out for a standard name
A decorative banner as the only comment (// ---- Helpers ----, /* ═══ ═══ */) Skipped - it is not a description Add a real comment under the banner
An HTML-only include with no <?php Nowhere to put a PHP comment Live with it, or add a <?php /** ... */ ?> header if the file can take one

8. βœ… Checking your work

  1. Save the file.
  2. Open System -> Engineering wiki and press Rescan - only changed files are read again, so it takes seconds. (Or run php scripts/engineering-wiki-scan.php from the command line.)
  3. Open the file's page under Code map -> files and look at it: summary, function table, Parameters, Database, Traps.

Before a release, run the scan from the command line so the shipped Code map matches the code - see the scanner guide.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally