@ngockhoale/ukit 3.0.6 → 3.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/scripts/bench/data-foundation.mjs +562 -0
- package/src/core/observability/adapters/common.js +75 -0
- package/src/core/observability/adapters/contextAdapter.js +55 -0
- package/src/core/observability/adapters/decisionAdapter.js +61 -0
- package/src/core/observability/adapters/routeAdapter.js +135 -0
- package/src/core/observability/analytics/digest.js +186 -0
- package/src/core/observability/analytics/fingerprints.js +126 -0
- package/src/core/observability/analytics/opportunities.js +329 -0
- package/src/core/observability/analytics/rebuild.js +56 -0
- package/src/core/observability/analytics/summary.js +298 -0
- package/src/core/observability/emit/config.js +29 -0
- package/src/core/observability/emit/recorder.js +297 -0
- package/src/core/observability/evaluation/aiPacket.js +230 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +172 -0
- package/src/core/observability/evaluation/replay.js +143 -0
- package/src/core/observability/evaluation/scorecard.js +445 -0
- package/src/core/observability/privacy/allowlist.js +185 -0
- package/src/core/observability/privacy/redaction.js +113 -0
- package/src/core/observability/privacy/sanitizeForSupport.js +133 -0
- package/src/core/observability/privacy/sanitizeObserved.js +134 -0
- package/src/core/observability/rollout.js +155 -0
- package/src/core/observability/schema/constants.js +66 -0
- package/src/core/observability/schema/registry.js +223 -0
- package/src/core/observability/schema/validate.js +227 -0
- package/src/core/observability/segments/internal.js +241 -0
- package/src/core/observability/segments/readSegments.js +215 -0
- package/src/core/observability/segments/recovery.js +123 -0
- package/src/core/observability/segments/retention.js +381 -0
- package/src/core/observability/support/import.js +402 -0
- package/src/core/observability/support/manifest.js +135 -0
- package/src/core/observability/support/paths.js +94 -0
- package/src/core/observability/support/projector.js +483 -0
- package/src/core/observability/support/renderer.js +130 -0
- package/src/core/observability/support/retention.js +155 -0
- package/template_project/.omp/RULES.md +6 -6
- package/template_project/.omp/config.yml +6 -0
- package/template_project/instructions/overlays/omp-rules.md +6 -6
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* readSegments.js (TASK-006, SPEC §5 DF-FR05 / §8)
|
|
3
|
+
*
|
|
4
|
+
* Typed reader over the append-only JSONL segment store.
|
|
5
|
+
*
|
|
6
|
+
* readSegments(root, { fromInclusive?, limit?, ioTimeoutMs? })
|
|
7
|
+
* → AsyncIterable<record> with attached:
|
|
8
|
+
* .coverage → { expired_segments, quarantined_segments, corrupt_lines,
|
|
9
|
+
* partial_tail_bytes, cursor_missed, degraded[] }
|
|
10
|
+
* .ok → boolean (false when any degraded entry was recorded)
|
|
11
|
+
*
|
|
12
|
+
* Read order is deterministic and never wall-clock based: sealed segments in
|
|
13
|
+
* sealed_seq order, then the active segment. `fromInclusive` is a record_id
|
|
14
|
+
* cursor — iteration starts at the first retained record with that id; when
|
|
15
|
+
* the cursor is not found in retained data, all retained records are yielded
|
|
16
|
+
* and coverage.cursor_missed = true (no claim of reproducibility over expired
|
|
17
|
+
* data). `limit` caps yielded records.
|
|
18
|
+
*
|
|
19
|
+
* Integrity: every sealed segment's sha256 is verified against its meta
|
|
20
|
+
* sidecar before a single row is served; a mismatch or missing meta sends the
|
|
21
|
+
* segment to quarantine/ — corrupt data is never falsified into the stream.
|
|
22
|
+
* A partial tail row in the active segment (hard-kill artifact) is skipped
|
|
23
|
+
* and counted, never parsed. Symlinked or foreign-named files are never
|
|
24
|
+
* followed or read.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
|
|
30
|
+
import { validateSemanticRecord } from '../schema/validate.js';
|
|
31
|
+
import { quarantineSegment } from './recovery.js';
|
|
32
|
+
import {
|
|
33
|
+
DEFAULT_IO_TIMEOUT_MS,
|
|
34
|
+
ioReason,
|
|
35
|
+
listSegments,
|
|
36
|
+
metaPathFor,
|
|
37
|
+
readState,
|
|
38
|
+
resolveRoot,
|
|
39
|
+
sha256Buffer,
|
|
40
|
+
withTimeout,
|
|
41
|
+
} from './internal.js';
|
|
42
|
+
|
|
43
|
+
export function readSegments(root, opts = {}) {
|
|
44
|
+
const timeoutMs = Number.isFinite(opts.ioTimeoutMs) ? opts.ioTimeoutMs : DEFAULT_IO_TIMEOUT_MS;
|
|
45
|
+
const fromInclusive = typeof opts.fromInclusive === 'string' ? opts.fromInclusive : null;
|
|
46
|
+
const limit = Number.isInteger(opts.limit) && opts.limit >= 0 ? opts.limit : null;
|
|
47
|
+
|
|
48
|
+
const coverage = {
|
|
49
|
+
expired_segments: 0,
|
|
50
|
+
quarantined_segments: 0,
|
|
51
|
+
corrupt_lines: 0,
|
|
52
|
+
partial_tail_bytes: 0,
|
|
53
|
+
cursor_missed: false,
|
|
54
|
+
degraded: [],
|
|
55
|
+
};
|
|
56
|
+
const degrade = (reason, file) => {
|
|
57
|
+
coverage.degraded.push({ reason, file });
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
async function* iterate() {
|
|
61
|
+
const resolved = await resolveRoot(root, timeoutMs);
|
|
62
|
+
if (!resolved.ok) {
|
|
63
|
+
degrade(resolved.reason, String(root));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!resolved.exists) return; // empty store → empty stream, still ok
|
|
67
|
+
|
|
68
|
+
const state = await readState(resolved.root, timeoutMs);
|
|
69
|
+
coverage.expired_segments = state.expired_segments || 0;
|
|
70
|
+
|
|
71
|
+
const listing = await listSegments(resolved.root, timeoutMs);
|
|
72
|
+
if (!listing.ok) {
|
|
73
|
+
degrade(listing.reason, resolved.root);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const skipped of listing.skipped) {
|
|
77
|
+
degrade(skipped.reason, skipped.name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let yielded = 0;
|
|
81
|
+
let cursorPending = fromInclusive !== null;
|
|
82
|
+
// Records scanned while the cursor is still pending. If the cursor is
|
|
83
|
+
// found the buffer is discarded (records before it are not yielded); if
|
|
84
|
+
// the scan ends with the cursor unfound, the buffer IS the retained set
|
|
85
|
+
// and is flushed so consumers still get every retained record.
|
|
86
|
+
let pending = cursorPending ? [] : null;
|
|
87
|
+
|
|
88
|
+
const emit = (record) => {
|
|
89
|
+
if (cursorPending) {
|
|
90
|
+
if (record.record_id !== fromInclusive) {
|
|
91
|
+
pending.push(record);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
cursorPending = false;
|
|
95
|
+
pending = null;
|
|
96
|
+
}
|
|
97
|
+
return record;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Sealed segments: verify checksum BEFORE serving a single row.
|
|
101
|
+
for (const seg of listing.sealed) {
|
|
102
|
+
if (limit !== null && yielded >= limit) return;
|
|
103
|
+
let meta = null;
|
|
104
|
+
try {
|
|
105
|
+
meta = JSON.parse(
|
|
106
|
+
await withTimeout(fs.promises.readFile(metaPathFor(seg.path), 'utf8'), timeoutMs),
|
|
107
|
+
);
|
|
108
|
+
} catch {
|
|
109
|
+
meta = null;
|
|
110
|
+
}
|
|
111
|
+
let buf = null;
|
|
112
|
+
try {
|
|
113
|
+
buf = await withTimeout(fs.promises.readFile(seg.path), timeoutMs);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
degrade(ioReason(err), seg.name);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const checksumOk =
|
|
119
|
+
meta && typeof meta.checksum === 'string' && meta.checksum === sha256Buffer(buf);
|
|
120
|
+
if (!checksumOk) {
|
|
121
|
+
const q = await quarantineSegment(resolved.root, seg.name, 'checksum_mismatch', {
|
|
122
|
+
ioTimeoutMs: timeoutMs,
|
|
123
|
+
});
|
|
124
|
+
if (q.ok) {
|
|
125
|
+
coverage.quarantined_segments += 1;
|
|
126
|
+
degrade('checksum_mismatch_quarantined', seg.name);
|
|
127
|
+
} else {
|
|
128
|
+
degrade(`quarantine_failed:${q.reason}`, seg.name);
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const text = buf.toString('utf8');
|
|
133
|
+
const complete = text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
134
|
+
for (const line of complete.split('\n')) {
|
|
135
|
+
if (limit !== null && yielded >= limit) return;
|
|
136
|
+
if (line.length === 0) continue;
|
|
137
|
+
let record;
|
|
138
|
+
try {
|
|
139
|
+
record = JSON.parse(line);
|
|
140
|
+
} catch {
|
|
141
|
+
coverage.corrupt_lines += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!validateSemanticRecord(record).ok) {
|
|
145
|
+
coverage.corrupt_lines += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const out = emit(record);
|
|
149
|
+
if (out) {
|
|
150
|
+
yielded += 1;
|
|
151
|
+
yield out;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Active segment: no checksum (still open); a partial tail row is a
|
|
157
|
+
// hard-kill artifact — skipped and counted, never parsed.
|
|
158
|
+
if (listing.active && (limit === null || yielded < limit)) {
|
|
159
|
+
let buf;
|
|
160
|
+
try {
|
|
161
|
+
buf = await withTimeout(fs.promises.readFile(listing.active.path), timeoutMs);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
degrade(ioReason(err), 'active.jsonl');
|
|
164
|
+
buf = null;
|
|
165
|
+
}
|
|
166
|
+
if (buf) {
|
|
167
|
+
const text = buf.toString('utf8');
|
|
168
|
+
const endsComplete = text.length === 0 || text.endsWith('\n');
|
|
169
|
+
const body = endsComplete ? text.slice(0, -1) : text.slice(0, text.lastIndexOf('\n') + 1);
|
|
170
|
+
if (!endsComplete) {
|
|
171
|
+
coverage.partial_tail_bytes += buf.length - (text.lastIndexOf('\n') + 1);
|
|
172
|
+
}
|
|
173
|
+
for (const line of body.split('\n')) {
|
|
174
|
+
if (limit !== null && yielded >= limit) return;
|
|
175
|
+
if (line.length === 0) continue;
|
|
176
|
+
let record;
|
|
177
|
+
try {
|
|
178
|
+
record = JSON.parse(line);
|
|
179
|
+
} catch {
|
|
180
|
+
coverage.corrupt_lines += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!validateSemanticRecord(record).ok) {
|
|
184
|
+
coverage.corrupt_lines += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const out = emit(record);
|
|
188
|
+
if (out) {
|
|
189
|
+
yielded += 1;
|
|
190
|
+
yield out;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (cursorPending) {
|
|
196
|
+
coverage.cursor_missed = true;
|
|
197
|
+
for (const record of pending) {
|
|
198
|
+
if (limit !== null && yielded >= limit) break;
|
|
199
|
+
yielded += 1;
|
|
200
|
+
yield record;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const iterable = {
|
|
206
|
+
coverage,
|
|
207
|
+
get ok() {
|
|
208
|
+
return coverage.degraded.length === 0;
|
|
209
|
+
},
|
|
210
|
+
[Symbol.asyncIterator]() {
|
|
211
|
+
return iterate();
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
return iterable;
|
|
215
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recovery.js (TASK-006, SPEC §5 DF-FR05)
|
|
3
|
+
*
|
|
4
|
+
* Partial-tail recovery and corrupt-segment quarantine for the append-only
|
|
5
|
+
* JSONL segment store.
|
|
6
|
+
*
|
|
7
|
+
* recoverTail(segmentPath, opts?)
|
|
8
|
+
* → { ok: true, recovered, droppedBytes, droppedLines }
|
|
9
|
+
* | { ok: false, reason }
|
|
10
|
+
*
|
|
11
|
+
* quarantineSegment(root, segmentName, reason)
|
|
12
|
+
* → { ok: true } | { ok: false, reason }
|
|
13
|
+
*
|
|
14
|
+
* Recovery never fabricates data: a truncated tail is dropped, the complete
|
|
15
|
+
* prefix is preserved byte-for-byte, the segment is marked `recovered: true`
|
|
16
|
+
* in its sidecar meta, and any existing checksum is recomputed over the
|
|
17
|
+
* truncated bytes (never carried over from the pre-truncation file).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_IO_TIMEOUT_MS,
|
|
25
|
+
QUARANTINE_DIR,
|
|
26
|
+
ioReason,
|
|
27
|
+
metaPathFor,
|
|
28
|
+
resolveFileInDir,
|
|
29
|
+
sha256Buffer,
|
|
30
|
+
withTimeout,
|
|
31
|
+
} from './internal.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Move a corrupt sealed segment (and its meta sidecar) into `quarantine/`.
|
|
35
|
+
* The segment is never deleted and never silently repaired — it is preserved
|
|
36
|
+
* for inspection outside the live read path.
|
|
37
|
+
*/
|
|
38
|
+
export async function quarantineSegment(root, segmentName, reason, opts = {}) {
|
|
39
|
+
const timeoutMs = Number.isFinite(opts.ioTimeoutMs) ? opts.ioTimeoutMs : DEFAULT_IO_TIMEOUT_MS;
|
|
40
|
+
const base = path.basename(String(segmentName));
|
|
41
|
+
if (base !== segmentName || !base.endsWith('.jsonl')) {
|
|
42
|
+
return { ok: false, reason: 'unsafe_path' };
|
|
43
|
+
}
|
|
44
|
+
const quarantineDir = path.join(root, QUARANTINE_DIR);
|
|
45
|
+
try {
|
|
46
|
+
await withTimeout(fs.promises.mkdir(quarantineDir, { recursive: true }), timeoutMs);
|
|
47
|
+
const from = path.join(root, base);
|
|
48
|
+
let to = path.join(quarantineDir, base);
|
|
49
|
+
try {
|
|
50
|
+
await withTimeout(fs.promises.lstat(to), timeoutMs);
|
|
51
|
+
to = path.join(quarantineDir, `${base}.${Date.now()}.corrupt`);
|
|
52
|
+
} catch {
|
|
53
|
+
// Free name — keep the original filename for inspectability.
|
|
54
|
+
}
|
|
55
|
+
await withTimeout(fs.promises.rename(from, to), timeoutMs);
|
|
56
|
+
const metaFrom = metaPathFor(from);
|
|
57
|
+
try {
|
|
58
|
+
await withTimeout(fs.promises.rename(metaFrom, `${to}.meta.json`), timeoutMs);
|
|
59
|
+
} catch {
|
|
60
|
+
// Missing meta is expected for some corruption modes — not fatal.
|
|
61
|
+
}
|
|
62
|
+
return { ok: true };
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return { ok: false, reason: ioReason(err) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Drop a partial tail row (hard-kill artifact) from a JSONL segment.
|
|
70
|
+
* The file must end in '\n' to be considered complete; anything after the
|
|
71
|
+
* last newline is uncommitted and removed. The complete prefix is preserved
|
|
72
|
+
* exactly — recovery never rewrites surviving rows.
|
|
73
|
+
*/
|
|
74
|
+
export async function recoverTail(segmentPath, opts = {}) {
|
|
75
|
+
const timeoutMs = Number.isFinite(opts.ioTimeoutMs) ? opts.ioTimeoutMs : DEFAULT_IO_TIMEOUT_MS;
|
|
76
|
+
const resolved = await resolveFileInDir(segmentPath, timeoutMs);
|
|
77
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason };
|
|
78
|
+
|
|
79
|
+
let buf;
|
|
80
|
+
try {
|
|
81
|
+
buf = await withTimeout(fs.promises.readFile(resolved.path), timeoutMs);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return { ok: false, reason: ioReason(err) };
|
|
84
|
+
}
|
|
85
|
+
if (buf.length === 0) {
|
|
86
|
+
return { ok: true, recovered: false, droppedBytes: 0, droppedLines: 0 };
|
|
87
|
+
}
|
|
88
|
+
if (buf[buf.length - 1] === 0x0a) {
|
|
89
|
+
return { ok: true, recovered: false, droppedBytes: 0, droppedLines: 0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const lastNl = buf.lastIndexOf(0x0a);
|
|
93
|
+
const keep = lastNl + 1; // 0 when the whole file is one partial row
|
|
94
|
+
const dropped = buf.subarray(keep);
|
|
95
|
+
const droppedLines = dropped.toString('utf8').split('\n').filter((s) => s.length > 0).length;
|
|
96
|
+
try {
|
|
97
|
+
await withTimeout(fs.promises.truncate(resolved.path, keep), timeoutMs);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return { ok: false, reason: ioReason(err) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Mark recovery in the sidecar meta. If a checksum exists it is recomputed
|
|
103
|
+
// over the truncated bytes — a stale checksum would falsify the segment.
|
|
104
|
+
const metaPath = metaPathFor(resolved.path);
|
|
105
|
+
let meta = {};
|
|
106
|
+
try {
|
|
107
|
+
meta = JSON.parse(await withTimeout(fs.promises.readFile(metaPath, 'utf8'), timeoutMs));
|
|
108
|
+
} catch {
|
|
109
|
+
meta = {};
|
|
110
|
+
}
|
|
111
|
+
meta.recovered = true;
|
|
112
|
+
meta.recovered_dropped_bytes = dropped.length;
|
|
113
|
+
if (typeof meta.checksum === 'string') {
|
|
114
|
+
meta.checksum = sha256Buffer(buf.subarray(0, keep));
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await withTimeout(fs.promises.writeFile(metaPath, JSON.stringify(meta, null, 2)), timeoutMs);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return { ok: false, reason: ioReason(err) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { ok: true, recovered: true, droppedBytes: dropped.length, droppedLines };
|
|
123
|
+
}
|