Aryan Mishra
feat: initial commit - Multilingual ABSA project setup with 6 phases, 36 requirements
a6b96c2
Raw
History Blame Contribute Delete
6.06 kB
"use strict";
/**
* DispatchLogger interface + default implementation β€” issue #177 (ADR-0174 P1.3).
*
* Interface:
* { onEvent(event: DispatchEvent): void }
*
* Default behaviour (createDefaultLogger):
* 1. Silent on success β€” no stdout/stderr when result.kind === 'ok'.
* 2. Structured JSON to stderr on error β€” one line per dispatch error.
* 3. Opt-in audit file β€” when GSD_AUDIT=1 OR config.audit.enabled===true,
* appends every event (success + error) as one JSON line to
* .planning/.gsd-trace.jsonl relative to `cwd`. Creates .planning/ if absent.
* 4. Args redaction β€” args omitted by default; included when GSD_AUDIT_ARGS=1.
*
* No-op logger (createNoOpLogger):
* Silent on all events. Used as the Hub default when no logger is injected.
*
* ADR-457 build-at-publish: the hand-written bin/lib/observability/logger.cjs
* collapsed to a TypeScript source of truth. Behaviour is preserved
* byte-for-behaviour from the prior hand-written .cjs; only types are added.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const redaction_cjs_1 = require("./redaction.cjs");
const AUDIT_FILE_NAME = '.gsd-trace.jsonl';
const PLANNING_DIR = '.planning';
// ─── helpers ─────────────────────────────────────────────────────────────────
/**
* Safely serialise a value to JSON, falling back to a placeholder on circular refs.
*/
function _safeStringify(value) {
try {
return JSON.stringify(value);
}
catch {
return JSON.stringify({ _serializationError: true });
}
}
/**
* Determine whether the audit file should be written to.
*/
function _isAuditEnabled(config) {
if (process.env['GSD_AUDIT'] === '1')
return true;
if (config && config.audit && config.audit.enabled === true)
return true;
return false;
}
/**
* Build the redacted plain object for the audit file.
* Preserves the full DispatchEvent structure.
*/
function _toAuditRecord(event) {
return (0, redaction_cjs_1.redactEvent)(event);
}
/**
* Build the flattened stderr error line.
*
* Per ADR-0174 P1.3 contract: { "kind": "<variant>", "traceId": "<uuid>", ...typedPayload }
* The result's kind is promoted to top-level and the typed payload fields are spread in.
* The `result` wrapper is removed.
*/
function _toStderrRecord(event) {
const redacted = (0, redaction_cjs_1.redactEvent)(event);
const { result, ...eventWithoutResult } = redacted;
// Flatten: top-level gets kind + typed payload fields from result
const resultObj = result;
const { kind, ...typedPayload } = resultObj;
return Object.assign({}, eventWithoutResult, { kind }, typedPayload);
}
/**
* Append one JSON line to the audit file.
* Creates .planning/ directory if it does not exist.
*
* Uses synchronous fs API (crash-safe for v1 β€” dispatch is synchronous).
*/
function _appendAuditLine(cwd, event) {
const planningDir = node_path_1.default.join(cwd, PLANNING_DIR);
// Ensure the directory exists
if (!node_fs_1.default.existsSync(planningDir)) {
node_fs_1.default.mkdirSync(planningDir, { recursive: true });
}
const auditPath = node_path_1.default.join(planningDir, AUDIT_FILE_NAME);
node_fs_1.default.appendFileSync(auditPath, _safeStringify(event) + '\n', 'utf8');
}
/**
* Create a no-op logger. All events are silently dropped.
* This is the Hub's default when no logger is injected by the caller.
*/
function createNoOpLogger() {
return {
onEvent(_event) {
// intentionally empty
},
};
}
/**
* Create the default DispatchLogger.
*/
function createDefaultLogger({ cwd = process.cwd(), config } = {}) {
return {
/**
* @param event - A DispatchEvent from the Hub.
*/
onEvent(event) {
const resultObj = event && event['result'];
const isOk = resultObj && resultObj['kind'] === 'ok';
// ── Audit file (both ok and error) ────────────────────────────────────
if (_isAuditEnabled(config)) {
try {
const auditRecord = _toAuditRecord(event);
_appendAuditLine(cwd, auditRecord);
}
catch (auditErr) {
// Audit errors must not surface to callers
process.stderr.write(_safeStringify({
level: 'warn',
source: 'DispatchLogger',
message: 'audit file write failed: ' + String(auditErr?.message ?? auditErr),
}) + '\n');
}
}
// ── Stderr on error ───────────────────────────────────────────────────
if (!isOk) {
try {
const stderrRecord = _toStderrRecord(event);
process.stderr.write(_safeStringify(stderrRecord) + '\n');
}
catch (stderrErr) {
// Last-resort: we cannot throw from the logger
process.stderr.write(_safeStringify({
level: 'warn',
source: 'DispatchLogger',
message: 'stderr emit failed: ' + String(stderrErr?.message ?? stderrErr),
}) + '\n');
}
}
// ── Silent on success (no else branch needed) ─────────────────────────
},
};
}
module.exports = { createDefaultLogger, createNoOpLogger };