Repository navigation
Engineering Wiki Commenting 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.
| 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.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96