pediatric-ai-scribe-v3/src/utils/fileLog.js
Daniel 83206e907c
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 48s
Forgejo Docker Build / Root app tests (push) Successful in 51s
Forgejo Android APK / Build signed APK (push) Successful in 2m16s
Forgejo Docker Build / Build Docker image (push) Successful in 20s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
fix: a second deck reused the first deck's pictures
The image job key was the request-body hash plus the figure's position — slide
index for a deck, reply index for a document. Two generations from the same form
produce the same body hash, so figure 4 of the second generation collided with
figure 4 of the first on the unique (owner, workflow, idempotency_key). The
constraint handed back the existing job, and the new deck displayed the old
deck's artwork. The decks are not even the same length, so the reused picture
could land on a slide about something else entirely.

Keyed on what is being drawn now: the body hash stays, so submitting the
identical request twice still dedupes rather than billing twice, and a hash of
the prompt (plus layout and shape) is what makes two different pictures two
different jobs. Same fix in deckBuild and resourceImages.

Also split fileLog out of logger. logger requires the database at module load,
so importing it to record a diagnostic pulls in a connection pool — wrong on its
own terms, and it hung the whole test suite when imageTool started logging its
refusals: a unit test that never touches a database inherited an open pool
handle and never exited. logger.file now delegates to fileLog, so there is still
one implementation of where a line goes and how it is redacted.

With that in place, every image-tool refusal is recorded durably. There are five
of them, they want five different fixes, and until now none of them left any
trace once the container was replaced.

Verified against a mutation: restoring the index-based key fails two of the four
collision tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-12 08:26:09 +02:00

38 lines
1.7 KiB
JavaScript

// ============================================================
// FILE LOG
// ============================================================
// Appending a line to the dated log file, and nothing else.
//
// Split out of logger.js because logger requires the database at module load,
// so importing it just to record a diagnostic pulls in a connection pool. That
// is wrong on its own terms — a note about what happened should not need a
// database — and it hung the test suite: a unit test that exercised a code path
// containing a log call inherited an open pool handle and never exited.
//
// fs and the redactor only. logger.file delegates here, so there is one
// implementation of where a line goes and how it is redacted.
var fs = require('fs');
var path = require('path');
var { redact } = require('./redact');
var LOG_DIR = path.join(__dirname, '../../data/logs');
function write(level, message, data) {
try {
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
var now = new Date();
var file = path.join(LOG_DIR, now.toISOString().split('T')[0] + '.log');
// Defensive redaction: both message and data go through redact() so PHI
// patterns cannot reach the file if a caller passes a request body, a
// clinical string, or a stack trace containing transcript text.
var line = '[' + now.toISOString() + '] [' + level.toUpperCase() + '] ' +
redact(String(message == null ? '' : message));
if (data != null) {
line += ' | ' + redact(typeof data === 'string' ? data : JSON.stringify(data));
}
fs.appendFileSync(file, line + '\n');
if (level === 'error') console.error(line);
} catch (e) { /* a diagnostic is never worth failing the caller for */ }
}
module.exports = { write, LOG_DIR };