@tekyzinc/gsd-t 4.13.11 → 4.14.10
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 +23 -0
- package/README.md +1 -1
- package/bin/gsd-t-graph-freshness.cjs +8 -4
- package/bin/gsd-t-graph-index.cjs +7 -2
- package/bin/gsd-t-graph-k1-sqlite-stream.cjs +1 -1
- package/bin/gsd-t-graph-metrics-rollup.cjs +527 -0
- package/bin/gsd-t-graph-query-cli.cjs +131 -31
- package/bin/gsd-t-graph-scip-upgrade.cjs +14 -5
- package/bin/gsd-t-graph-store-bakeoff.cjs +1 -1
- package/bin/gsd-t-graph-store-resolver.cjs +484 -0
- package/bin/gsd-t-verify-gate.cjs +9 -2
- package/bin/gsd-t.js +46 -1
- package/package.json +1 -1
- package/scripts/gsd-t-graph-intercept.js +89 -8
- package/scripts/gsd-t-read-intercept.js +87 -18
- package/templates/workflows/gsd-t-debug.workflow.js +19 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +19 -0
- package/templates/workflows/gsd-t-phase.workflow.js +19 -0
- package/templates/workflows/gsd-t-quick.workflow.js +19 -0
- package/templates/workflows/gsd-t-scan.workflow.js +78 -22
- package/templates/workflows/gsd-t-verify.workflow.js +19 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gsd-t-graph-store-resolver — M99 D1 (single path resolver + migration shim + sink)
|
|
6
|
+
*
|
|
7
|
+
* THE ONLY place a graph store path is derived after M99.
|
|
8
|
+
* D2 and D3 import from this module; they never re-derive a path or contain
|
|
9
|
+
* a raw `.gsd-t/graph.db` / `.gsd-t/graphDB/` literal.
|
|
10
|
+
*
|
|
11
|
+
* Prevents the M96-class silent split-brain (two code paths disagreeing on
|
|
12
|
+
* where the store lives).
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* resolveGraphDir(projectRoot?) → abs path to `.gsd-t/graphDB/`
|
|
16
|
+
* resolveStorePath(projectRoot?) → abs path to `.gsd-t/graphDB/graph.db`
|
|
17
|
+
* resolveLogsDir(projectRoot?) → abs path to `.gsd-t/graphDB/logs/`
|
|
18
|
+
* deriveProjectRoot(storePath) → repo root (3-up from graphDB/graph.db; 2-up from JSONL)
|
|
19
|
+
* migrateGraphStore(projectRoot?) → copy-verify-swap shim (idempotent, interruption-safe)
|
|
20
|
+
* append_ledger_line(record) → fail-open ledger append with rotation + toggle
|
|
21
|
+
*
|
|
22
|
+
* [RULE] one-resolver-only
|
|
23
|
+
* [RULE] projectroot-depth-corrected-with-move
|
|
24
|
+
* [RULE] jsonl-branch-depth-preserved
|
|
25
|
+
* [RULE] copy-verify-swap-never-orphan
|
|
26
|
+
* [RULE] migration-real-root-only
|
|
27
|
+
* [RULE] fail-open-telemetry
|
|
28
|
+
* [RULE] layer1-shape-kept
|
|
29
|
+
* [RULE] rollover-boundary-proven
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const fs = require("node:fs");
|
|
33
|
+
const path = require("node:path");
|
|
34
|
+
const os = require("node:os");
|
|
35
|
+
|
|
36
|
+
// ─── Legacy store path constant (migration shim uses this explicitly) ─────────
|
|
37
|
+
// ONLY this module is allowed to reference the old path. All producers route
|
|
38
|
+
// through resolveStorePath(). Exported so the query-cli discovery loop can
|
|
39
|
+
// check for the legacy file without re-deriving the literal.
|
|
40
|
+
const LEGACY_DB_SUBPATH = path.join(".gsd-t", "graph.db"); // spike-local-store: legacy path reference; migration shim only
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return the absolute legacy store path (pre-M99: .gsd-t/graph.db).
|
|
44
|
+
* Used ONLY for self-heal detection in the discovery loop.
|
|
45
|
+
* @param {string} dir — the directory to check in
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function resolveLegacyStorePath(dir) {
|
|
49
|
+
return path.join(dir, LEGACY_DB_SUBPATH);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── New store layout ─────────────────────────────────────────────────────────
|
|
53
|
+
const GRAPH_DIR_NAME = "graphDB";
|
|
54
|
+
const DB_FILENAME = "graph.db";
|
|
55
|
+
const LOGS_DIR_NAME = "logs";
|
|
56
|
+
|
|
57
|
+
// ─── Rotation defaults (production) ──────────────────────────────────────────
|
|
58
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
59
|
+
const DEFAULT_MAX_ENTRIES = 250_000;
|
|
60
|
+
|
|
61
|
+
// ─── Real-root guard constants ────────────────────────────────────────────────
|
|
62
|
+
// Refuse to migrate inside tmp dirs, the filesystem root, or the home dir.
|
|
63
|
+
const HOME_DIR = os.homedir();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the project root.
|
|
67
|
+
* When no explicit root is given, walks up from cwd looking for `.gsd-t/`.
|
|
68
|
+
* Falls back to cwd if no `.gsd-t/` is found.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} [explicitRoot]
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function _findProjectRoot(explicitRoot) {
|
|
74
|
+
if (explicitRoot) return explicitRoot;
|
|
75
|
+
let dir = process.cwd();
|
|
76
|
+
for (let i = 0; i < 12; i++) {
|
|
77
|
+
if (fs.existsSync(path.join(dir, ".gsd-t"))) return dir;
|
|
78
|
+
const parent = path.dirname(dir);
|
|
79
|
+
if (parent === dir) break;
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
return process.cwd();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── T1: Core path resolution ─────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Absolute path to the graphDB directory: `<projectRoot>/.gsd-t/graphDB/`
|
|
89
|
+
* @param {string} [projectRoot]
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function resolveGraphDir(projectRoot) {
|
|
93
|
+
return path.join(_findProjectRoot(projectRoot), ".gsd-t", GRAPH_DIR_NAME);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Absolute path to the SQLite store: `<projectRoot>/.gsd-t/graphDB/graph.db`
|
|
98
|
+
* @param {string} [projectRoot]
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function resolveStorePath(projectRoot) {
|
|
102
|
+
return path.join(resolveGraphDir(projectRoot), DB_FILENAME);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Absolute path to the logs directory: `<projectRoot>/.gsd-t/graphDB/logs/`
|
|
107
|
+
* @param {string} [projectRoot]
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
function resolveLogsDir(projectRoot) {
|
|
111
|
+
return path.join(resolveGraphDir(projectRoot), LOGS_DIR_NAME);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Absolute path to a SCIP index file under graphDB/.
|
|
116
|
+
* Red Team HIGH (M99 round 4): SC#1 requires ALL graph artifacts — incl.
|
|
117
|
+
* index.scip / index-python.scip — under `.gsd-t/graphDB/`, but the SCIP
|
|
118
|
+
* upgrader wrote them to `.gsd-t/` via hardcoded literals. Route them here.
|
|
119
|
+
* [RULE] scip-index-under-graphdb
|
|
120
|
+
* @param {string} filename - e.g. "index.scip" | "index-python.scip"
|
|
121
|
+
* @param {string} [projectRoot]
|
|
122
|
+
* @returns {string} `<projectRoot>/.gsd-t/graphDB/<filename>`
|
|
123
|
+
*/
|
|
124
|
+
function resolveScipPath(filename, projectRoot) {
|
|
125
|
+
return path.join(resolveGraphDir(projectRoot), filename);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Derive the project root from a known store path.
|
|
130
|
+
*
|
|
131
|
+
* Two branches — DIFFERENT depths post-migration:
|
|
132
|
+
* `.gsd-t/graphDB/graph.db` → 3 levels up (graphDB/ adds one level vs. old 2-up)
|
|
133
|
+
* `.gsd-t/graph-index/` → 2 levels up (JSONL branch stays at 2-up)
|
|
134
|
+
*
|
|
135
|
+
* [RULE] projectroot-depth-corrected-with-move
|
|
136
|
+
* [RULE] jsonl-branch-depth-preserved
|
|
137
|
+
*
|
|
138
|
+
* @param {string} storePath — abs path to store (either `.db` file or `graph-index/` dir)
|
|
139
|
+
* @returns {string} — absolute path to project root
|
|
140
|
+
*/
|
|
141
|
+
function deriveProjectRoot(storePath) {
|
|
142
|
+
if (typeof storePath !== "string") return process.cwd();
|
|
143
|
+
|
|
144
|
+
// New graphDB branch: .gsd-t/graphDB/graph.db → 3 levels up
|
|
145
|
+
if (storePath.endsWith(path.join("graphDB", "graph.db")) || storePath.endsWith("graphDB/graph.db")) {
|
|
146
|
+
return path.dirname(path.dirname(path.dirname(storePath)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Legacy SQLite: .gsd-t/graph.db → 2 levels up
|
|
150
|
+
if (storePath.endsWith(".db")) {
|
|
151
|
+
return path.dirname(path.dirname(storePath));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// JSONL directory branch: .gsd-t/graph-index/ → 2 levels up
|
|
155
|
+
return path.dirname(path.dirname(storePath));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── T2: copy-verify-swap migration shim ─────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Real-root guard — refuse to migrate inside tmp, filesystem root, or home.
|
|
162
|
+
* (M94 lesson: fake root → walkTree walked whole disk → OOM)
|
|
163
|
+
*
|
|
164
|
+
* @param {string} root
|
|
165
|
+
* @returns {boolean} true if this looks like a safe real project root
|
|
166
|
+
*/
|
|
167
|
+
function _isRealProjectRoot(root) {
|
|
168
|
+
const abs = path.resolve(root);
|
|
169
|
+
if (abs === "/" || abs === HOME_DIR) return false;
|
|
170
|
+
// Reject well-known temp patterns
|
|
171
|
+
const tmp = os.tmpdir();
|
|
172
|
+
if (abs.startsWith(tmp + path.sep) || abs === tmp) return false;
|
|
173
|
+
// Also reject macOS-style /private/tmp and /var/folders
|
|
174
|
+
if (abs.startsWith("/private/tmp") || abs.startsWith("/var/folders")) return false;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Verify that a SQLite file at `dbPath` is readable (can open + query).
|
|
180
|
+
*
|
|
181
|
+
* @param {string} dbPath
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
function _verifySqliteReadable(dbPath) {
|
|
185
|
+
try {
|
|
186
|
+
const { requireBetterSqlite } = require("./gsd-t-require-store.cjs");
|
|
187
|
+
const Database = requireBetterSqlite();
|
|
188
|
+
const db = new Database(dbPath, { readonly: true });
|
|
189
|
+
// Quick sanity: try to read the schema
|
|
190
|
+
db.prepare("SELECT count(*) FROM sqlite_master").get();
|
|
191
|
+
db.close();
|
|
192
|
+
return true;
|
|
193
|
+
} catch (_e) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Checkpoint a SQLite store's write-ahead log INTO its main .db file, then
|
|
200
|
+
* truncate the WAL — so the .db becomes fully self-contained (no -wal/-shm
|
|
201
|
+
* dependency). Best-effort: returns true on success, false on any failure
|
|
202
|
+
* (caller falls back to copying the db+wal+shm triple).
|
|
203
|
+
*
|
|
204
|
+
* WHY (Red Team HIGH, M99): the migration's STEP-3 renamed graph.db BEFORE its
|
|
205
|
+
* -wal/-shm. A kill in that window left graph.db present but its -wal orphaned →
|
|
206
|
+
* SQLite reopened at the last checkpoint and SILENTLY dropped uncheckpointed rows
|
|
207
|
+
* (reproduced 4000→2000). Checkpointing the WAL into the .db BEFORE the copy means
|
|
208
|
+
* there is no sidecar to orphan: the single self-contained .db rename is genuinely
|
|
209
|
+
* atomic. WAL is the normal live state, so this path is the common case.
|
|
210
|
+
* [RULE] migration-wal-checkpoint-before-copy
|
|
211
|
+
*
|
|
212
|
+
* @param {string} dbPath
|
|
213
|
+
* @returns {boolean} true if the WAL was checkpointed + truncated
|
|
214
|
+
*/
|
|
215
|
+
function _checkpointWal(dbPath) {
|
|
216
|
+
try {
|
|
217
|
+
const { requireBetterSqlite } = require("./gsd-t-require-store.cjs");
|
|
218
|
+
const Database = requireBetterSqlite();
|
|
219
|
+
const db = new Database(dbPath); // writable — checkpoint mutates the .db
|
|
220
|
+
// TRUNCATE: fold all WAL frames into the .db AND reset the -wal to zero length.
|
|
221
|
+
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
222
|
+
db.close();
|
|
223
|
+
return true;
|
|
224
|
+
} catch (_e) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* WAL-safe copy: copies `src.db`, `src.db-wal`, and `src.db-shm` to the
|
|
231
|
+
* target directory atomically (all three must exist for a consistent snapshot).
|
|
232
|
+
*
|
|
233
|
+
* Strategy: copy all three files. A missing -wal / -shm is fine (WAL may be
|
|
234
|
+
* checkpointed). We copy the triple so any uncommitted WAL entries travel with
|
|
235
|
+
* the main db file (pre-mortem #6: WAL-interruption safety).
|
|
236
|
+
*
|
|
237
|
+
* @param {string} srcDb — absolute path to source `.db` file
|
|
238
|
+
* @param {string} dstDir — absolute path to destination directory (must exist)
|
|
239
|
+
* @returns {string} — absolute path to the copied `.db` file
|
|
240
|
+
*/
|
|
241
|
+
function _copyDbWithWal(srcDb, dstDir) {
|
|
242
|
+
const basename = path.basename(srcDb);
|
|
243
|
+
const dstDb = path.join(dstDir, basename);
|
|
244
|
+
fs.copyFileSync(srcDb, dstDb);
|
|
245
|
+
for (const suf of ["-wal", "-shm"]) {
|
|
246
|
+
const walSrc = srcDb + suf;
|
|
247
|
+
if (fs.existsSync(walSrc)) {
|
|
248
|
+
fs.copyFileSync(walSrc, path.join(dstDir, basename + suf));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return dstDb;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* migrateGraphStore — copy-verify-swap from `.gsd-t/graph.db` → `.gsd-t/graphDB/graph.db`
|
|
256
|
+
*
|
|
257
|
+
* Invariants:
|
|
258
|
+
* 1. Idempotent — second run returns `{ migrated: false, reason: 'already-migrated' }`.
|
|
259
|
+
* 2. Interruption-safe — at EVERY point either the old store OR the new store is readable.
|
|
260
|
+
* We retain the old file until the new one is verified readable, then swap (move).
|
|
261
|
+
* 3. Real-root-only — refuses to run inside tmpdir / home / fs-root when called without
|
|
262
|
+
* explicit projectRoot (the auto-fire/self-heal case). Callers that pass an explicit
|
|
263
|
+
* projectRoot (tests, CPUA) opt in to bypass the guard via `{ forceAllow: true }`.
|
|
264
|
+
* 4. No-op when the legacy file does not exist.
|
|
265
|
+
*
|
|
266
|
+
* [RULE] copy-verify-swap-never-orphan
|
|
267
|
+
* [RULE] migration-real-root-only
|
|
268
|
+
* [RULE] discovery-loop-end-to-end
|
|
269
|
+
*
|
|
270
|
+
* @param {string} [projectRoot]
|
|
271
|
+
* @param {{ forceAllow?: boolean }} [opts] — pass `{ forceAllow: true }` to bypass real-root guard (test opt-in)
|
|
272
|
+
* @returns {{ migrated: boolean, reason: string }}
|
|
273
|
+
*/
|
|
274
|
+
function migrateGraphStore(projectRoot, opts) {
|
|
275
|
+
const root = _findProjectRoot(projectRoot);
|
|
276
|
+
const forceAllow = opts && opts.forceAllow === true;
|
|
277
|
+
|
|
278
|
+
// Real-root guard (M94 lesson): only applies when NOT force-allowed
|
|
279
|
+
if (!forceAllow && !_isRealProjectRoot(root)) {
|
|
280
|
+
return { migrated: false, reason: "real-root-guard: not a real project root" };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const legacyPath = path.join(root, LEGACY_DB_SUBPATH);
|
|
284
|
+
const targetDir = resolveGraphDir(root);
|
|
285
|
+
const targetPath = resolveStorePath(root);
|
|
286
|
+
|
|
287
|
+
// Idempotent: already migrated
|
|
288
|
+
if (fs.existsSync(targetPath) && _verifySqliteReadable(targetPath)) {
|
|
289
|
+
return { migrated: false, reason: "already-migrated" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Nothing to migrate
|
|
293
|
+
if (!fs.existsSync(legacyPath)) {
|
|
294
|
+
return { migrated: false, reason: "no-legacy-store" };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Ensure target directory exists
|
|
298
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
299
|
+
|
|
300
|
+
// STEP 1: Copy legacy store (+ WAL/SHM) to a temp file inside the target dir.
|
|
301
|
+
// We use a temp name so a crash here leaves the legacy store untouched.
|
|
302
|
+
const tmpDb = path.join(targetDir, "graph.db.migrating");
|
|
303
|
+
const tmpWal = tmpDb + "-wal";
|
|
304
|
+
const tmpShm = tmpDb + "-shm";
|
|
305
|
+
|
|
306
|
+
// Clean any previous partial migration
|
|
307
|
+
for (const f of [tmpDb, tmpWal, tmpShm]) {
|
|
308
|
+
try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch {}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// STEP 0 (Red Team HIGH fix): checkpoint the legacy WAL INTO graph.db first, so
|
|
312
|
+
// the .db is fully self-contained before we copy. Then a single self-contained
|
|
313
|
+
// .db rename has no -wal/-shm to orphan if interrupted mid-swap (the old STEP-3
|
|
314
|
+
// window that silently dropped uncheckpointed rows, 4000→2000). [RULE]
|
|
315
|
+
// migration-wal-checkpoint-before-copy. Best-effort: if checkpoint fails (e.g.
|
|
316
|
+
// a concurrent writer holds the lock), we STILL copy the db+wal+shm triple below
|
|
317
|
+
// so a clean migration remains WAL-complete — the interruption window only narrows
|
|
318
|
+
// when the checkpoint succeeded (the normal case), never widens.
|
|
319
|
+
const checkpointed = _checkpointWal(legacyPath);
|
|
320
|
+
|
|
321
|
+
// Copy the .db (now self-contained if checkpointed). Also copy any remaining
|
|
322
|
+
// -wal/-shm: harmless when checkpointed (the -wal is zero-length post-TRUNCATE),
|
|
323
|
+
// and the safety net when the checkpoint could not run.
|
|
324
|
+
fs.copyFileSync(legacyPath, tmpDb);
|
|
325
|
+
for (const suf of ["-wal", "-shm"]) {
|
|
326
|
+
const src = legacyPath + suf;
|
|
327
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, tmpDb + suf);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// STEP 2: Verify the copy is readable BEFORE committing the rename.
|
|
331
|
+
// If this throws / returns false, the legacy store is still intact.
|
|
332
|
+
if (!_verifySqliteReadable(tmpDb)) {
|
|
333
|
+
// Clean up and bail — old store still intact
|
|
334
|
+
for (const f of [tmpDb, tmpWal, tmpShm]) {
|
|
335
|
+
try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch {}
|
|
336
|
+
}
|
|
337
|
+
return { migrated: false, reason: "verify-failed: copy not readable" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// STEP 3: Atomic rename (swap) — after this, the new path is live.
|
|
341
|
+
// The old store remains for now (we delete it after confirming).
|
|
342
|
+
// Node rename is atomic on most filesystems within the same mountpoint.
|
|
343
|
+
//
|
|
344
|
+
// ORDER MATTERS (Red Team HIGH defense-in-depth): rename the -wal/-shm sidecars
|
|
345
|
+
// BEFORE the main .db. If the checkpoint above succeeded the sidecars are empty
|
|
346
|
+
// and order is moot; but if it could NOT run, a kill between renames must never
|
|
347
|
+
// leave graph.db present without its matching -wal (that orphans uncheckpointed
|
|
348
|
+
// rows). Sidecars-first means an interrupted swap leaves EITHER no new .db (old
|
|
349
|
+
// store still the only live one — self-heal re-runs) OR a complete .db+wal pair,
|
|
350
|
+
// never a .db missing its wal. [RULE] migration-sidecars-renamed-before-db
|
|
351
|
+
for (const suf of ["-wal", "-shm"]) {
|
|
352
|
+
const tmp = tmpDb + suf;
|
|
353
|
+
if (fs.existsSync(tmp)) {
|
|
354
|
+
try { fs.renameSync(tmp, targetPath + suf); } catch {}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
fs.renameSync(tmpDb, targetPath);
|
|
358
|
+
|
|
359
|
+
// STEP 4: Final verify of the swapped-in store
|
|
360
|
+
if (!_verifySqliteReadable(targetPath)) {
|
|
361
|
+
// Undo: move back if possible (the legacy is still there)
|
|
362
|
+
try { fs.renameSync(targetPath, legacyPath + ".restore"); } catch {}
|
|
363
|
+
return { migrated: false, reason: "post-swap-verify-failed" };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// SUCCESS — old store can be removed (but we leave it for now; a future
|
|
367
|
+
// cleanup pass can remove it once users confirm the migration is stable).
|
|
368
|
+
// We do NOT delete legacyPath here to honour the "never-orphan" invariant
|
|
369
|
+
// in case of any partial filesystem issue. The no-raw-literals test enforces
|
|
370
|
+
// that only the resolver looks at the legacy path.
|
|
371
|
+
|
|
372
|
+
return { migrated: true, reason: "copy-verify-swap complete" };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ─── T3: append_ledger_line — Layer-1 telemetry sink ─────────────────────────
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Find the current ledger file path, handling rotation.
|
|
379
|
+
*
|
|
380
|
+
* File naming: `graph-events-001.jsonl`, `graph-events-002.jsonl`, ...
|
|
381
|
+
*
|
|
382
|
+
* Rotation backstop: when the current file exceeds maxBytes OR maxEntries,
|
|
383
|
+
* we seal it and start the next one. This is a RUNAWAY backstop (a full
|
|
384
|
+
* analysis session should fit in one file); routine rotation does NOT happen.
|
|
385
|
+
*
|
|
386
|
+
* [RULE] rollover-boundary-proven
|
|
387
|
+
*
|
|
388
|
+
* @param {string} logsDir
|
|
389
|
+
* @param {number} maxBytes
|
|
390
|
+
* @param {number} maxEntries
|
|
391
|
+
* @returns {string} — absolute path to the active ledger file
|
|
392
|
+
*/
|
|
393
|
+
function _resolveLedgerPath(logsDir, maxBytes, maxEntries) {
|
|
394
|
+
// List existing ledger files, sorted ascending
|
|
395
|
+
let files = [];
|
|
396
|
+
try {
|
|
397
|
+
files = fs.readdirSync(logsDir)
|
|
398
|
+
.filter((f) => /^graph-events-\d+\.jsonl$/.test(f))
|
|
399
|
+
.sort();
|
|
400
|
+
} catch (_e) {
|
|
401
|
+
// logsDir does not exist yet — will be created at write time
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (files.length === 0) {
|
|
405
|
+
return path.join(logsDir, "graph-events-001.jsonl");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const current = path.join(logsDir, files[files.length - 1]);
|
|
409
|
+
|
|
410
|
+
// Check size and entry count of current file
|
|
411
|
+
let tooBig = false;
|
|
412
|
+
let tooMany = false;
|
|
413
|
+
try {
|
|
414
|
+
const stat = fs.statSync(current);
|
|
415
|
+
if (stat.size >= maxBytes) tooBig = true;
|
|
416
|
+
} catch (_e) { /* not exists yet — ok */ }
|
|
417
|
+
|
|
418
|
+
if (!tooBig) {
|
|
419
|
+
try {
|
|
420
|
+
const content = fs.readFileSync(current, "utf8");
|
|
421
|
+
const entryCount = content.split("\n").filter((l) => l.trim().length > 0).length;
|
|
422
|
+
if (entryCount >= maxEntries) tooMany = true;
|
|
423
|
+
} catch (_e) { /* ok */ }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!tooBig && !tooMany) return current;
|
|
427
|
+
|
|
428
|
+
// Need a new file: increment the sequence number
|
|
429
|
+
const lastFile = files[files.length - 1];
|
|
430
|
+
const match = lastFile.match(/^graph-events-(\d+)\.jsonl$/);
|
|
431
|
+
const nextNum = match ? parseInt(match[1], 10) + 1 : 1;
|
|
432
|
+
const nextName = `graph-events-${String(nextNum).padStart(3, "0")}.jsonl`;
|
|
433
|
+
return path.join(logsDir, nextName);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* append_ledger_line — fail-open ledger append.
|
|
438
|
+
*
|
|
439
|
+
* Writes one JSON line to `graphDB/logs/graph-events-NNN.jsonl`.
|
|
440
|
+
* Honors `GSDT_GRAPH_TELEMETRY`: default ON; `"0"` → OFF (zero lines written).
|
|
441
|
+
* Sized rotation backstop: 50 MB OR 250,000 entries (test-overridable via env vars).
|
|
442
|
+
* A throw inside the sink is SWALLOWED and never propagates.
|
|
443
|
+
*
|
|
444
|
+
* [RULE] fail-open-telemetry
|
|
445
|
+
* [RULE] rollover-boundary-proven
|
|
446
|
+
*
|
|
447
|
+
* @param {object} record
|
|
448
|
+
* @param {string} [projectRoot] — optional override for tests
|
|
449
|
+
*/
|
|
450
|
+
function append_ledger_line(record, projectRoot) {
|
|
451
|
+
// Toggle: default ON; "0" → OFF
|
|
452
|
+
if (process.env.GSDT_GRAPH_TELEMETRY === "0") return;
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const logsDir = resolveLogsDir(projectRoot);
|
|
456
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
457
|
+
|
|
458
|
+
// Test-only threshold overrides (pre-mortem #5)
|
|
459
|
+
const maxBytes = process.env.GSDT_GRAPH_TELEMETRY_MAXBYTES
|
|
460
|
+
? parseInt(process.env.GSDT_GRAPH_TELEMETRY_MAXBYTES, 10)
|
|
461
|
+
: DEFAULT_MAX_BYTES;
|
|
462
|
+
const maxEntries = process.env.GSDT_GRAPH_TELEMETRY_MAXENTRIES
|
|
463
|
+
? parseInt(process.env.GSDT_GRAPH_TELEMETRY_MAXENTRIES, 10)
|
|
464
|
+
: DEFAULT_MAX_ENTRIES;
|
|
465
|
+
|
|
466
|
+
const ledgerPath = _resolveLedgerPath(logsDir, maxBytes, maxEntries);
|
|
467
|
+
fs.appendFileSync(ledgerPath, JSON.stringify(record) + "\n");
|
|
468
|
+
} catch (_e) {
|
|
469
|
+
// FAIL-OPEN: telemetry must never block or alter any graph/grep/read decision
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
module.exports = {
|
|
476
|
+
resolveGraphDir,
|
|
477
|
+
resolveStorePath,
|
|
478
|
+
resolveLogsDir,
|
|
479
|
+
resolveScipPath,
|
|
480
|
+
resolveLegacyStorePath,
|
|
481
|
+
deriveProjectRoot,
|
|
482
|
+
migrateGraphStore,
|
|
483
|
+
append_ledger_line,
|
|
484
|
+
};
|
|
@@ -220,8 +220,15 @@ function _detectDefaultTrack2(projectDir, notes) {
|
|
|
220
220
|
try { return fs.existsSync(path.join(projectDir, rel)); } catch (_) { return false; }
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
-
// typecheck — tsc
|
|
224
|
-
|
|
223
|
+
// typecheck — tsc. Run ONLY when tsc is actually INSTALLED (the binary exists).
|
|
224
|
+
// A bare `tsconfig.json` is NOT sufficient: scip-typescript auto-creates an empty
|
|
225
|
+
// `{}` tsconfig at the root of any project it indexes (including zero-dep plain-JS
|
|
226
|
+
// repos like GSD-T itself). The old `has('tsconfig.json')` OR-trigger then ran
|
|
227
|
+
// `npx --no-install tsc` with no tsc present → "TypeScript not installed" → a
|
|
228
|
+
// FALSE verify-gate FAIL on a non-TS project. A genuine TS project ships tsc in
|
|
229
|
+
// node_modules/.bin/, so requiring the binary cannot disable typecheck where it
|
|
230
|
+
// truly applies. (We still require a tsconfig so tsc has a config to read.)
|
|
231
|
+
if (has('node_modules/.bin/tsc') && has('tsconfig.json')) {
|
|
225
232
|
plan.push({
|
|
226
233
|
id: 'tsc',
|
|
227
234
|
cmd: 'npx',
|
package/bin/gsd-t.js
CHANGED
|
@@ -3882,9 +3882,54 @@ function doGraph(args) {
|
|
|
3882
3882
|
case "blast-radius": { const e = _graphQueryCli(["blast-radius", args[1] || ""]); log(JSON.stringify(e, null, 2)); break; }
|
|
3883
3883
|
case "body": { const e = _graphQueryCli(["body", args[1] || ""]); log(JSON.stringify(e, null, 2)); break; }
|
|
3884
3884
|
case "tasks": doGraphTaskOutput(args[1] || "table"); break;
|
|
3885
|
+
case "metrics": { // M99 D3-T2: append-only arm — rollup the telemetry ledger
|
|
3886
|
+
const _metricsRollup = require("./gsd-t-graph-metrics-rollup.cjs");
|
|
3887
|
+
const _metricsJson = (args[1] === "--json" || args[2] === "--json");
|
|
3888
|
+
_metricsRollup.printRollup(undefined, { json: _metricsJson });
|
|
3889
|
+
break;
|
|
3890
|
+
}
|
|
3891
|
+
case "wiring-log": {
|
|
3892
|
+
// M99 D2: thin CLI shim for persisting a kind:'wiring' ledger line from a
|
|
3893
|
+
// Workflow agent() Bash call (avoids embedding require() in workflow strings,
|
|
3894
|
+
// which trips the M71 sandbox lint).
|
|
3895
|
+
//
|
|
3896
|
+
// Usage A (explicit mode):
|
|
3897
|
+
// gsd-t graph wiring-log --consumer <consumer> --mode <mode> [--project <dir>]
|
|
3898
|
+
//
|
|
3899
|
+
// Usage B (auto mode — WIRED if store exists, else fallback-announced):
|
|
3900
|
+
// gsd-t graph wiring-log --consumer <consumer> --auto [--project <dir>]
|
|
3901
|
+
//
|
|
3902
|
+
// Exits 0 always (fail-open).
|
|
3903
|
+
const _projIdx = args.indexOf("--project");
|
|
3904
|
+
const _projDir = _projIdx !== -1 ? args[_projIdx + 1] : process.cwd();
|
|
3905
|
+
const _consumerIdx = args.indexOf("--consumer");
|
|
3906
|
+
const _consumerArg = _consumerIdx !== -1 ? args[_consumerIdx + 1] : "cli";
|
|
3907
|
+
const _modeIdx = args.indexOf("--mode");
|
|
3908
|
+
const _modeArg = _modeIdx !== -1 ? args[_modeIdx + 1] : null;
|
|
3909
|
+
const _autoFlag = args.includes("--auto");
|
|
3910
|
+
try {
|
|
3911
|
+
const _resolver = require("./gsd-t-graph-store-resolver.cjs");
|
|
3912
|
+
let _mode;
|
|
3913
|
+
if (_autoFlag) {
|
|
3914
|
+
const _sp = _resolver.resolveStorePath(_projDir);
|
|
3915
|
+
_mode = (_sp && fs.existsSync(_sp)) ? "WIRED" : "fallback-announced";
|
|
3916
|
+
} else if (_modeArg) {
|
|
3917
|
+
_mode = _modeArg;
|
|
3918
|
+
} else {
|
|
3919
|
+
process.stdout.write("ledger-write-skipped: need --mode or --auto\n");
|
|
3920
|
+
process.exit(0);
|
|
3921
|
+
}
|
|
3922
|
+
const _record = { kind: "wiring", ts: new Date().toISOString(), consumer: _consumerArg, graphWiringMode: _mode };
|
|
3923
|
+
_resolver.append_ledger_line(_record, _projDir);
|
|
3924
|
+
process.stdout.write(`ok: ${_mode}\n`);
|
|
3925
|
+
} catch (_e) {
|
|
3926
|
+
process.stdout.write(`ledger-write-skipped: ${_e && _e.message ? _e.message : String(_e)}\n`);
|
|
3927
|
+
}
|
|
3928
|
+
process.exit(0);
|
|
3929
|
+
}
|
|
3885
3930
|
default:
|
|
3886
3931
|
error(`Unknown graph subcommand: ${sub}`);
|
|
3887
|
-
info("Usage: gsd-t graph [index|status|query|who-imports|who-calls|blast-radius|body|tasks]");
|
|
3932
|
+
info("Usage: gsd-t graph [index|status|query|who-imports|who-calls|blast-radius|body|tasks|metrics|wiring-log]");
|
|
3888
3933
|
info(" gsd-t graph --output json|table (task DAG)");
|
|
3889
3934
|
}
|
|
3890
3935
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.14.10",
|
|
4
4
|
"description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
|
|
5
5
|
"author": "Tekyz, Inc.",
|
|
6
6
|
"license": "MIT",
|