@getbourdon/participants 0.2.0 → 0.2.1

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.
@@ -1,163 +0,0 @@
1
- /**
2
- * Shared read-only SQLite + normalization helpers — port of
3
- * `participants/_sqlite_base.py`.
4
- *
5
- * A SQLite-backed participant (hermes today; cursor/copilot_cli in the follow-on
6
- * slice) reads a foreign agent's native DB read-only so a live writer's lock can
7
- * never block the export, probes the schema defensively (native schemas drift
8
- * across versions), and turns epoch timestamps + paths into the L5 shapes.
9
- *
10
- * `better-sqlite3` is a NATIVE addon. It is loaded lazily through `createRequire`
11
- * inside a try/catch so this package — and the file/network readers — keep
12
- * working even on a toolchain where the addon failed to build: in that case
13
- * {@link sqliteAvailable} returns false and the sqlite readers degrade rather
14
- * than crashing import. Nothing here applies visibility or builds manifests;
15
- * that stays in the participant (per PARTICIPANT_CONTRACT.md).
16
- */
17
- import { statSync } from "node:fs";
18
- import { createRequire } from "node:module";
19
- const moduleUrl = (typeof import.meta !== "undefined" && import.meta.url) ||
20
- (typeof __filename !== "undefined" ? __filename : process.cwd());
21
- const nodeRequire = createRequire(moduleUrl);
22
- let driver;
23
- /** Lazily resolve the better-sqlite3 constructor, or null if the addon is
24
- * unavailable (failed native build). Cached after the first attempt. */
25
- function loadDriver() {
26
- if (driver !== undefined)
27
- return driver;
28
- try {
29
- driver = nodeRequire("better-sqlite3");
30
- }
31
- catch {
32
- driver = null;
33
- }
34
- return driver;
35
- }
36
- /** True when the better-sqlite3 native addon loaded successfully. Tests gate
37
- * the SQLite-reader assertions on this so a missing addon skips, not fails. */
38
- export function sqliteAvailable() {
39
- return loadDriver() !== null;
40
- }
41
- // -- Read-only connection -----------------------------------------------------
42
- /**
43
- * Open a SQLite DB read-only so a live writer is never disturbed. The default
44
- * (`immutable` omitted) is the safe `mode=ro`: it respects SQLite locking and
45
- * reads the `-wal` sidecar of a live, actively-writing agent. Raises on a
46
- * genuine open failure; callers wanting the never-raises contract use
47
- * {@link tryOpenReadonly}.
48
- */
49
- export function openReadonly(dbPath, opts = {}) {
50
- const Ctor = loadDriver();
51
- if (!Ctor) {
52
- throw new Error("better-sqlite3 native addon is not available");
53
- }
54
- // better-sqlite3 maps `{readonly:true}` to SQLITE_OPEN_READONLY. There is no
55
- // first-class `immutable=1` knob; for our static snapshots `readonly` is
56
- // sufficient (we never open a concurrently-written fixture DB).
57
- return new Ctor(dbPath, {
58
- readonly: true,
59
- fileMustExist: true,
60
- timeout: opts.timeoutMs ?? 2000,
61
- });
62
- }
63
- /**
64
- * Like {@link openReadonly} but returns null instead of raising — for the
65
- * export/health path where a missing or corrupt DB must degrade to "no rows".
66
- */
67
- export function tryOpenReadonly(dbPath, opts = {}) {
68
- try {
69
- if (!statSync(dbPath).isFile())
70
- return null;
71
- }
72
- catch {
73
- return null;
74
- }
75
- try {
76
- return openReadonly(dbPath, opts);
77
- }
78
- catch {
79
- return null;
80
- }
81
- }
82
- // -- Defensive schema probes --------------------------------------------------
83
- /** True if `name` is a table in the connected DB (swallows errors → false). */
84
- export function tableExists(db, name) {
85
- try {
86
- const row = db
87
- .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1")
88
- .get(name);
89
- return row !== undefined;
90
- }
91
- catch {
92
- return false;
93
- }
94
- }
95
- /** Column names for `table` (empty set if absent / on error). PRAGMA cannot be
96
- * parameterized, matching the Python f-string. */
97
- export function tableColumns(db, table) {
98
- try {
99
- const rows = db.prepare(`PRAGMA table_info(${table})`).all();
100
- return new Set(rows.map((r) => r.name));
101
- }
102
- catch {
103
- return new Set();
104
- }
105
- }
106
- // -- Normalization ------------------------------------------------------------
107
- function toFiniteNumber(value) {
108
- if (value === null || value === undefined)
109
- return null;
110
- const n = typeof value === "number" ? value : Number(value);
111
- return Number.isFinite(n) ? n : null;
112
- }
113
- /**
114
- * Convert epoch seconds (number/string) to an ISO `YYYY-MM-DD` UTC date, or
115
- * null for missing/unparseable values. Mirrors `epoch_to_iso_date`.
116
- */
117
- export function epochToIsoDate(value) {
118
- const n = toFiniteNumber(value);
119
- if (n === null)
120
- return null;
121
- const d = new Date(n * 1000);
122
- if (Number.isNaN(d.getTime()))
123
- return null;
124
- return d.toISOString().slice(0, 10);
125
- }
126
- /**
127
- * Convert epoch seconds to a full ISO 8601 UTC datetime, or null. The offset is
128
- * rendered `+00:00` (Python `datetime(...).isoformat()` style) rather than `Z`.
129
- */
130
- export function epochToIsoDatetime(value) {
131
- const n = toFiniteNumber(value);
132
- if (n === null)
133
- return null;
134
- const d = new Date(n * 1000);
135
- if (Number.isNaN(d.getTime()))
136
- return null;
137
- return d.toISOString().replace(/\.\d{3}Z$/, "+00:00").replace(/Z$/, "+00:00");
138
- }
139
- function basenameOf(p) {
140
- const trimmed = p.replace(/[\\/]+$/, "");
141
- const parts = trimmed.split(/[\\/]/);
142
- return parts[parts.length - 1] ?? "";
143
- }
144
- /**
145
- * Derive a lowercase project key from a working-directory path: the basename
146
- * lowercased, or null when the cwd is empty or its basename is "generic"
147
- * (carries no project identity). Mirrors `project_key_from_cwd`.
148
- */
149
- export function projectKeyFromCwd(cwd, genericNames = new Set()) {
150
- if (!cwd)
151
- return null;
152
- const name = basenameOf(cwd).trim().toLowerCase();
153
- if (!name || genericNames.has(name))
154
- return null;
155
- return name;
156
- }
157
- /** Turn a slug-ish key (`my-cool_project`) into a Title Case label. */
158
- export function friendlyLabel(key) {
159
- return key
160
- .replace(/[-_]+/g, " ")
161
- .trim()
162
- .replace(/\S+/g, (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
163
- }