The workstation
Real files, annotated.
No fragments written for the occasion. Every block below comes from a repository that runs: the atlasmuseum wiki, and this platform itself. Next to each block: what it does, which pattern it follows, what the trade-off was and what it delivered.
Counted in the atlasmuseum repository on 12 September 2026, without the vendored jQuery and without the import script from before the engagement. The most recently changed file of my own dates from 11 September 2026. The atlasmuseum files are kept in this repository as verbatim copies, path for path, so the line numbers here are the line numbers in the wiki.
atlasmuseum
The wiki that had been given up on: migrated from MediaWiki 1.26.4 to 1.39.10, with its own API layer, own extensions and own skin. Four files from the own work.
The map query, with bound parameters
amapi/generators/map.php
try {
$mysqli = new mysqli(DB_SERVER, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT, DB_SOCKET);
} catch (mysqli_sql_exception $e) {
error_log('[AMAPI Map] database connection failed: ' . $e->getMessage());
return [];
}
$mysqli->set_charset('utf8mb4');
// Only the columns actually used below, instead of SELECT *.
$columns = 'article, title, artist, lat, lon, nature, wikidata';
try {
if (is_array($nature) && sizeof($nature) > 0) {
$placeholders = implode(', ', array_fill(0, sizeof($nature), '?'));
$stmt = $mysqli->prepare(
'SELECT ' . $columns . ' FROM ' . DATABASE_MAP . ' WHERE nature IN (' . $placeholders . ')'
);
// All bound values are strings; str_repeat builds the type string.
$stmt->bind_param(str_repeat('s', sizeof($nature)), ...array_values($nature));
$stmt->execute();
$result = $stmt->get_result();
} else {
$result = $mysqli->query('SELECT ' . $columns . ' FROM ' . DATABASE_MAP);
}
} catch (mysqli_sql_exception $e) {
- Role
- The only place where AMAPI reads SQL directly. Every other endpoint goes through the MediaWiki action API, so an upgrade of the wiki cannot break the backend. A map with thousands of points is the exception: it comes from a denormalised map table.
- Pattern
- Prepared statement with one placeholder per value; the type string is built from the length of the list. The connection sits in a try/catch because mysqli reports errors as exceptions since PHP 8.1, and the old error branch was never reached any more.
- Trade-off
- Only the columns the map uses instead of SELECT *, at the cost of a longer query in the code. A failed connection yields an empty map rather than an empty response with a fatal error.
- What it delivered
- This replaced a hand-written str_replace that escaped the quote but not the backslash: an SQL injection reachable from outside through the nature parameter. Fixed with exactly this block.
The map rebuilt in one transaction
amapi/rebuildMap.php
// 3) Rewrite am_map inside a transaction (atomic; the map is never empty
// mid-rebuild). DELETE + INSERT rather than TRUNCATE so it can roll back.
$mysqli = @new mysqli( DB_SERVER, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT, DB_SOCKET );
if ( $mysqli->connect_errno ) {
rm_log( 'Database connection failed: ' . $mysqli->connect_error );
flock( $lock, LOCK_UN );
exit( 1 );
}
$mysqli->set_charset( 'utf8' );
$mysqli->begin_transaction();
try {
$mysqli->query( 'DELETE FROM ' . DATABASE_MAP . ';' );
$batchSize = 100;
for ( $j = 0; $j < $total; $j += $batchSize ) {
$values = [];
for ( $i = $j; $i < $j + $batchSize && $i < $total; $i++ ) {
$row = $dataAM[ $i ];
// Keep each value within its column width so a stray oversized value
// can never abort the whole rebuild. Wikidata must be a Q-id.
$wd = (string)( $row['wikidata'] ?? '' );
if ( !preg_match( '/^Q[0-9]{1,14}$/', $wd ) ) {
$wd = '';
}
$article = mb_substr( (string)( $row['article'] ?? '' ), 0, 255 );
$title = mb_substr( (string)( $row['title'] ?? '' ), 0, 255 );
$artist = mb_substr( (string)( $row['artist'] ?? '' ), 0, 255 );
$nature = mb_substr( (string)( $row['nature'] ?? '' ), 0, 20 );
$image = mb_substr( (string)( $row['image'] ?? '' ), 0, 255 );
$values[] = sprintf(
'(%d,"%s","%s","%s",%f,%f,"%s","%s","%s")',
$i + 1,
$mysqli->real_escape_string( $article ),
$mysqli->real_escape_string( $title ),
$mysqli->real_escape_string( $artist ),
isset( $row['lat'] ) ? (float)$row['lat'] : 0,
isset( $row['lon'] ) ? (float)$row['lon'] : 0,
$mysqli->real_escape_string( $nature ),
$mysqli->real_escape_string( $wd ),
$mysqli->real_escape_string( $image )
);
}
if ( $values ) {
$sql = 'INSERT INTO ' . DATABASE_MAP
. ' (id,article,title,artist,lat,lon,nature,wikidata,image) VALUES '
. implode( ',', $values );
if ( !$mysqli->query( $sql ) ) {
throw new RuntimeException( $mysqli->error );
}
}
}
$mysqli->commit();
} catch ( \Throwable $e ) {
$mysqli->rollback();
rm_log( 'Rebuild failed, rolled back: ' . $e->getMessage() );
flock( $lock, LOCK_UN );
exit( 1 );
}
$ms = round( ( microtime( true ) - $t0 ) * 1000 );
rm_log( "Map rebuilt: $total points written to " . DATABASE_MAP . " in {$ms} ms." );
- Role
- The command line script that refills the map table from Semantic MediaWiki and Wikidata. It runs from the command line, so no HTTP call is nested inside another.
- Pattern
- DELETE and INSERT inside one transaction, in batches of a hundred, with a rollback on any error. No TRUNCATE, because that cannot roll back. So the map is never empty while it is being rebuilt.
- Trade-off
- A transaction over thousands of rows holds a lock longer than a TRUNCATE would. That is the price of a map that still works halfway through a failed rebuild.
- What it delivered
- Above this block sit two safeties: an empty layer is never written, and neither is an incomplete one. The second came after a batch with a timeout was once read as "no more results" and the map fell from 4,537 to 988 points.
Labels from Wikidata, in chunks of fifty
extensions/AtlasRepair/includes/Repair.php
public static function resolveLabels( array $qids ): array {
$out = [];
$fetch = [];
$dir = sys_get_temp_dir();
foreach ( $qids as $q ) {
$cf = $dir . '/amrepair-' . $q . '.txt';
if ( is_file( $cf ) && ( time() - filemtime( $cf ) < 2592000 ) ) {
$v = @file_get_contents( $cf );
if ( $v !== false && $v !== '' ) { $out[ $q ] = $v; continue; }
}
$fetch[] = $q;
}
// Wikidata allows up to 50 ids per call.
foreach ( array_chunk( $fetch, 50 ) as $chunk ) {
$url = 'https://www.wikidata.org/w/api.php?action=wbgetentities&props=labels'
. '&languages=fr%7Cen&format=json&ids=' . implode( '%7C', $chunk );
$raw = self::httpGet( $url );
$json = $raw ? json_decode( $raw ) : null;
if ( !$json || !isset( $json->entities ) ) { continue; }
foreach ( $chunk as $q ) {
$label = '';
if ( isset( $json->entities->{$q}->labels->fr->value ) ) {
$label = $json->entities->{$q}->labels->fr->value;
} elseif ( isset( $json->entities->{$q}->labels->en->value ) ) {
$label = $json->entities->{$q}->labels->en->value;
}
if ( $label !== '' ) {
$out[ $q ] = $label;
@file_put_contents( $dir . '/amrepair-' . $q . '.txt', $label );
}
}
}
return $out;
}
/** Simple GET with a descriptive User-Agent (Wikidata requires one). */
- Role
- AtlasRepair replaces Q-numbers that an old import left in artist and work notices with the name Wikidata knows for them. This is the function that fetches those names.
- Pattern
- A cache per Q-number on disk, valid for thirty days, then one call per fifty numbers: the maximum the Wikidata API allows per request. French first, English as the fallback.
- Trade-off
- A file cache rather than a table: no migration needed in a wiki that had just been rescued, and a repair you can repeat without loading Wikidata again. The file can disappear on a server restart; then it is simply fetched again.
- What it delivered
- The repair first runs as a plan and only with --apply as a change, with a report in between. Four files, 488 lines, and the notices were readable again.
The quality index, one score per notice
extensions/AtlasQualite/includes/QualityIndex.php
public static function rebuild( ?callable $progress = null ): array {
$config = MediaWikiServices::getInstance()->getMainConfig();
$category = $config->get( 'AtlasQualiteCategory' );
$checks = self::checks();
$rows = self::querySmw( $category, $checks );
$records = [];
$issueCounts = [];
foreach ( $checks as $c ) {
$issueCounts[ $c['key'] ] = 0;
}
$complete = 0;
$done = 0;
foreach ( $rows as $page => $values ) {
$missing = [];
$maxWeight = 0;
$gotWeight = 0;
foreach ( $checks as $c ) {
$maxWeight += $c['weight'];
$val = $values[ $c['key'] ] ?? '';
$ok = self::passes( $c, $val );
if ( $ok ) {
$gotWeight += $c['weight'];
} else {
$missing[] = $c['key'];
$issueCounts[ $c['key'] ]++;
}
}
$score = $maxWeight > 0 ? (int) round( 100 * $gotWeight / $maxWeight ) : 100;
if ( $score === 100 ) {
$complete++;
}
$records[] = [
'page' => $page,
'score' => $score,
'missing' => $missing,
];
$done++;
if ( $progress && ( $done % 200 === 0 ) ) {
$progress( $done );
}
}
- Role
- AtlasQualité gives every work notice a score from a list of weighted checks and puts the most incomplete notices first: a curator's worklist.
- Pattern
- One pass over the results of a semantic query, a weighted sum per page, and an index as JSON on disk. The build runs only from the command line, never on a page view.
- Trade-off
- An index that can be as old as the last rebuild when the dashboard opens, in exchange for a dashboard that opens instantly on a wiki with tens of thousands of pages.
- What it delivered
- Four files and one stylesheet, 640 lines, and an association that could see for the first time which notices still needed attention.
This platform
The site you are reading measures itself and keeps every measurement in a chain that cannot be changed unnoticed. Three files from the core.
The link of the chain
packages/core/src/chain/hash-chain.ts
export function linkHash(prevHash: Sha256, body: ChainRecordBody): Sha256 {
const canonical = canonicalJsonOrThrow(bodyForHashing(body));
return Sha256.unsafe(sha256Hex(concatBytes(utf8(prevHash), utf8(canonical))));
}
/**
* The hashed shape is written out field by field rather than spread from the
* argument. An extra property on a caller's object must never silently become
* part of the digest, and a renamed field must break the build rather than
* quietly invalidate every stored chain.
*/
function bodyForHashing(body: ChainRecordBody): JsonValue {
return {
chainId: body.chainId,
payload: body.payload,
recordedAt: body.recordedAt,
sequence: body.sequence,
};
}
- Role
- The core of the evidence on this site. Every measurement gets a hash over the hash of its predecessor and its own canonical JSON. No I/O, so fully unit-testable.
- Pattern
- The hashed shape is written out field by field rather than spread from the argument. An extra property on a caller's object can never silently become part of the digest, and a renamed field breaks the build instead of every stored chain.
- Trade-off
- More lines than a spread, and that is the point: the shape of the link is a contract that the server, the worker and the verifier in the browser share byte for byte.
- What it delivered
- The same function runs in the worker that writes, in the API that reads, and in the browser of the visitor who checks. There is one definition, and it is here.
The same rule, once more, in the database
packages/db/migrations/0001_targets_runs_measurements.sql
CREATE TABLE measurements (
id uuid PRIMARY KEY,
run_id uuid NOT NULL UNIQUE REFERENCES runs (id) ON DELETE RESTRICT,
target_id uuid NOT NULL REFERENCES targets (id) ON DELETE RESTRICT,
-- The chain. `payload` is the canonical-JSON body; `hash` is
-- sha256(prev_hash || canonical_json(body)) as defined in packages/core.
chain_id uuid NOT NULL,
sequence bigint NOT NULL,
recorded_at timestamptz NOT NULL,
payload jsonb NOT NULL,
prev_hash char(64) NOT NULL,
hash char(64) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT measurements_sequence_non_negative CHECK (sequence >= 0),
CONSTRAINT measurements_hash_hex CHECK (hash ~ '^[0-9a-f]{64}$'),
CONSTRAINT measurements_prev_hash_hex CHECK (prev_hash ~ '^[0-9a-f]{64}$'),
-- The genesis record is the only one that may point at the zero hash.
CONSTRAINT measurements_genesis_is_first CHECK (
(sequence = 0) = (prev_hash = repeat('0', 64))
),
-- A chain never forks: one record per position, and one successor per record.
CONSTRAINT measurements_one_per_position UNIQUE (chain_id, sequence),
CONSTRAINT measurements_one_successor UNIQUE (chain_id, prev_hash),
CONSTRAINT measurements_hash_unique UNIQUE (hash)
);
- Role
- Defence in depth. Every invariant the core enforces in TypeScript stands here again as a constraint: a hash is 64 hexadecimal characters, only the first record points at the zero hash, and a chain never forks.
- Pattern
- CHECK constraints for the shape, UNIQUE constraints for the structure: one record per position and one successor per record. It is the chain as schema, not as agreement.
- Trade-off
- The same rule twice is not repetition. The code gives the developer a guarantee at compile and test time; the database gives the operator a guarantee that also holds against a migration script or a manual psql session.
- What it delivered
- A chain that stays intact only as long as the application writes carefully is not a chain. These constraints are what makes "cannot be changed unnoticed" true.
Claiming a job without running it twice
packages/db/src/jobs.ts
export async function claimJobs(
sql: Sql,
workerId: string,
limit = 1,
leaseMs: number = DEFAULT_LEASE_MS,
): Promise<ClaimedJob[]> {
const rows = await sql<JobRow[]>`
UPDATE jobs AS j
SET state = 'claimed',
claimed_by = ${workerId},
claimed_at = now(),
lease_expires_at = now() + make_interval(secs => ${leaseMs / 1000}),
attempts = j.attempts + 1,
updated_at = now()
WHERE j.id IN (
SELECT c.id
FROM jobs c
WHERE c.state = 'pending'
AND c.run_at <= now()
-- A job with no attempts left must never be picked up: the claim
-- increments attempts, and jobs_attempts_bounded would reject it.
-- Reaching that constraint means the queue tried to run something it
-- had already given up on.
AND c.attempts < c.max_attempts
ORDER BY c.run_at
FOR UPDATE SKIP LOCKED
LIMIT ${limit}
)
RETURNING j.id, j.target_id, j.state, j.run_at, j.attempts, j.max_attempts,
j.claimed_by, j.lease_expires_at, j.last_error, j.dead_lettered_at
`;
return rows.map(toClaimed);
}
- Role
- The measurement workers' queue runs on Postgres, without a separate message broker. This is the one statement with which a worker takes a job for itself.
- Pattern
- UPDATE with a nested SELECT ... FOR UPDATE SKIP LOCKED. The inner select locks candidates and skips rows another transaction already holds. Two workers at the same instant therefore get disjoint sets, and the UPDATE flips the state before the lock is released.
- Trade-off
- Postgres as a queue means one system fewer to operate, with a measured throughput that is ample at this scale. The decision and the figures are in the architecture decisions of the repository.
- What it delivered
- Without SKIP LOCKED this is the classic queue bug: two workers read the same oldest row and run the same job. A row lock alone is not enough either, it lines the workers up in a queue of one.
More of this platform, with the measurements it keeps on itself, is on the work page.