File size: 6,059 Bytes
a6b96c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"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 };