Engineering Wiki Commenting Guide - edmozley/freeitsm GitHub Wiki
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.
| 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 |
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.
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): voidRules 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
getUserthat also creates the user when missing should say so. - British English, plain hyphens, no "we".
| 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} * @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 nothingUnits, 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'
) {}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.
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/@writeson 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.
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.
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.
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.
| 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 |
- Save the file.
- Open System -> Engineering wiki and press Rescan - only changed files are read again, so it takes seconds. (Or run
php scripts/engineering-wiki-scan.phpfrom the command line.) - 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.