@open-tomato/rafa 0.26.0 → 0.31.0
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/README.md +220 -14
- package/dist/bundled/skills/dev-planner/SKILL.md +4 -4
- package/dist/cli.js +26094 -18698
- package/dist/{describe-vbwqxj1f.js → describe-b2kfwphn.js} +3 -3
- package/dist/effort/store/index.js +4 -4
- package/dist/{index-azptzvp3.js → index-13wzpgc1.js} +2248 -269
- package/dist/{index-p1265xgj.js → index-89j1m0f6.js} +3 -3
- package/dist/{index-2yd8bber.js → index-b766n1aw.js} +3417 -2752
- package/dist/{index-sf2brh4w.js → index-dnwcx1cs.js} +2 -1
- package/dist/{index-4j1e3h26.js → index-e10rwa4g.js} +1 -1
- package/dist/{index-xwr54rzt.js → index-p0njjkjj.js} +2 -2
- package/dist/{index-59x3zsh4.js → index-szgyrz6j.js} +6974 -2599
- package/dist/{index-rv7y8y1j.js → index-t6q4k1tq.js} +113 -26
- package/dist/index.js +12 -10
- package/dist/plan/index.js +3 -3
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
CONFIG_DEFAULTS,
|
|
4
4
|
CONFIG_FILE,
|
|
5
|
+
ConfigError,
|
|
5
6
|
FINDING_DOMAINS,
|
|
6
7
|
FINDING_KINDS,
|
|
7
8
|
FINDING_SIGNALS,
|
|
@@ -10,20 +11,24 @@ import {
|
|
|
10
11
|
configFilePath,
|
|
11
12
|
createTextOutput,
|
|
12
13
|
isStampableStub,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
parseConfigText,
|
|
15
|
+
readRafaBlocks,
|
|
16
|
+
resolveConfig
|
|
17
|
+
} from "./index-t6q4k1tq.js";
|
|
15
18
|
import {
|
|
16
19
|
bless,
|
|
17
20
|
merge
|
|
18
21
|
} from "./index-d6d0942q.js";
|
|
19
22
|
import {
|
|
23
|
+
CommandExit,
|
|
20
24
|
commandSpelling
|
|
21
|
-
} from "./index-
|
|
25
|
+
} from "./index-e10rwa4g.js";
|
|
22
26
|
import {
|
|
23
27
|
BUSY_TIMEOUT_MAX_MS,
|
|
24
28
|
BUSY_TIMEOUT_MIN_MS,
|
|
25
29
|
RAFA_INSTINCTS_PATH,
|
|
26
30
|
STORE_BACKENDS,
|
|
31
|
+
SYNC_STRATEGIES,
|
|
27
32
|
bundledBinDirectory,
|
|
28
33
|
describeValue,
|
|
29
34
|
isBusyTimeoutMs,
|
|
@@ -31,7 +36,7 @@ import {
|
|
|
31
36
|
messageOf,
|
|
32
37
|
resolveInstinctScopes,
|
|
33
38
|
tierExists
|
|
34
|
-
} from "./index-
|
|
39
|
+
} from "./index-dnwcx1cs.js";
|
|
35
40
|
import {
|
|
36
41
|
actionHash,
|
|
37
42
|
triggerKey
|
|
@@ -238,13 +243,13 @@ function openNdjsonStore(repoRoot) {
|
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
// src/effort/store/sqlite.ts
|
|
241
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
242
|
-
import { dirname as
|
|
246
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
247
|
+
import { dirname as dirname9, join as join9 } from "path";
|
|
243
248
|
import { Database } from "bun:sqlite";
|
|
244
249
|
|
|
245
250
|
// package.json
|
|
246
251
|
var name = "@open-tomato/rafa";
|
|
247
|
-
var version = "0.
|
|
252
|
+
var version = "0.31.0";
|
|
248
253
|
|
|
249
254
|
// src/cli/version.ts
|
|
250
255
|
var RAFA_VERSION = version;
|
|
@@ -254,23 +259,99 @@ function versionLine() {
|
|
|
254
259
|
|
|
255
260
|
// src/effort/store/development-build.ts
|
|
256
261
|
import { tmpdir as tmpdir2 } from "os";
|
|
257
|
-
import { basename as basename2, dirname as
|
|
262
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5 } from "path";
|
|
258
263
|
|
|
259
264
|
// src/loop/sessions.ts
|
|
260
265
|
import {
|
|
261
266
|
linkSync,
|
|
262
|
-
mkdirSync as
|
|
267
|
+
mkdirSync as mkdirSync4,
|
|
263
268
|
readdirSync,
|
|
264
|
-
readFileSync as
|
|
265
|
-
renameSync,
|
|
266
|
-
rmSync,
|
|
267
|
-
writeFileSync
|
|
269
|
+
readFileSync as readFileSync4,
|
|
270
|
+
renameSync as renameSync3,
|
|
271
|
+
rmSync as rmSync3,
|
|
272
|
+
writeFileSync as writeFileSync3
|
|
268
273
|
} from "fs";
|
|
269
|
-
import { basename, dirname as
|
|
274
|
+
import { basename, dirname as dirname5, isAbsolute as isAbsolute4, join as join7 } from "path";
|
|
275
|
+
|
|
276
|
+
// src/next/hop-record.ts
|
|
277
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
278
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
279
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
280
|
+
|
|
281
|
+
// src/project/position.ts
|
|
282
|
+
import { randomUUID } from "crypto";
|
|
283
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
|
|
284
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
270
285
|
|
|
271
286
|
// src/project/scope.ts
|
|
272
287
|
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
|
|
273
|
-
import { dirname as dirname2, isAbsolute as
|
|
288
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4 } from "path";
|
|
289
|
+
|
|
290
|
+
// src/project/worktree-root.ts
|
|
291
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
292
|
+
|
|
293
|
+
// src/pr/git.ts
|
|
294
|
+
import { spawnSync } from "child_process";
|
|
295
|
+
function createGitRunner(cwd) {
|
|
296
|
+
return (args) => {
|
|
297
|
+
const result = spawnSync("git", [...args], {
|
|
298
|
+
cwd,
|
|
299
|
+
encoding: "utf8",
|
|
300
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
301
|
+
});
|
|
302
|
+
if (result.error !== undefined) {
|
|
303
|
+
return { ok: false, stdout: "", stderr: `could not run git in ${cwd}: ${messageOf(result.error)}` };
|
|
304
|
+
}
|
|
305
|
+
return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function gitSaid(result) {
|
|
309
|
+
return [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream !== "").join(`
|
|
310
|
+
`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/project/worktree-root.ts
|
|
314
|
+
var NOT_A_REPOSITORY = "fatal: not a git repository (or any ";
|
|
315
|
+
var WORKTREE_LIST_ARGS = Object.freeze(["worktree", "list", "--porcelain", "-z"]);
|
|
316
|
+
var WORKTREE_PREFIX = "worktree ";
|
|
317
|
+
var BARE_LINE = "bare";
|
|
318
|
+
|
|
319
|
+
class WorktreeRootError extends Error {
|
|
320
|
+
constructor(message, options) {
|
|
321
|
+
super(`rafa worktree root: ${message}`, options);
|
|
322
|
+
this.name = "WorktreeRootError";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function firstRecordOf(stdout) {
|
|
326
|
+
const lines = stdout.split("\x00");
|
|
327
|
+
const end = lines.indexOf("");
|
|
328
|
+
return end === -1 ? lines : lines.slice(0, end);
|
|
329
|
+
}
|
|
330
|
+
function readListing(dir, stdout) {
|
|
331
|
+
const record = firstRecordOf(stdout);
|
|
332
|
+
const head = record[0];
|
|
333
|
+
if (head === undefined || !head.startsWith(WORKTREE_PREFIX)) {
|
|
334
|
+
throw new WorktreeRootError(`git worktree list in ${dir} printed no worktree line first`);
|
|
335
|
+
}
|
|
336
|
+
if (record.includes(BARE_LINE))
|
|
337
|
+
return null;
|
|
338
|
+
return head.slice(WORKTREE_PREFIX.length);
|
|
339
|
+
}
|
|
340
|
+
function readFailure(dir, result) {
|
|
341
|
+
if (result.stderr.startsWith(NOT_A_REPOSITORY))
|
|
342
|
+
return null;
|
|
343
|
+
throw new WorktreeRootError(`git worktree list failed in ${dir}: ${gitSaid(result)}`);
|
|
344
|
+
}
|
|
345
|
+
function mainCheckoutOf(dir, seams = {}) {
|
|
346
|
+
if (!isAbsolute2(dir)) {
|
|
347
|
+
throw new WorktreeRootError(`directory ${JSON.stringify(dir)} is not an absolute path`);
|
|
348
|
+
}
|
|
349
|
+
const git = (seams.openGit ?? createGitRunner)(dir);
|
|
350
|
+
const result = git(WORKTREE_LIST_ARGS);
|
|
351
|
+
return result.ok ? readListing(dir, result.stdout) : readFailure(dir, result);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/project/scope.ts
|
|
274
355
|
var SCOPE_DIR = dirname2(CONFIG_FILE);
|
|
275
356
|
var INIT_COMMAND = "rafa init";
|
|
276
357
|
var DISK_FILE_SYSTEM = Object.freeze({
|
|
@@ -287,17 +368,19 @@ class ScopeError extends Error {
|
|
|
287
368
|
function scopeAt(base) {
|
|
288
369
|
return { dir: join4(base, SCOPE_DIR), configFile: configFilePath(base) };
|
|
289
370
|
}
|
|
290
|
-
function initHint(start, passedUserConfig = null) {
|
|
371
|
+
function initHint(start, passedUserConfig = null, probedMainCheckout = null) {
|
|
291
372
|
const passed = passedUserConfig === null ? [] : [`${passedUserConfig} is the user scope's config and marks no project`];
|
|
373
|
+
const main = probedMainCheckout === null ? [] : [`nor in ${probedMainCheckout}, the main checkout of the repository holding it`];
|
|
292
374
|
return [
|
|
293
375
|
`not inside a rafa project: no ${CONFIG_FILE} in ${start} or any directory above it`,
|
|
376
|
+
...main,
|
|
294
377
|
...passed,
|
|
295
378
|
`run \`${INIT_COMMAND}\` to set one up`
|
|
296
379
|
].join(`
|
|
297
380
|
`);
|
|
298
381
|
}
|
|
299
382
|
function requireAbsolute(label, path) {
|
|
300
|
-
if (!
|
|
383
|
+
if (!isAbsolute3(path)) {
|
|
301
384
|
throw new ScopeError(`${label} is ${describeValue(path)}, expected an absolute path`);
|
|
302
385
|
}
|
|
303
386
|
}
|
|
@@ -331,17 +414,215 @@ function walkUp(realStart, realHome, fs) {
|
|
|
331
414
|
}
|
|
332
415
|
return { root: null, passedUserConfig };
|
|
333
416
|
}
|
|
417
|
+
function mainCheckoutAt(realStart, mainCheckout) {
|
|
418
|
+
try {
|
|
419
|
+
return mainCheckout(realStart);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (!(error instanceof WorktreeRootError))
|
|
422
|
+
throw error;
|
|
423
|
+
throw new ScopeError(`cannot read the main checkout of ${realStart} (${error.message})`, { cause: error });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function fallBack(realStart, realHome, fs, mainCheckout) {
|
|
427
|
+
const main = mainCheckoutAt(realStart, mainCheckout);
|
|
428
|
+
if (main === null || main === realHome || selfAndAncestors(realStart).includes(main)) {
|
|
429
|
+
return { root: null, probed: null };
|
|
430
|
+
}
|
|
431
|
+
return fs.exists(configFilePath(main)) ? { root: main, probed: main } : { root: null, probed: main };
|
|
432
|
+
}
|
|
334
433
|
function resolveScope(start, seams) {
|
|
335
|
-
const { home, fs = DISK_FILE_SYSTEM } = seams;
|
|
434
|
+
const { home, fs = DISK_FILE_SYSTEM, mainCheckout = mainCheckoutOf } = seams;
|
|
336
435
|
requireAbsolute("start directory", start);
|
|
337
436
|
requireAbsolute("home directory", home);
|
|
338
437
|
const user = scopeAt(home);
|
|
339
|
-
const
|
|
340
|
-
|
|
438
|
+
const realStart = realStartOf(start, fs);
|
|
439
|
+
const realHome = realHomeOf(home, fs);
|
|
440
|
+
const walk = walkUp(realStart, realHome, fs);
|
|
441
|
+
const fallback = walk.root === null ? fallBack(realStart, realHome, fs, mainCheckout) : { root: walk.root, probed: null };
|
|
442
|
+
if (fallback.root === null) {
|
|
341
443
|
const passed = walk.passedUserConfig ? user.configFile : null;
|
|
342
|
-
return { found: false, start, home, user, hint: initHint(start, passed) };
|
|
444
|
+
return { found: false, start, home, user, hint: initHint(start, passed, fallback.probed) };
|
|
445
|
+
}
|
|
446
|
+
return { found: true, root: fallback.root, home, project: scopeAt(fallback.root), user };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/project/position.ts
|
|
450
|
+
var POSITION_FILE = "position.json";
|
|
451
|
+
function positionFilePath(root) {
|
|
452
|
+
return join5(scopeAt(root).dir, POSITION_FILE);
|
|
453
|
+
}
|
|
454
|
+
function isRecord(value) {
|
|
455
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
456
|
+
}
|
|
457
|
+
function isIssueNumber(value) {
|
|
458
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
459
|
+
}
|
|
460
|
+
function asPlace(value) {
|
|
461
|
+
if (!isRecord(value) || !isIssueNumber(value.board))
|
|
462
|
+
return null;
|
|
463
|
+
if (value.epic !== null && !isIssueNumber(value.epic))
|
|
464
|
+
return null;
|
|
465
|
+
return { board: value.board, epic: value.epic };
|
|
466
|
+
}
|
|
467
|
+
function asPosition(value) {
|
|
468
|
+
if (!isRecord(value))
|
|
469
|
+
return null;
|
|
470
|
+
const current = asPlace(value.current);
|
|
471
|
+
const home = asPlace(value.home);
|
|
472
|
+
const previous = value.previous === null ? null : asPlace(value.previous);
|
|
473
|
+
if (current === null || home === null)
|
|
474
|
+
return null;
|
|
475
|
+
if (previous === null && value.previous !== null)
|
|
476
|
+
return null;
|
|
477
|
+
return { current, previous, home };
|
|
478
|
+
}
|
|
479
|
+
function readPositionFile(root) {
|
|
480
|
+
const file = positionFilePath(root);
|
|
481
|
+
let text;
|
|
482
|
+
try {
|
|
483
|
+
text = readFileSync2(file, "utf8");
|
|
484
|
+
} catch (error) {
|
|
485
|
+
const code = error.code;
|
|
486
|
+
return code === "ENOENT" ? { set: false, reason: "absent", detail: `No position file at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
|
|
487
|
+
}
|
|
488
|
+
let parsed;
|
|
489
|
+
try {
|
|
490
|
+
parsed = JSON.parse(text);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return { set: false, reason: "invalid-json", detail: `${file} holds no JSON: ${messageOf(error)}` };
|
|
493
|
+
}
|
|
494
|
+
const position = asPosition(parsed);
|
|
495
|
+
return position === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a current, previous and home place` } : { set: true, position };
|
|
496
|
+
}
|
|
497
|
+
function writePositionFile(root, position) {
|
|
498
|
+
const file = positionFilePath(root);
|
|
499
|
+
mkdirSync2(dirname3(file), { recursive: true });
|
|
500
|
+
const temporary = `${file}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
501
|
+
const { current, previous, home } = position;
|
|
502
|
+
try {
|
|
503
|
+
writeFileSync(temporary, `${JSON.stringify({ current, previous, home }, null, 2)}
|
|
504
|
+
`);
|
|
505
|
+
renameSync(temporary, file);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
rmSync(temporary, { force: true });
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function positionAt(place) {
|
|
512
|
+
return { current: place, previous: null, home: place };
|
|
513
|
+
}
|
|
514
|
+
function rehome(position, place) {
|
|
515
|
+
return { current: place, previous: position.current, home: place };
|
|
516
|
+
}
|
|
517
|
+
function hop(position, place) {
|
|
518
|
+
return { current: place, previous: position.current, home: position.home };
|
|
519
|
+
}
|
|
520
|
+
function goHome(position) {
|
|
521
|
+
return { current: position.home, previous: position.current, home: position.home };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/next/hop-record.ts
|
|
525
|
+
var HOP_FILE = "hop.json";
|
|
526
|
+
var HOP_KINDS = ["blocker", "dry"];
|
|
527
|
+
var HOP_STATES = ["away", "waiting", "merged", "halted"];
|
|
528
|
+
function hopFilePath(root) {
|
|
529
|
+
return join6(scopeAt(root).dir, HOP_FILE);
|
|
530
|
+
}
|
|
531
|
+
function isRecord2(value) {
|
|
532
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
533
|
+
}
|
|
534
|
+
function isIssueNumber2(value) {
|
|
535
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
536
|
+
}
|
|
537
|
+
function isIssueOrNull(value) {
|
|
538
|
+
return value === null || isIssueNumber2(value);
|
|
539
|
+
}
|
|
540
|
+
function isTimestamp(value) {
|
|
541
|
+
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
542
|
+
}
|
|
543
|
+
function issuesMatchKind(kind, blocked, target) {
|
|
544
|
+
return kind === "blocker" ? blocked !== null && target !== null : blocked === null && target === null;
|
|
545
|
+
}
|
|
546
|
+
function asHopRecord(value) {
|
|
547
|
+
if (!isRecord2(value))
|
|
548
|
+
return null;
|
|
549
|
+
const { kind, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = value;
|
|
550
|
+
const home = asPlace(value.home);
|
|
551
|
+
const from = asPlace(value.from);
|
|
552
|
+
if (typeof kind !== "string" || !HOP_KINDS.includes(kind))
|
|
553
|
+
return null;
|
|
554
|
+
if (typeof state !== "string" || !HOP_STATES.includes(state))
|
|
555
|
+
return null;
|
|
556
|
+
if (home === null || from === null)
|
|
557
|
+
return null;
|
|
558
|
+
if (!isIssueOrNull(blocked) || !isIssueOrNull(target) || !isIssueOrNull(pullRequest))
|
|
559
|
+
return null;
|
|
560
|
+
if (!isIssueNumber2(targetEpic) || !isIssueNumber2(targetBoard) || !isTimestamp(startedAt))
|
|
561
|
+
return null;
|
|
562
|
+
const hopKind = kind;
|
|
563
|
+
if (!issuesMatchKind(hopKind, blocked, target))
|
|
564
|
+
return null;
|
|
565
|
+
return {
|
|
566
|
+
kind: hopKind,
|
|
567
|
+
home,
|
|
568
|
+
from,
|
|
569
|
+
blocked,
|
|
570
|
+
target,
|
|
571
|
+
targetEpic,
|
|
572
|
+
targetBoard,
|
|
573
|
+
state,
|
|
574
|
+
pullRequest,
|
|
575
|
+
startedAt
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function readHopRecord(root) {
|
|
579
|
+
const file = hopFilePath(root);
|
|
580
|
+
let text;
|
|
581
|
+
try {
|
|
582
|
+
text = readFileSync3(file, "utf8");
|
|
583
|
+
} catch (error) {
|
|
584
|
+
const code = error.code;
|
|
585
|
+
return code === "ENOENT" ? { set: false, reason: "absent", detail: `No hop record at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
|
|
586
|
+
}
|
|
587
|
+
let parsed;
|
|
588
|
+
try {
|
|
589
|
+
parsed = JSON.parse(text);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
return { set: false, reason: "wrong-shape", detail: `${file} holds no JSON: ${messageOf(error)}` };
|
|
343
592
|
}
|
|
344
|
-
|
|
593
|
+
const record = asHopRecord(parsed);
|
|
594
|
+
return record === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a hop record` } : { set: true, record };
|
|
595
|
+
}
|
|
596
|
+
function ownKeys(record) {
|
|
597
|
+
const { kind, home, from, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = record;
|
|
598
|
+
return {
|
|
599
|
+
kind,
|
|
600
|
+
home: { board: home.board, epic: home.epic },
|
|
601
|
+
from: { board: from.board, epic: from.epic },
|
|
602
|
+
blocked,
|
|
603
|
+
target,
|
|
604
|
+
targetEpic,
|
|
605
|
+
targetBoard,
|
|
606
|
+
state,
|
|
607
|
+
pullRequest,
|
|
608
|
+
startedAt
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function writeHopRecord(root, record) {
|
|
612
|
+
const file = hopFilePath(root);
|
|
613
|
+
mkdirSync3(dirname4(file), { recursive: true });
|
|
614
|
+
const temporary = `${file}.${String(process.pid)}.${randomUUID2()}.tmp`;
|
|
615
|
+
try {
|
|
616
|
+
writeFileSync2(temporary, `${JSON.stringify(ownKeys(record), null, 2)}
|
|
617
|
+
`);
|
|
618
|
+
renameSync2(temporary, file);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
rmSync2(temporary, { force: true });
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function staleAgainst(record, position) {
|
|
625
|
+
return position.home.board !== record.home.board || position.home.epic !== record.home.epic;
|
|
345
626
|
}
|
|
346
627
|
|
|
347
628
|
// src/loop/sessions.ts
|
|
@@ -384,13 +665,13 @@ function isSessionId(value) {
|
|
|
384
665
|
return SESSION_ID_PATTERN.test(value);
|
|
385
666
|
}
|
|
386
667
|
function runsDir(root) {
|
|
387
|
-
return
|
|
668
|
+
return join7(scopeAt(root).dir, RUNS_DIR);
|
|
388
669
|
}
|
|
389
670
|
function sessionFilePath(root, sessionId) {
|
|
390
671
|
if (!isSessionId(sessionId)) {
|
|
391
672
|
throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
|
|
392
673
|
}
|
|
393
|
-
return
|
|
674
|
+
return join7(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
|
|
394
675
|
}
|
|
395
676
|
function describeValue2(value) {
|
|
396
677
|
return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
|
|
@@ -417,6 +698,18 @@ function taskProblem(task) {
|
|
|
417
698
|
}
|
|
418
699
|
return isText(field(task, "text")) ? null : `task.text is ${describeValue2(field(task, "text"))}, expected a non-empty string`;
|
|
419
700
|
}
|
|
701
|
+
function hopProblem(fields) {
|
|
702
|
+
if (!Object.hasOwn(fields, "hop"))
|
|
703
|
+
return null;
|
|
704
|
+
const hop2 = field(fields, "hop");
|
|
705
|
+
return asHopRecord(hop2) === null ? `hop is ${describeValue2(hop2)}, expected a hop record` : null;
|
|
706
|
+
}
|
|
707
|
+
function worktreeProblem(fields) {
|
|
708
|
+
if (!Object.hasOwn(fields, "worktree"))
|
|
709
|
+
return null;
|
|
710
|
+
const worktree = field(fields, "worktree");
|
|
711
|
+
return isText(worktree) && isAbsolute4(worktree) ? null : `worktree is ${describeValue2(worktree)}, expected an absolute path`;
|
|
712
|
+
}
|
|
420
713
|
function sessionIdProblem(sessionId, file) {
|
|
421
714
|
if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
|
|
422
715
|
return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
|
|
@@ -440,12 +733,20 @@ function recordProblems(fields, file) {
|
|
|
440
733
|
isPositiveWhole(pid) ? null : `pid is ${describeValue2(pid)}, expected a whole number from 1`,
|
|
441
734
|
typeof startedAt === "string" && !Number.isNaN(Date.parse(startedAt)) ? null : `startedAt is ${describeValue2(startedAt)}, expected a timestamp`,
|
|
442
735
|
SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
|
|
443
|
-
taskProblem(field(fields, "task"))
|
|
736
|
+
taskProblem(field(fields, "task")),
|
|
737
|
+
hopProblem(fields),
|
|
738
|
+
worktreeProblem(fields)
|
|
444
739
|
];
|
|
445
740
|
return problems.filter((problem) => problem !== null);
|
|
446
741
|
}
|
|
742
|
+
function freezeHop(value) {
|
|
743
|
+
const hop2 = asHopRecord(value);
|
|
744
|
+
return Object.freeze({ ...hop2, home: Object.freeze(hop2.home), from: Object.freeze(hop2.from) });
|
|
745
|
+
}
|
|
447
746
|
function freezeRecord(fields) {
|
|
448
747
|
const task = field(fields, "task");
|
|
748
|
+
const hop2 = field(fields, "hop");
|
|
749
|
+
const worktree = field(fields, "worktree");
|
|
449
750
|
return Object.freeze({
|
|
450
751
|
sessionId: field(fields, "sessionId"),
|
|
451
752
|
planStub: field(fields, "planStub"),
|
|
@@ -454,7 +755,9 @@ function freezeRecord(fields) {
|
|
|
454
755
|
pid: field(fields, "pid"),
|
|
455
756
|
startedAt: field(fields, "startedAt"),
|
|
456
757
|
state: field(fields, "state"),
|
|
457
|
-
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null
|
|
758
|
+
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
|
|
759
|
+
...hop2 === undefined ? {} : { hop: freezeHop(hop2) },
|
|
760
|
+
...worktree === undefined ? {} : { worktree }
|
|
458
761
|
});
|
|
459
762
|
}
|
|
460
763
|
function parseSessionRecord(text, file) {
|
|
@@ -492,7 +795,7 @@ function readState(record, isAlive = isPidAlive) {
|
|
|
492
795
|
function readRecordFile(file) {
|
|
493
796
|
let text;
|
|
494
797
|
try {
|
|
495
|
-
text =
|
|
798
|
+
text = readFileSync4(file, "utf8");
|
|
496
799
|
} catch (error) {
|
|
497
800
|
throw new SessionRecordError(file, `cannot be read: ${messageOf(error)}`);
|
|
498
801
|
}
|
|
@@ -513,7 +816,7 @@ function byStart(a, b) {
|
|
|
513
816
|
function readSessions(root, seams = {}) {
|
|
514
817
|
const dir = runsDir(root);
|
|
515
818
|
const isAlive = seams.isAlive ?? isPidAlive;
|
|
516
|
-
const records = recordNames(dir).filter((name2) => name2.endsWith(RECORD_EXTENSION)).map((name2) => readRecordFile(
|
|
819
|
+
const records = recordNames(dir).filter((name2) => name2.endsWith(RECORD_EXTENSION)).map((name2) => readRecordFile(join7(dir, name2))).map((record) => Object.freeze({ ...record, state: readState(record, isAlive) }));
|
|
517
820
|
return Object.freeze(records.sort(byStart));
|
|
518
821
|
}
|
|
519
822
|
function readSession(root, sessionId, seams = {}) {
|
|
@@ -539,17 +842,17 @@ function checkRecord(record, file) {
|
|
|
539
842
|
}
|
|
540
843
|
function writeRecordFile(file, record, isNew) {
|
|
541
844
|
checkRecord(record, file);
|
|
542
|
-
|
|
845
|
+
mkdirSync4(dirname5(file), { recursive: true });
|
|
543
846
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
544
|
-
|
|
847
|
+
writeFileSync3(temporary, `${JSON.stringify(record, null, 2)}
|
|
545
848
|
`);
|
|
546
849
|
try {
|
|
547
850
|
if (isNew)
|
|
548
851
|
linkSync(temporary, file);
|
|
549
852
|
else
|
|
550
|
-
|
|
853
|
+
renameSync3(temporary, file);
|
|
551
854
|
} finally {
|
|
552
|
-
|
|
855
|
+
rmSync3(temporary, { force: true });
|
|
553
856
|
}
|
|
554
857
|
}
|
|
555
858
|
function beginSession(root, draft, seams = {}) {
|
|
@@ -577,10 +880,10 @@ function updateSession(root, sessionId, change) {
|
|
|
577
880
|
}
|
|
578
881
|
|
|
579
882
|
// src/runtime/identity.ts
|
|
580
|
-
import { existsSync as existsSync4, readFileSync as
|
|
581
|
-
import { dirname as
|
|
883
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3 } from "fs";
|
|
884
|
+
import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
|
|
582
885
|
var MANIFEST = "package.json";
|
|
583
|
-
var CHECKOUT_ENTRY =
|
|
886
|
+
var CHECKOUT_ENTRY = join8("src", "rafa.ts");
|
|
584
887
|
function realOrResolved(path) {
|
|
585
888
|
const resolved = resolve2(path);
|
|
586
889
|
return existsSync4(resolved) ? realpathSync3(resolved) : resolved;
|
|
@@ -588,7 +891,7 @@ function realOrResolved(path) {
|
|
|
588
891
|
function manifestName(file) {
|
|
589
892
|
let parsed;
|
|
590
893
|
try {
|
|
591
|
-
parsed = JSON.parse(
|
|
894
|
+
parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
592
895
|
} catch {
|
|
593
896
|
return null;
|
|
594
897
|
}
|
|
@@ -600,10 +903,10 @@ function manifestName(file) {
|
|
|
600
903
|
function nearestRafaPackage(start) {
|
|
601
904
|
let dir = start;
|
|
602
905
|
for (;; ) {
|
|
603
|
-
const manifest =
|
|
906
|
+
const manifest = join8(dir, MANIFEST);
|
|
604
907
|
if (existsSync4(manifest) && manifestName(manifest) === name)
|
|
605
908
|
return dir;
|
|
606
|
-
const parent =
|
|
909
|
+
const parent = dirname6(dir);
|
|
607
910
|
if (parent === dir)
|
|
608
911
|
return null;
|
|
609
912
|
dir = parent;
|
|
@@ -611,8 +914,8 @@ function nearestRafaPackage(start) {
|
|
|
611
914
|
}
|
|
612
915
|
function readRuntimeIdentity(entry = Bun.main) {
|
|
613
916
|
const real = realOrResolved(entry);
|
|
614
|
-
const packageDir = nearestRafaPackage(
|
|
615
|
-
if (packageDir !== null && existsSync4(
|
|
917
|
+
const packageDir = nearestRafaPackage(dirname6(real));
|
|
918
|
+
if (packageDir !== null && existsSync4(join8(packageDir, CHECKOUT_ENTRY))) {
|
|
616
919
|
return { kind: "development", entry: real, checkout: packageDir };
|
|
617
920
|
}
|
|
618
921
|
return { kind: "installed", entry: real };
|
|
@@ -638,17 +941,17 @@ class DevelopmentBuildRefusedError extends Error {
|
|
|
638
941
|
}
|
|
639
942
|
}
|
|
640
943
|
function storeProjectRoot(path) {
|
|
641
|
-
const storeDir =
|
|
642
|
-
const scopeDir =
|
|
944
|
+
const storeDir = dirname7(path);
|
|
945
|
+
const scopeDir = dirname7(storeDir);
|
|
643
946
|
if (basename2(storeDir) !== EFFORT_DIR_NAME || basename2(scopeDir) !== SCOPE_DIR)
|
|
644
947
|
return null;
|
|
645
|
-
return
|
|
948
|
+
return dirname7(scopeDir);
|
|
646
949
|
}
|
|
647
950
|
function isOwnedStore(path, env, tempDir) {
|
|
648
951
|
if (isUnderTempDir(path, tempDir))
|
|
649
952
|
return true;
|
|
650
953
|
const override = env[EFFORT_DIR_VARIABLE];
|
|
651
|
-
if (override === undefined || override === "" || !
|
|
954
|
+
if (override === undefined || override === "" || !isAbsolute5(override))
|
|
652
955
|
return false;
|
|
653
956
|
return storeProjectRoot(path) === null && isUnderTempDir(path, override);
|
|
654
957
|
}
|
|
@@ -926,6 +1229,120 @@ var SQLITE_MIGRATIONS = [
|
|
|
926
1229
|
CHECK ((verdict = 'red') = (json_array_length(failing) > 0))
|
|
927
1230
|
);
|
|
928
1231
|
`
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
id: "row-origins",
|
|
1235
|
+
breaks: [],
|
|
1236
|
+
sql: `
|
|
1237
|
+
ALTER TABLE sessions ADD COLUMN origin_store TEXT;
|
|
1238
|
+
ALTER TABLE sessions ADD COLUMN origin_seq INTEGER;
|
|
1239
|
+
CREATE UNIQUE INDEX sessions_by_origin
|
|
1240
|
+
ON sessions (origin_store, origin_seq)
|
|
1241
|
+
WHERE origin_store IS NOT NULL;
|
|
1242
|
+
|
|
1243
|
+
ALTER TABLE commits ADD COLUMN origin_store TEXT;
|
|
1244
|
+
ALTER TABLE commits ADD COLUMN origin_seq INTEGER;
|
|
1245
|
+
CREATE UNIQUE INDEX commits_by_origin
|
|
1246
|
+
ON commits (origin_store, origin_seq)
|
|
1247
|
+
WHERE origin_store IS NOT NULL;
|
|
1248
|
+
|
|
1249
|
+
ALTER TABLE findings ADD COLUMN origin_store TEXT;
|
|
1250
|
+
ALTER TABLE findings ADD COLUMN origin_seq INTEGER;
|
|
1251
|
+
CREATE UNIQUE INDEX findings_by_origin
|
|
1252
|
+
ON findings (origin_store, origin_seq)
|
|
1253
|
+
WHERE origin_store IS NOT NULL;
|
|
1254
|
+
|
|
1255
|
+
ALTER TABLE blockers ADD COLUMN origin_store TEXT;
|
|
1256
|
+
ALTER TABLE blockers ADD COLUMN origin_seq INTEGER;
|
|
1257
|
+
CREATE UNIQUE INDEX blockers_by_origin
|
|
1258
|
+
ON blockers (origin_store, origin_seq)
|
|
1259
|
+
WHERE origin_store IS NOT NULL;
|
|
1260
|
+
|
|
1261
|
+
ALTER TABLE out_of_scope_bugs ADD COLUMN origin_store TEXT;
|
|
1262
|
+
ALTER TABLE out_of_scope_bugs ADD COLUMN origin_seq INTEGER;
|
|
1263
|
+
CREATE UNIQUE INDEX out_of_scope_bugs_by_origin
|
|
1264
|
+
ON out_of_scope_bugs (origin_store, origin_seq)
|
|
1265
|
+
WHERE origin_store IS NOT NULL;
|
|
1266
|
+
|
|
1267
|
+
ALTER TABLE report_absences ADD COLUMN origin_store TEXT;
|
|
1268
|
+
ALTER TABLE report_absences ADD COLUMN origin_seq INTEGER;
|
|
1269
|
+
CREATE UNIQUE INDEX report_absences_by_origin
|
|
1270
|
+
ON report_absences (origin_store, origin_seq)
|
|
1271
|
+
WHERE origin_store IS NOT NULL;
|
|
1272
|
+
|
|
1273
|
+
ALTER TABLE task_reports ADD COLUMN origin_store TEXT;
|
|
1274
|
+
ALTER TABLE task_reports ADD COLUMN origin_seq INTEGER;
|
|
1275
|
+
CREATE UNIQUE INDEX task_reports_by_origin
|
|
1276
|
+
ON task_reports (origin_store, origin_seq)
|
|
1277
|
+
WHERE origin_store IS NOT NULL;
|
|
1278
|
+
|
|
1279
|
+
ALTER TABLE preflight ADD COLUMN origin_store TEXT;
|
|
1280
|
+
ALTER TABLE preflight ADD COLUMN origin_seq INTEGER;
|
|
1281
|
+
CREATE UNIQUE INDEX preflight_by_origin
|
|
1282
|
+
ON preflight (origin_store, origin_seq)
|
|
1283
|
+
WHERE origin_store IS NOT NULL;
|
|
1284
|
+
|
|
1285
|
+
ALTER TABLE dispatches ADD COLUMN origin_store TEXT;
|
|
1286
|
+
ALTER TABLE dispatches ADD COLUMN origin_seq INTEGER;
|
|
1287
|
+
CREATE UNIQUE INDEX dispatches_by_origin
|
|
1288
|
+
ON dispatches (origin_store, origin_seq)
|
|
1289
|
+
WHERE origin_store IS NOT NULL;
|
|
1290
|
+
|
|
1291
|
+
ALTER TABLE changes ADD COLUMN origin_store TEXT;
|
|
1292
|
+
ALTER TABLE changes ADD COLUMN origin_seq INTEGER;
|
|
1293
|
+
CREATE UNIQUE INDEX changes_by_origin
|
|
1294
|
+
ON changes (origin_store, origin_seq)
|
|
1295
|
+
WHERE origin_store IS NOT NULL;
|
|
1296
|
+
|
|
1297
|
+
ALTER TABLE skill_invocations ADD COLUMN origin_store TEXT;
|
|
1298
|
+
ALTER TABLE skill_invocations ADD COLUMN origin_seq INTEGER;
|
|
1299
|
+
CREATE UNIQUE INDEX skill_invocations_by_origin
|
|
1300
|
+
ON skill_invocations (origin_store, origin_seq)
|
|
1301
|
+
WHERE origin_store IS NOT NULL;
|
|
1302
|
+
|
|
1303
|
+
ALTER TABLE plan_ci ADD COLUMN origin_store TEXT;
|
|
1304
|
+
ALTER TABLE plan_ci ADD COLUMN origin_seq INTEGER;
|
|
1305
|
+
CREATE UNIQUE INDEX plan_ci_by_origin
|
|
1306
|
+
ON plan_ci (origin_store, origin_seq)
|
|
1307
|
+
WHERE origin_store IS NOT NULL;
|
|
1308
|
+
`
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
id: "store-meta",
|
|
1312
|
+
breaks: [],
|
|
1313
|
+
sql: `
|
|
1314
|
+
CREATE TABLE store_meta (
|
|
1315
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1316
|
+
store_id TEXT NOT NULL CHECK (store_id <> ''),
|
|
1317
|
+
project_root_commit TEXT NOT NULL CHECK (project_root_commit <> ''),
|
|
1318
|
+
project_remote TEXT CHECK (project_remote <> ''),
|
|
1319
|
+
host_id TEXT NOT NULL CHECK (host_id <> ''),
|
|
1320
|
+
store_path TEXT NOT NULL CHECK (store_path <> ''),
|
|
1321
|
+
file_dev INTEGER NOT NULL CHECK (typeof(file_dev) = 'integer'),
|
|
1322
|
+
file_ino INTEGER NOT NULL CHECK (typeof(file_ino) = 'integer'),
|
|
1323
|
+
minted_at TEXT NOT NULL CHECK (minted_at <> '')
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
CREATE TABLE merges (
|
|
1327
|
+
seq INTEGER PRIMARY KEY,
|
|
1328
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1329
|
+
other_store TEXT CHECK (other_store <> ''),
|
|
1330
|
+
merged_at TEXT NOT NULL CHECK (merged_at <> ''),
|
|
1331
|
+
rows_added INTEGER NOT NULL CHECK (typeof(rows_added) = 'integer' AND rows_added >= 0),
|
|
1332
|
+
rows_skipped INTEGER NOT NULL CHECK (typeof(rows_skipped) = 'integer' AND rows_skipped >= 0),
|
|
1333
|
+
rows_in_conflict INTEGER NOT NULL CHECK (typeof(rows_in_conflict) = 'integer' AND rows_in_conflict >= 0)
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
CREATE TABLE merge_conflicts (
|
|
1337
|
+
seq INTEGER PRIMARY KEY,
|
|
1338
|
+
merge_id TEXT NOT NULL CHECK (merge_id <> ''),
|
|
1339
|
+
table_name TEXT NOT NULL CHECK (table_name <> ''),
|
|
1340
|
+
local_seq INTEGER NOT NULL CHECK (typeof(local_seq) = 'integer'),
|
|
1341
|
+
field TEXT CHECK (field <> ''),
|
|
1342
|
+
incoming TEXT NOT NULL CHECK (json_valid(incoming) AND json_type(incoming) = 'object'),
|
|
1343
|
+
recorded_at TEXT NOT NULL CHECK (recorded_at <> '')
|
|
1344
|
+
);
|
|
1345
|
+
`
|
|
929
1346
|
}
|
|
930
1347
|
];
|
|
931
1348
|
var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
|
|
@@ -1080,188 +1497,1752 @@ function usablePlan(store, catalogue, access, caller) {
|
|
|
1080
1497
|
throw new SchemaRefusedError(plan);
|
|
1081
1498
|
return plan;
|
|
1082
1499
|
}
|
|
1083
|
-
function hasWork(plan) {
|
|
1084
|
-
return plan.adopted.length > 0 || plan.pending.length > 0;
|
|
1500
|
+
function hasWork(plan) {
|
|
1501
|
+
return plan.adopted.length > 0 || plan.pending.length > 0;
|
|
1502
|
+
}
|
|
1503
|
+
function writeNames(plan) {
|
|
1504
|
+
const adoption = plan.adopted.length > 0 ? [ADOPTION_NAME] : [];
|
|
1505
|
+
return [...adoption, ...plan.pending.map(({ id }) => id)];
|
|
1506
|
+
}
|
|
1507
|
+
function nothingWritten(db) {
|
|
1508
|
+
return { adopted: [], applied: [], userVersion: readUserVersion(db) };
|
|
1509
|
+
}
|
|
1510
|
+
function readUserVersion(db) {
|
|
1511
|
+
const row = db.query("PRAGMA user_version").get();
|
|
1512
|
+
return row?.user_version ?? 0;
|
|
1513
|
+
}
|
|
1514
|
+
function readMigrationLog(db, path) {
|
|
1515
|
+
const table = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE);
|
|
1516
|
+
if (table === null)
|
|
1517
|
+
return null;
|
|
1518
|
+
return db.query(`SELECT id, sha256, breaks, applied_at, applied_by FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map((row) => ({
|
|
1519
|
+
id: row.id,
|
|
1520
|
+
sha256: row.sha256,
|
|
1521
|
+
breaks: parseBreaks(row.breaks, row.id, path),
|
|
1522
|
+
appliedAt: row.applied_at,
|
|
1523
|
+
appliedBy: row.applied_by
|
|
1524
|
+
}));
|
|
1525
|
+
}
|
|
1526
|
+
function parseBreaks(text, id, path) {
|
|
1527
|
+
const refused = new Error(`effort store: ${path} logs migration ${id} with breaks ${text},` + " which is not a JSON array of words; refusing to read or write it");
|
|
1528
|
+
let parsed;
|
|
1529
|
+
try {
|
|
1530
|
+
parsed = JSON.parse(text);
|
|
1531
|
+
} catch (cause) {
|
|
1532
|
+
throw new Error(refused.message, { cause });
|
|
1533
|
+
}
|
|
1534
|
+
if (!Array.isArray(parsed) || !parsed.every((word) => typeof word === "string"))
|
|
1535
|
+
throw refused;
|
|
1536
|
+
return parsed;
|
|
1537
|
+
}
|
|
1538
|
+
function bodyOf(migrations, id) {
|
|
1539
|
+
const migration = migrations.find((candidate) => candidate.id === id);
|
|
1540
|
+
if (migration === undefined)
|
|
1541
|
+
throw new Error(`bring forward: no migration ${id} in the catalogue it planned from`);
|
|
1542
|
+
return migration.sql;
|
|
1543
|
+
}
|
|
1544
|
+
function logMigration(db, entry, stamp) {
|
|
1545
|
+
db.run(`INSERT INTO ${MIGRATION_LOG_TABLE} (id, sha256, breaks, applied_at, applied_by) VALUES (?, ?, ?, ?, ?)`, [entry.id, entry.sha256, JSON.stringify(entry.breaks), stamp.appliedAt, stamp.appliedBy]);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/effort/store/origins.ts
|
|
1549
|
+
var ACROSS_STORES_ORDER = "collected_at, origin_store, origin_seq, seq";
|
|
1550
|
+
var STAMPED_COLUMNS = "seq, origin_store, origin_seq";
|
|
1551
|
+
var STORE_ID = "(SELECT store_id FROM store_meta WHERE id = 1)";
|
|
1552
|
+
function stampedValues(table) {
|
|
1553
|
+
const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${table})`;
|
|
1554
|
+
return `${nextSeq}, ${STORE_ID}, CASE WHEN ${STORE_ID} IS NULL THEN NULL ELSE ${nextSeq} END`;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/effort/store/settings.ts
|
|
1558
|
+
var DEFAULT_STORE_SETTINGS = Object.freeze({
|
|
1559
|
+
busyTimeoutMs: CONFIG_DEFAULTS.effortBusyTimeoutMs
|
|
1560
|
+
});
|
|
1561
|
+
var active = DEFAULT_STORE_SETTINGS;
|
|
1562
|
+
function setActiveStoreSettings(settings) {
|
|
1563
|
+
if (settings === null) {
|
|
1564
|
+
active = DEFAULT_STORE_SETTINGS;
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (!isBusyTimeoutMs(settings.busyTimeoutMs)) {
|
|
1568
|
+
throw new RangeError(`effort store: busy timeout ${String(settings.busyTimeoutMs)} is not a whole number ` + `of milliseconds from ${String(BUSY_TIMEOUT_MIN_MS)} to ${String(BUSY_TIMEOUT_MAX_MS)}`);
|
|
1569
|
+
}
|
|
1570
|
+
active = Object.freeze({ busyTimeoutMs: settings.busyTimeoutMs });
|
|
1571
|
+
}
|
|
1572
|
+
function activeStoreSettings() {
|
|
1573
|
+
return active;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/effort/store/store-meta.ts
|
|
1577
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1578
|
+
import { dirname as dirname8 } from "path";
|
|
1579
|
+
|
|
1580
|
+
// src/effort/store/store-identity.ts
|
|
1581
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1582
|
+
import { createHmac } from "crypto";
|
|
1583
|
+
import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync } from "fs";
|
|
1584
|
+
import { hostname, platform as currentPlatform } from "os";
|
|
1585
|
+
|
|
1586
|
+
// src/schema/project-id.ts
|
|
1587
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1588
|
+
var PROJECT_ID_LENGTH = 12;
|
|
1589
|
+
var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
|
|
1590
|
+
function hasColonAfterAt(url) {
|
|
1591
|
+
const at = url.indexOf("@");
|
|
1592
|
+
return at !== -1 && url.lastIndexOf(":") > at;
|
|
1593
|
+
}
|
|
1594
|
+
function normalizeRemote(url) {
|
|
1595
|
+
const stripped = url.replace(/:\/\/[^@]+@/, "://");
|
|
1596
|
+
const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
|
|
1597
|
+
const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
1598
|
+
return network ? trimmed.toLowerCase() : trimmed;
|
|
1599
|
+
}
|
|
1600
|
+
function gitRemoteUrl(dir) {
|
|
1601
|
+
const result = spawnSync2("git", ["remote", "get-url", "origin"], {
|
|
1602
|
+
cwd: dir,
|
|
1603
|
+
encoding: "utf8",
|
|
1604
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
1605
|
+
});
|
|
1606
|
+
if (result.error !== undefined || result.status !== 0)
|
|
1607
|
+
return null;
|
|
1608
|
+
const url = result.stdout.trim();
|
|
1609
|
+
return url === "" ? null : url;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/effort/store/store-identity.ts
|
|
1613
|
+
var HOST_ID_KEY = "rafa effort store host id";
|
|
1614
|
+
var MACHINE_ID_PATH = "/etc/machine-id";
|
|
1615
|
+
var IOREG_ARGS = ["-rd1", "-c", "IOPlatformExpertDevice"];
|
|
1616
|
+
function hostIdFrom(source, value) {
|
|
1617
|
+
return createHmac("sha256", HOST_ID_KEY).update(`${source}:${value}`).digest("hex");
|
|
1618
|
+
}
|
|
1619
|
+
function readMachineIdFile() {
|
|
1620
|
+
try {
|
|
1621
|
+
const value = readFileSync6(MACHINE_ID_PATH, "utf8").trim();
|
|
1622
|
+
return value === "" ? null : value;
|
|
1623
|
+
} catch {
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
function parseIoregPlatformUuid(output) {
|
|
1628
|
+
const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(output);
|
|
1629
|
+
return match?.[1] ?? null;
|
|
1630
|
+
}
|
|
1631
|
+
function readIoregPlatformUuid() {
|
|
1632
|
+
const result = spawnSync3("ioreg", [...IOREG_ARGS], { encoding: "utf8" });
|
|
1633
|
+
if (result.error !== undefined || result.status !== 0)
|
|
1634
|
+
return null;
|
|
1635
|
+
return parseIoregPlatformUuid(result.stdout);
|
|
1636
|
+
}
|
|
1637
|
+
function readHostId(seams = {}) {
|
|
1638
|
+
const platform = seams.platform ?? currentPlatform();
|
|
1639
|
+
if (platform === "linux") {
|
|
1640
|
+
const machineId = (seams.readMachineId ?? readMachineIdFile)();
|
|
1641
|
+
if (machineId !== null)
|
|
1642
|
+
return hostIdFrom("machine-id", machineId);
|
|
1643
|
+
}
|
|
1644
|
+
if (platform === "darwin") {
|
|
1645
|
+
const platformUuid = (seams.readPlatformUuid ?? readIoregPlatformUuid)();
|
|
1646
|
+
if (platformUuid !== null)
|
|
1647
|
+
return hostIdFrom("platform-uuid", platformUuid);
|
|
1648
|
+
}
|
|
1649
|
+
return hostIdFrom("hostname", (seams.readHostname ?? hostname)());
|
|
1650
|
+
}
|
|
1651
|
+
function readStoreFileFacts(path) {
|
|
1652
|
+
const storePath = realpathSync4(path);
|
|
1653
|
+
const stats = statSync(storePath, { bigint: true });
|
|
1654
|
+
return { storePath, fileDev: stats.dev, fileIno: stats.ino };
|
|
1655
|
+
}
|
|
1656
|
+
function observeStore(path, readHost = readHostId) {
|
|
1657
|
+
return { hostId: readHost(), ...readStoreFileFacts(path) };
|
|
1658
|
+
}
|
|
1659
|
+
function movedFacts(recorded, facts) {
|
|
1660
|
+
const sameFile = BigInt(recorded.fileDev) === facts.fileDev && BigInt(recorded.fileIno) === facts.fileIno;
|
|
1661
|
+
const moved = [
|
|
1662
|
+
["host", recorded.hostId !== facts.hostId],
|
|
1663
|
+
["path", recorded.storePath !== facts.storePath],
|
|
1664
|
+
["file", !sameFile]
|
|
1665
|
+
];
|
|
1666
|
+
return moved.filter(([, differs]) => differs).map(([reason]) => reason);
|
|
1667
|
+
}
|
|
1668
|
+
function decideStoreIdentity(access, recorded, observe) {
|
|
1669
|
+
if (access === "read")
|
|
1670
|
+
return { action: "none" };
|
|
1671
|
+
const facts = observe();
|
|
1672
|
+
if (recorded === null)
|
|
1673
|
+
return { action: "mint", reasons: ["unminted"], facts };
|
|
1674
|
+
const reasons = movedFacts(recorded, facts);
|
|
1675
|
+
return reasons.length === 0 ? { action: "keep", storeId: recorded.storeId } : { action: "mint", reasons, facts };
|
|
1676
|
+
}
|
|
1677
|
+
function smallestRootCommit(git) {
|
|
1678
|
+
const result = git(["rev-list", "--max-parents=0", "HEAD"]);
|
|
1679
|
+
if (!result.ok)
|
|
1680
|
+
return null;
|
|
1681
|
+
const roots = result.stdout.split(`
|
|
1682
|
+
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
1683
|
+
return [...roots].sort()[0] ?? null;
|
|
1684
|
+
}
|
|
1685
|
+
function normalisedRemote(git) {
|
|
1686
|
+
const result = git(["remote", "get-url", "origin"]);
|
|
1687
|
+
if (!result.ok)
|
|
1688
|
+
return null;
|
|
1689
|
+
const remote = normalizeRemote(result.stdout.trim());
|
|
1690
|
+
return remote === "" ? null : remote;
|
|
1691
|
+
}
|
|
1692
|
+
function readProjectIdentity(dir, git = createGitRunner(dir)) {
|
|
1693
|
+
return { rootCommit: smallestRootCommit(git), remote: normalisedRemote(git) };
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/effort/store/store-meta.ts
|
|
1697
|
+
var STORED_INTEGER_BITS = 64;
|
|
1698
|
+
function toStoredInteger(value) {
|
|
1699
|
+
return BigInt.asIntN(STORED_INTEGER_BITS, value);
|
|
1700
|
+
}
|
|
1701
|
+
function fromStoredInteger(text) {
|
|
1702
|
+
return BigInt.asUintN(STORED_INTEGER_BITS, BigInt(text));
|
|
1703
|
+
}
|
|
1704
|
+
function readStoreMeta(db) {
|
|
1705
|
+
const row = db.query("SELECT store_id, project_root_commit, project_remote, host_id, store_path," + " CAST(file_dev AS TEXT) AS file_dev, CAST(file_ino AS TEXT) AS file_ino, minted_at" + " FROM store_meta WHERE id = 1").get();
|
|
1706
|
+
if (row === null)
|
|
1707
|
+
return null;
|
|
1708
|
+
return {
|
|
1709
|
+
storeId: row.store_id,
|
|
1710
|
+
projectRootCommit: row.project_root_commit,
|
|
1711
|
+
projectRemote: row.project_remote,
|
|
1712
|
+
hostId: row.host_id,
|
|
1713
|
+
storePath: row.store_path,
|
|
1714
|
+
fileDev: fromStoredInteger(row.file_dev),
|
|
1715
|
+
fileIno: fromStoredInteger(row.file_ino),
|
|
1716
|
+
mintedAt: row.minted_at
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function projectFor(storePath, recorded, seams) {
|
|
1720
|
+
const read = (seams.readProject ?? readProjectIdentity)(dirname8(storePath));
|
|
1721
|
+
if (read.rootCommit !== null)
|
|
1722
|
+
return { rootCommit: read.rootCommit, remote: read.remote };
|
|
1723
|
+
if (recorded === null)
|
|
1724
|
+
return null;
|
|
1725
|
+
return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
|
|
1726
|
+
}
|
|
1727
|
+
function writeStoreMeta(db, facts, project, seams) {
|
|
1728
|
+
const storeId = (seams.newStoreId ?? randomUUID3)();
|
|
1729
|
+
const mintedAt = (seams.now?.() ?? new Date).toISOString();
|
|
1730
|
+
db.query("INSERT OR REPLACE INTO store_meta (id, store_id, project_root_commit, project_remote," + " host_id, store_path, file_dev, file_ino, minted_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)").run(storeId, project.rootCommit, project.remote, facts.hostId, facts.storePath, toStoredInteger(facts.fileDev), toStoredInteger(facts.fileIno), mintedAt);
|
|
1731
|
+
return storeId;
|
|
1732
|
+
}
|
|
1733
|
+
function unminted(decision) {
|
|
1734
|
+
return decision.action === "keep" ? { action: "keep", storeId: decision.storeId } : { action: "none" };
|
|
1735
|
+
}
|
|
1736
|
+
function settleStoreIdentity(db, path, access, seams = {}) {
|
|
1737
|
+
if (access === "read")
|
|
1738
|
+
return { action: "none" };
|
|
1739
|
+
const observe = () => observeStore(path, seams.readHostId ?? readHostId);
|
|
1740
|
+
const recorded = readStoreMeta(db);
|
|
1741
|
+
const first = decideStoreIdentity(access, recorded, observe);
|
|
1742
|
+
if (first.action !== "mint")
|
|
1743
|
+
return unminted(first);
|
|
1744
|
+
const project = projectFor(first.facts.storePath, recorded, seams);
|
|
1745
|
+
if (project === null)
|
|
1746
|
+
return { action: "no-project", reasons: first.reasons };
|
|
1747
|
+
const mintUnderLock = db.transaction(() => {
|
|
1748
|
+
const second = decideStoreIdentity(access, readStoreMeta(db), observe);
|
|
1749
|
+
if (second.action !== "mint")
|
|
1750
|
+
return unminted(second);
|
|
1751
|
+
const storeId = writeStoreMeta(db, second.facts, project, seams);
|
|
1752
|
+
return { action: "mint", storeId, reasons: second.reasons };
|
|
1753
|
+
});
|
|
1754
|
+
return mintUnderLock.immediate();
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// src/effort/store/sqlite.ts
|
|
1758
|
+
var SQLITE_STORE_FILE_NAME = "effort.sqlite";
|
|
1759
|
+
function sqliteStorePath(repoRoot) {
|
|
1760
|
+
return join9(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
|
|
1761
|
+
}
|
|
1762
|
+
var KIND_TABLES = {
|
|
1763
|
+
sessions: { name: "sessions", keyColumn: "session_id" },
|
|
1764
|
+
commits: { name: "commits", keyColumn: "sha" }
|
|
1765
|
+
};
|
|
1766
|
+
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
1767
|
+
function hasKey(entry) {
|
|
1768
|
+
return entry.key !== null;
|
|
1769
|
+
}
|
|
1770
|
+
function refused(index, length, reason) {
|
|
1771
|
+
return new Error(`effort store: row ${index} of ${length} ${reason}; batch refused`);
|
|
1772
|
+
}
|
|
1773
|
+
function batchEntries(kind, rows) {
|
|
1774
|
+
const keyOf = EFFORT_KEY_PROJECTIONS[kind];
|
|
1775
|
+
const projected = rows.map((row) => ({ key: keyOf(row), row }));
|
|
1776
|
+
const keyed = projected.filter(hasKey);
|
|
1777
|
+
if (keyed.length < projected.length) {
|
|
1778
|
+
const keylessAt = projected.findIndex((entry) => !hasKey(entry));
|
|
1779
|
+
throw refused(keylessAt, rows.length, "carries no key");
|
|
1780
|
+
}
|
|
1781
|
+
const unholdableAt = keyed.findIndex(({ key }) => LONE_SURROGATE.test(key));
|
|
1782
|
+
if (unholdableAt !== -1) {
|
|
1783
|
+
const reason = "carries a key with a lone UTF-16 surrogate," + " which SQLite text cannot hold";
|
|
1784
|
+
throw refused(unholdableAt, rows.length, reason);
|
|
1785
|
+
}
|
|
1786
|
+
return keyed.map(({ key, row }) => ({ key, body: JSON.stringify(row) }));
|
|
1787
|
+
}
|
|
1788
|
+
function withSqliteStore(path, access, create, use, identity = {}) {
|
|
1789
|
+
guardTestProcess(path);
|
|
1790
|
+
if (create)
|
|
1791
|
+
mkdirSync5(dirname9(path), { recursive: true });
|
|
1792
|
+
const db = new Database(path, { readwrite: true, create });
|
|
1793
|
+
try {
|
|
1794
|
+
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
1795
|
+
bringForward(db, path, access, "open");
|
|
1796
|
+
settleStoreIdentity(db, path, access, identity);
|
|
1797
|
+
return use(db);
|
|
1798
|
+
} finally {
|
|
1799
|
+
db.close();
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function insertBatch(db, table, entries) {
|
|
1803
|
+
const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json, ${STAMPED_COLUMNS})` + ` VALUES (?, ?, ${stampedValues(table.name)})` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
|
|
1804
|
+
const insertAll = db.transaction(() => entries.reduce((appended, { key, body }) => appended + insert.run(key, body).changes, 0));
|
|
1805
|
+
return insertAll.immediate();
|
|
1806
|
+
}
|
|
1807
|
+
function writeSqliteStore(path, rowCount, empty, insert) {
|
|
1808
|
+
const exists = existsSync5(path);
|
|
1809
|
+
if (rowCount > 0)
|
|
1810
|
+
return withSqliteStore(path, "write", !exists, insert);
|
|
1811
|
+
if (exists)
|
|
1812
|
+
withSqliteStore(path, "write", false, () => {
|
|
1813
|
+
return;
|
|
1814
|
+
});
|
|
1815
|
+
return empty;
|
|
1816
|
+
}
|
|
1817
|
+
function appendKind2(path, kind, rows) {
|
|
1818
|
+
const entries = batchEntries(kind, rows);
|
|
1819
|
+
const table = KIND_TABLES[kind];
|
|
1820
|
+
const appended = writeSqliteStore(path, entries.length, 0, (db) => insertBatch(db, table, entries));
|
|
1821
|
+
return { path, appended, skipped: entries.length - appended };
|
|
1822
|
+
}
|
|
1823
|
+
function keysOfKind2(path, kind) {
|
|
1824
|
+
if (!existsSync5(path))
|
|
1825
|
+
return new Set;
|
|
1826
|
+
const { name: name2, keyColumn } = KIND_TABLES[kind];
|
|
1827
|
+
const keys = withSqliteStore(path, "read", false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${name2} ORDER BY seq`).all());
|
|
1828
|
+
return new Set(keys.map(({ key }) => key));
|
|
1829
|
+
}
|
|
1830
|
+
function parsedObject(body) {
|
|
1831
|
+
try {
|
|
1832
|
+
const value = JSON.parse(body);
|
|
1833
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1834
|
+
} catch {
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function rowOf(body, where) {
|
|
1839
|
+
const value = parsedObject(body);
|
|
1840
|
+
if (value === null) {
|
|
1841
|
+
throw new Error(`effort store: ${where} does not hold a JSON object;` + " no append writes one");
|
|
1842
|
+
}
|
|
1843
|
+
return value;
|
|
1844
|
+
}
|
|
1845
|
+
function readKind2(path, kind) {
|
|
1846
|
+
if (!existsSync5(path))
|
|
1847
|
+
return [];
|
|
1848
|
+
const { name: name2 } = KIND_TABLES[kind];
|
|
1849
|
+
const stored = withSqliteStore(path, "read", false, (db) => db.query(`SELECT seq, row_json AS body FROM ${name2} ORDER BY seq`).all());
|
|
1850
|
+
return stored.map(({ seq, body }) => rowOf(body, `${path}: ${name2} row ${seq}`));
|
|
1851
|
+
}
|
|
1852
|
+
function openSqliteStore(repoRoot) {
|
|
1853
|
+
const path = sqliteStorePath(repoRoot);
|
|
1854
|
+
return {
|
|
1855
|
+
append: (kind, rows) => appendKind2(path, kind, rows),
|
|
1856
|
+
keys: (kind) => keysOfKind2(path, kind),
|
|
1857
|
+
read: (kind) => readKind2(path, kind),
|
|
1858
|
+
path: () => path
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// src/effort/sync/file.ts
|
|
1863
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1864
|
+
import { join as join12, resolve as resolve5 } from "path";
|
|
1865
|
+
|
|
1866
|
+
// src/effort/store/fix-schema.ts
|
|
1867
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1868
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
1869
|
+
|
|
1870
|
+
// src/effort/store/rebuild-aside.ts
|
|
1871
|
+
import { existsSync as existsSync6, renameSync as renameSync4, rmSync as rmSync4 } from "fs";
|
|
1872
|
+
|
|
1873
|
+
class RebuildRefusal extends Error {
|
|
1874
|
+
constructor(message) {
|
|
1875
|
+
super(message);
|
|
1876
|
+
this.name = "RebuildRefusal";
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
function quoted(name2) {
|
|
1880
|
+
return `"${name2.replaceAll('"', '""')}"`;
|
|
1881
|
+
}
|
|
1882
|
+
function count(db, sql) {
|
|
1883
|
+
return db.query(sql).get()?.n ?? 0;
|
|
1884
|
+
}
|
|
1885
|
+
function refuseInFlight(path) {
|
|
1886
|
+
for (const suffix of ["-journal", "-wal"]) {
|
|
1887
|
+
if (existsSync6(`${path}${suffix}`)) {
|
|
1888
|
+
throw new RebuildRefusal(`${path}${suffix} is there, so a write is in flight or was interrupted; stop whatever uses the store` + " and open it once with rafa before repairing it");
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
function checkedCounts(db, tables, original, added = {}) {
|
|
1893
|
+
return tables.map((table) => {
|
|
1894
|
+
const rows = count(db, `SELECT count(*) AS n FROM main.${quoted(table)}`);
|
|
1895
|
+
const originalRows = count(db, `SELECT count(*) AS n FROM ${original}.${quoted(table)}`);
|
|
1896
|
+
const addedRows = added[table] ?? 0;
|
|
1897
|
+
if (rows !== originalRows + addedRows) {
|
|
1898
|
+
const plus = addedRows === 0 ? "" : ` plus ${String(addedRows)} added`;
|
|
1899
|
+
throw new RebuildRefusal(`the rebuilt ${table} holds ${String(rows)} rows, the store ${String(originalRows)}${plus}`);
|
|
1900
|
+
}
|
|
1901
|
+
return { table, rows };
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
function refuseCorrupt(db) {
|
|
1905
|
+
const answer = db.query("PRAGMA main.integrity_check").get()?.integrity_check;
|
|
1906
|
+
if (answer !== "ok")
|
|
1907
|
+
throw new RebuildRefusal(`the rebuilt store failed integrity_check: ${answer ?? "no answer"}`);
|
|
1908
|
+
}
|
|
1909
|
+
function removeParallel(parallelPath) {
|
|
1910
|
+
rmSync4(parallelPath, { force: true });
|
|
1911
|
+
rmSync4(`${parallelPath}-journal`, { force: true });
|
|
1912
|
+
}
|
|
1913
|
+
function swapIn(path, parallelPath, backupPath) {
|
|
1914
|
+
renameSync4(path, backupPath);
|
|
1915
|
+
try {
|
|
1916
|
+
renameSync4(parallelPath, path);
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
renameSync4(backupPath, path);
|
|
1919
|
+
throw error;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
function refuseTaken(paths) {
|
|
1923
|
+
for (const taken of paths) {
|
|
1924
|
+
if (existsSync6(taken))
|
|
1925
|
+
throw new RebuildRefusal(`${taken} is already there; move it aside or pass another stamp`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function rebuildAside(options) {
|
|
1929
|
+
const { path, parallelPath, backupPath, dryRun, build } = options;
|
|
1930
|
+
refuseInFlight(path);
|
|
1931
|
+
refuseTaken([parallelPath, backupPath]);
|
|
1932
|
+
let built;
|
|
1933
|
+
try {
|
|
1934
|
+
built = build(parallelPath);
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
removeParallel(parallelPath);
|
|
1937
|
+
throw error;
|
|
1938
|
+
}
|
|
1939
|
+
if (dryRun) {
|
|
1940
|
+
removeParallel(parallelPath);
|
|
1941
|
+
return { built, backupPath: null };
|
|
1942
|
+
}
|
|
1943
|
+
swapIn(path, parallelPath, backupPath);
|
|
1944
|
+
return { built, backupPath };
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// src/effort/store/fix-schema.ts
|
|
1948
|
+
var REPAIRABLE_REASONS = [
|
|
1949
|
+
"pre-log-unreleased",
|
|
1950
|
+
"gate-mismatch",
|
|
1951
|
+
"edited",
|
|
1952
|
+
"unknown-breaks-readers",
|
|
1953
|
+
"unknown-breaks-writers"
|
|
1954
|
+
];
|
|
1955
|
+
var LIVE = "live";
|
|
1956
|
+
function tableNames(db, schema) {
|
|
1957
|
+
return db.query(`SELECT name FROM ${schema}.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ? ORDER BY name`).all(MIGRATION_LOG_TABLE).map((row) => row.name);
|
|
1958
|
+
}
|
|
1959
|
+
function columnNames(db, schema, table) {
|
|
1960
|
+
return db.query("SELECT name FROM pragma_table_info(?, ?)").all(table, schema).map((row) => row.name);
|
|
1961
|
+
}
|
|
1962
|
+
function readOnlySchema(path) {
|
|
1963
|
+
const db = new Database2(path, { readonly: true });
|
|
1964
|
+
try {
|
|
1965
|
+
return readStoreSchema(db, path);
|
|
1966
|
+
} finally {
|
|
1967
|
+
db.close();
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
function refuseRemoved(db, known, live) {
|
|
1971
|
+
for (const table of known) {
|
|
1972
|
+
if (!live.has(table)) {
|
|
1973
|
+
throw new RebuildRefusal(`the store holds no table ${table}, which this rafa writes; its newer schema is not additive`);
|
|
1974
|
+
}
|
|
1975
|
+
const liveColumns = new Set(columnNames(db, LIVE, table));
|
|
1976
|
+
const missing = columnNames(db, "main", table).filter((column) => !liveColumns.has(column));
|
|
1977
|
+
if (missing.length > 0) {
|
|
1978
|
+
throw new RebuildRefusal(`the store holds no column ${missing.map((column) => `${table}.${column}`).join(", ")}, which this rafa` + " writes; its newer schema is not additive");
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
function copyKnown(db, known) {
|
|
1983
|
+
db.transaction(() => {
|
|
1984
|
+
for (const table of known) {
|
|
1985
|
+
const columns = columnNames(db, "main", table).map(quoted).join(", ");
|
|
1986
|
+
db.run(`INSERT INTO main.${quoted(table)} (${columns}) SELECT ${columns} FROM ${LIVE}.${quoted(table)}`);
|
|
1987
|
+
}
|
|
1988
|
+
})();
|
|
1989
|
+
}
|
|
1990
|
+
function leftBehind(db, known, live) {
|
|
1991
|
+
const leftTables = live.filter((table) => !known.has(table)).map((table) => ({ table, rows: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)}`) }));
|
|
1992
|
+
const leftColumns = live.filter((table) => known.has(table)).flatMap((table) => {
|
|
1993
|
+
const kept = new Set(columnNames(db, "main", table));
|
|
1994
|
+
return columnNames(db, LIVE, table).filter((column) => !kept.has(column)).map((column) => ({
|
|
1995
|
+
table,
|
|
1996
|
+
column,
|
|
1997
|
+
values: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)} WHERE ${quoted(column)} IS NOT NULL`)
|
|
1998
|
+
}));
|
|
1999
|
+
});
|
|
2000
|
+
return { leftTables, leftColumns };
|
|
2001
|
+
}
|
|
2002
|
+
function refuseUnusable(db, parallelPath, migrations) {
|
|
2003
|
+
const plan = planSchema(readStoreSchema(db, parallelPath), sqliteCatalogue(migrations), "write", "open");
|
|
2004
|
+
if (plan.verdict === "refuse")
|
|
2005
|
+
throw new RebuildRefusal(`the rebuilt store is refused as ${plan.reason}: ${plan.message}`);
|
|
2006
|
+
const needs = writeNames(plan);
|
|
2007
|
+
if (needs.length > 0)
|
|
2008
|
+
throw new RebuildRefusal(`the rebuilt store still needs ${needs.join(", ")}`);
|
|
2009
|
+
}
|
|
2010
|
+
function buildParallel(path, parallelPath, migrations, stamp) {
|
|
2011
|
+
const db = new Database2(parallelPath, { create: true, readwrite: true });
|
|
2012
|
+
try {
|
|
2013
|
+
bringForward(db, parallelPath, "write", "open", { migrations, ...stamp, builtAside: true });
|
|
2014
|
+
db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
|
|
2015
|
+
const known = tableNames(db, "main");
|
|
2016
|
+
const live = tableNames(db, LIVE);
|
|
2017
|
+
refuseRemoved(db, known, new Set(live));
|
|
2018
|
+
copyKnown(db, known);
|
|
2019
|
+
const kept = checkedCounts(db, known, LIVE);
|
|
2020
|
+
const left = leftBehind(db, new Set(known), live);
|
|
2021
|
+
db.run(`DETACH DATABASE ${LIVE}`);
|
|
2022
|
+
refuseCorrupt(db);
|
|
2023
|
+
refuseUnusable(db, parallelPath, migrations);
|
|
2024
|
+
return { kept, ...left };
|
|
2025
|
+
} finally {
|
|
2026
|
+
db.close();
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
function asidePaths(path, version2, stamp) {
|
|
2030
|
+
return { parallelPath: `${path}.fix-${stamp}`, backupPath: `${path}.v${String(version2)}-${stamp}.bak` };
|
|
2031
|
+
}
|
|
2032
|
+
function isRepairable(reason) {
|
|
2033
|
+
return REPAIRABLE_REASONS.includes(reason);
|
|
2034
|
+
}
|
|
2035
|
+
function refuseDevelopmentSwap(path, reason, identity) {
|
|
2036
|
+
if (identity.kind !== "development")
|
|
2037
|
+
return;
|
|
2038
|
+
throw new RebuildRefusal(`${path} needs a rebuild (${reason}), and this rafa is a development build (${identity.checkout}), whose` + " migrations may be a branch's; a development build never swaps a rebuild in. Nothing was changed, and" + " --dry-run runs from here. Run the swap from an installed rafa. Next safe step: rafa effort fix-schema");
|
|
2039
|
+
}
|
|
2040
|
+
function fixStoreSchema(options) {
|
|
2041
|
+
const { path, dryRun, stamp } = options;
|
|
2042
|
+
guardTestProcess(path);
|
|
2043
|
+
const migrations = options.migrations ?? SQLITE_MIGRATIONS;
|
|
2044
|
+
const identity = options.identity ?? readRuntimeIdentity();
|
|
2045
|
+
const appliedBy = appliedByName(identity);
|
|
2046
|
+
const known = migrations.map(({ id }) => id);
|
|
2047
|
+
const nothing = { known, appliedBy, pending: [], kept: [], leftTables: [], leftColumns: [], backupPath: null };
|
|
2048
|
+
if (!existsSync7(path)) {
|
|
2049
|
+
return { path, status: "missing", storeVersion: null, logged: false, reason: null, refusal: null, unknown: [], ...nothing };
|
|
2050
|
+
}
|
|
2051
|
+
refuseInFlight(path);
|
|
2052
|
+
const store = readOnlySchema(path);
|
|
2053
|
+
const knownIds = new Set(known);
|
|
2054
|
+
const found = {
|
|
2055
|
+
path,
|
|
2056
|
+
storeVersion: store.userVersion,
|
|
2057
|
+
logged: store.log !== null,
|
|
2058
|
+
unknown: (store.log ?? []).filter(({ id }) => !knownIds.has(id))
|
|
2059
|
+
};
|
|
2060
|
+
const plan = planSchema(store, sqliteCatalogue(migrations), "write", "open");
|
|
2061
|
+
if (plan.verdict === "use") {
|
|
2062
|
+
const pending = writeNames(plan);
|
|
2063
|
+
const status2 = pending.length === 0 ? "current" : "behind";
|
|
2064
|
+
return { ...found, ...nothing, status: status2, reason: null, refusal: null, pending };
|
|
2065
|
+
}
|
|
2066
|
+
if (!isRepairable(plan.reason)) {
|
|
2067
|
+
throw new RebuildRefusal(`a rebuild at this rafa's migrations does not repair ${plan.reason}. ${plan.message}`);
|
|
2068
|
+
}
|
|
2069
|
+
if (!dryRun)
|
|
2070
|
+
refuseDevelopmentSwap(path, plan.reason, identity);
|
|
2071
|
+
const { built, backupPath } = rebuildAside({
|
|
2072
|
+
path,
|
|
2073
|
+
...asidePaths(path, store.userVersion, stamp),
|
|
2074
|
+
dryRun,
|
|
2075
|
+
build: (parallelPath) => buildParallel(path, parallelPath, migrations, {
|
|
2076
|
+
appliedBy,
|
|
2077
|
+
now: options.now ?? (() => new Date)
|
|
2078
|
+
})
|
|
2079
|
+
});
|
|
2080
|
+
const status = dryRun ? "would-rebuild" : "rebuilt";
|
|
2081
|
+
const refusal = withoutWayOut(plan.message, plan.nextStep);
|
|
2082
|
+
return { ...found, ...nothing, status, reason: plan.reason, refusal, ...built, backupPath };
|
|
2083
|
+
}
|
|
2084
|
+
function withoutWayOut(message, nextStep) {
|
|
2085
|
+
const ending = ` Next safe step: ${nextStep}`;
|
|
2086
|
+
return message.endsWith(ending) ? message.slice(0, -ending.length) : message;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/commands/plan/plan-files.ts
|
|
2090
|
+
import { statSync as statSync2 } from "fs";
|
|
2091
|
+
import { resolve as resolve3 } from "path";
|
|
2092
|
+
|
|
2093
|
+
// src/config-load.ts
|
|
2094
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, realpathSync as realpathSync5 } from "fs";
|
|
2095
|
+
import { isAbsolute as isAbsolute6 } from "path";
|
|
2096
|
+
function readConfigFile(root) {
|
|
2097
|
+
const path = configFilePath(root);
|
|
2098
|
+
if (!existsSync8(path))
|
|
2099
|
+
return null;
|
|
2100
|
+
let text;
|
|
2101
|
+
try {
|
|
2102
|
+
text = readFileSync7(path, "utf8");
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
const problem = `${path}: cannot be read (${messageOf(error)})`;
|
|
2105
|
+
throw new ConfigError([problem], { cause: error });
|
|
2106
|
+
}
|
|
2107
|
+
return parseConfigText(text, path);
|
|
2108
|
+
}
|
|
2109
|
+
function isSameDirectory(first, second) {
|
|
2110
|
+
try {
|
|
2111
|
+
return realpathSync5(first) === realpathSync5(second);
|
|
2112
|
+
} catch {
|
|
2113
|
+
return false;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function readUserConfigFile({ root, home }) {
|
|
2117
|
+
if (!isAbsolute6(home)) {
|
|
2118
|
+
throw new ConfigError([
|
|
2119
|
+
`home directory is ${describeValue(home)}, expected an absolute path`
|
|
2120
|
+
]);
|
|
2121
|
+
}
|
|
2122
|
+
return isSameDirectory(root, home) ? null : readConfigFile(home);
|
|
2123
|
+
}
|
|
2124
|
+
function printWarning(message) {
|
|
2125
|
+
activeOutput().warn(message);
|
|
2126
|
+
}
|
|
2127
|
+
function loadConfig(roots, cli = {}, warn = printWarning) {
|
|
2128
|
+
const user = readUserConfigFile(roots);
|
|
2129
|
+
const file = readConfigFile(roots.root);
|
|
2130
|
+
const resolved = resolveConfig({ cli, file, user });
|
|
2131
|
+
for (const warning of resolved.warnings)
|
|
2132
|
+
warn(warning);
|
|
2133
|
+
setActiveStoreSettings({ busyTimeoutMs: resolved.config.effortBusyTimeoutMs });
|
|
2134
|
+
return resolved;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// src/commands/plan/plan-files.ts
|
|
2138
|
+
function plansDirAt(root, planDir) {
|
|
2139
|
+
return { label: planDir, path: resolve3(root, planDir) };
|
|
2140
|
+
}
|
|
2141
|
+
function requireProject(context, command) {
|
|
2142
|
+
if (context.project === null)
|
|
2143
|
+
throw new Error(`${command} runs inside a project, and was handed none`);
|
|
2144
|
+
return context.project;
|
|
2145
|
+
}
|
|
2146
|
+
function resolvePlansDir(project, command, warn) {
|
|
2147
|
+
return plansDirAt(project.root, resolveProjectConfig(project, command, warn).planDir);
|
|
2148
|
+
}
|
|
2149
|
+
function resolveProjectConfig(project, command, warn) {
|
|
2150
|
+
try {
|
|
2151
|
+
return loadConfig({ root: project.root, home: project.home }, {}, warn).config;
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
if (!(error instanceof ConfigError))
|
|
2154
|
+
throw error;
|
|
2155
|
+
throw new CommandExit(1, [
|
|
2156
|
+
`\u274C ${command}: the config cannot be used:`,
|
|
2157
|
+
...error.problems.map((problem) => ` ${problem}`)
|
|
2158
|
+
].join(`
|
|
2159
|
+
`));
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
var CHECKBOXES = {
|
|
2163
|
+
unchecked: "[ ]",
|
|
2164
|
+
blocked: "[BLOCKED]",
|
|
2165
|
+
done: "[x]"
|
|
2166
|
+
};
|
|
2167
|
+
function countTasks(tasks) {
|
|
2168
|
+
const done = tasks.filter((task) => task.status === "done").length;
|
|
2169
|
+
const blocked = tasks.filter((task) => task.status === "blocked").length;
|
|
2170
|
+
return { total: tasks.length, done, blocked, open: tasks.length - done - blocked };
|
|
2171
|
+
}
|
|
2172
|
+
function formatCounts(counts) {
|
|
2173
|
+
return `${counts.done}/${counts.total} done, ${counts.blocked} blocked, ${counts.open} open`;
|
|
2174
|
+
}
|
|
2175
|
+
function checkbox(status) {
|
|
2176
|
+
return CHECKBOXES[status];
|
|
2177
|
+
}
|
|
2178
|
+
function planFileName(stub, tracker) {
|
|
2179
|
+
return tracker ? `PLAN_TRACKER-${stub}.md` : `PLAN-${stub}.md`;
|
|
2180
|
+
}
|
|
2181
|
+
function stubOfPlanFile(name2) {
|
|
2182
|
+
const stub = /^PLAN-(.+)\.md$/.exec(name2)?.[1];
|
|
2183
|
+
return stub !== undefined && isStampableStub(stub) ? stub : null;
|
|
2184
|
+
}
|
|
2185
|
+
function isFile(path) {
|
|
2186
|
+
return statSync2(path, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
2187
|
+
}
|
|
2188
|
+
function issueLine(file, issue) {
|
|
2189
|
+
return `${file}:${issue.line}: ${issue.reason}: ${issue.text}`;
|
|
2190
|
+
}
|
|
2191
|
+
function plural(count2, noun) {
|
|
2192
|
+
return count2 === 1 ? `${count2} ${noun}` : `${count2} ${noun}s`;
|
|
2193
|
+
}
|
|
2194
|
+
function expectNoArgument(args, usage) {
|
|
2195
|
+
if (args.length === 0)
|
|
2196
|
+
return;
|
|
2197
|
+
throw new CommandExit(1, `\u274C Expected no argument, got ${args.length}: ${args.join(" ")}
|
|
2198
|
+
Usage: ${usage}`);
|
|
2199
|
+
}
|
|
2200
|
+
function expectOneArgument(args, usage) {
|
|
2201
|
+
const [only] = args;
|
|
2202
|
+
if (args.length === 1 && only !== undefined)
|
|
2203
|
+
return only;
|
|
2204
|
+
const got = args.length === 0 ? "none" : `${args.length}: ${args.join(" ")}`;
|
|
2205
|
+
throw new CommandExit(1, `\u274C Expected one argument, got ${got}
|
|
2206
|
+
Usage: ${usage}`);
|
|
2207
|
+
}
|
|
2208
|
+
function expectAtMostOneArgument(args, usage) {
|
|
2209
|
+
const [only] = args;
|
|
2210
|
+
if (only === undefined)
|
|
2211
|
+
return null;
|
|
2212
|
+
if (args.length === 1)
|
|
2213
|
+
return only;
|
|
2214
|
+
throw new CommandExit(1, `\u274C Expected at most one argument, got ${args.length}: ${args.join(" ")}
|
|
2215
|
+
Usage: ${usage}`);
|
|
2216
|
+
}
|
|
2217
|
+
function readSwitch(name2, value, hint) {
|
|
2218
|
+
if (value === undefined || value === false || value === "false")
|
|
2219
|
+
return false;
|
|
2220
|
+
if (value === true || value === "true")
|
|
2221
|
+
return true;
|
|
2222
|
+
throw new CommandExit(1, `\u274C --${name2} takes no value, and read "${value}" as one. ${hint}`);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// src/commands/effort/fix-schema.ts
|
|
2226
|
+
var COMMAND_NAME = "rafa effort fix-schema";
|
|
2227
|
+
var FIX_SCHEMA_USAGE = "rafa effort fix-schema [--dry-run]";
|
|
2228
|
+
var DRY_RUN_FLAG = "dry-run";
|
|
2229
|
+
function fileStamp(date) {
|
|
2230
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
2231
|
+
}
|
|
2232
|
+
function refuseLiveLoop(root, isAlive) {
|
|
2233
|
+
let records;
|
|
2234
|
+
try {
|
|
2235
|
+
records = readSessions(root, isAlive === undefined ? {} : { isAlive });
|
|
2236
|
+
} catch (error) {
|
|
2237
|
+
throw new CommandExit(1, `\u274C ${COMMAND_NAME}: the loop's run records cannot be read, so a live loop cannot be ruled out: ${String(error)}`);
|
|
2238
|
+
}
|
|
2239
|
+
const live = records.find((record) => record.state === "running" || record.state === "paused");
|
|
2240
|
+
if (live === undefined)
|
|
2241
|
+
return;
|
|
2242
|
+
throw new CommandExit(1, `\u274C ${COMMAND_NAME}: loop session ${live.sessionId} on ${live.branch} is ${live.state} and writes the store;` + ` stop it with \`rafa loop stop -s ${live.sessionId}\` first. \`--dry-run\` runs beside it.`);
|
|
2243
|
+
}
|
|
2244
|
+
function migrationNames(result) {
|
|
2245
|
+
return result.unknown.map(({ id, appliedBy }) => `${id} (applied by ${appliedBy})`).join(", ");
|
|
2246
|
+
}
|
|
2247
|
+
function rebuildLines(result) {
|
|
2248
|
+
const rows = result.kept.reduce((sum, table) => sum + table.rows, 0);
|
|
2249
|
+
const kept = result.kept.map((table) => `${table.table} ${String(table.rows)}`).join(", ");
|
|
2250
|
+
const left = [
|
|
2251
|
+
...result.unknown.map(({ id, appliedBy }) => `migration ${id} (applied by ${appliedBy})`),
|
|
2252
|
+
...result.leftTables.map((table) => `table ${table.table} (${String(table.rows)} rows)`),
|
|
2253
|
+
...result.leftColumns.map((column) => `column ${column.table}.${column.column} (${String(column.values)} values)`)
|
|
2254
|
+
];
|
|
2255
|
+
const newer = result.unknown.length === 0 ? [] : [`A rafa that knows ${result.unknown.map(({ id }) => id).join(", ")} uses the store as it is and keeps that data` + " in it; install one instead where you can."];
|
|
2256
|
+
return [
|
|
2257
|
+
`Refused as ${String(result.reason)}: ${String(result.refusal)}`,
|
|
2258
|
+
`Rebuilds it at the ${String(result.known.length)} migrations this rafa knows, logged as applied by ${result.appliedBy}.`,
|
|
2259
|
+
`Keeps ${String(result.kept.length)} tables, ${String(rows)} rows: ${kept}.`,
|
|
2260
|
+
left.length === 0 ? "Leaves nothing behind." : `Leaves behind, kept only in the backup: ${left.join("; ")}.`,
|
|
2261
|
+
...newer
|
|
2262
|
+
];
|
|
2263
|
+
}
|
|
2264
|
+
function unknownAdditiveLines(result) {
|
|
2265
|
+
return result.unknown.length === 0 ? [] : [`It logs ${migrationNames(result)}, which this rafa does not know; each is additive, so this rafa uses the store` + " through them."];
|
|
2266
|
+
}
|
|
2267
|
+
function renderFixSchema(result) {
|
|
2268
|
+
switch (result.status) {
|
|
2269
|
+
case "missing":
|
|
2270
|
+
return [`No effort store at ${result.path}: nothing to repair.`];
|
|
2271
|
+
case "current":
|
|
2272
|
+
return [
|
|
2273
|
+
`\u2705 ${result.path} is current: this rafa reads and writes it as it is. Nothing to repair.`,
|
|
2274
|
+
...unknownAdditiveLines(result)
|
|
2275
|
+
];
|
|
2276
|
+
case "behind":
|
|
2277
|
+
return [
|
|
2278
|
+
`${result.path} is behind this rafa's migrations; the next command that opens it records` + ` ${result.pending.join(", ")}. Nothing to repair.`,
|
|
2279
|
+
...unknownAdditiveLines(result)
|
|
2280
|
+
];
|
|
2281
|
+
case "would-rebuild":
|
|
2282
|
+
return [
|
|
2283
|
+
...rebuildLines(result),
|
|
2284
|
+
"\uD83D\uDD0D Dry run: the rebuild was built, checked (row counts, integrity_check, this rafa's schema plan) and" + " deleted. Run without `--dry-run` to swap it in."
|
|
2285
|
+
];
|
|
2286
|
+
case "rebuilt":
|
|
2287
|
+
return [
|
|
2288
|
+
...rebuildLines(result),
|
|
2289
|
+
`\u2705 Rebuilt at this rafa's migrations. The original is kept whole at ${String(result.backupPath)};` + " rename it back to undo."
|
|
2290
|
+
];
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
function runFixSchema(context, seams) {
|
|
2294
|
+
expectNoArgument(context.args, FIX_SCHEMA_USAGE);
|
|
2295
|
+
const dryRun = readSwitch(DRY_RUN_FLAG, context.flags[DRY_RUN_FLAG], `Usage: ${FIX_SCHEMA_USAGE}`);
|
|
2296
|
+
if (context.project === null)
|
|
2297
|
+
throw new Error(`${COMMAND_NAME} runs inside a project, and was handed none`);
|
|
2298
|
+
const root = context.project.root;
|
|
2299
|
+
if (!dryRun)
|
|
2300
|
+
refuseLiveLoop(root, seams.isAlive);
|
|
2301
|
+
const now = seams.now ?? (() => new Date);
|
|
2302
|
+
let result;
|
|
2303
|
+
try {
|
|
2304
|
+
result = fixStoreSchema({
|
|
2305
|
+
path: sqliteStorePath(root),
|
|
2306
|
+
dryRun,
|
|
2307
|
+
stamp: fileStamp(now()),
|
|
2308
|
+
now,
|
|
2309
|
+
...seams.identity === undefined ? {} : { identity: seams.identity }
|
|
2310
|
+
});
|
|
2311
|
+
} catch (error) {
|
|
2312
|
+
if (error instanceof RebuildRefusal)
|
|
2313
|
+
throw new CommandExit(1, `\u274C ${COMMAND_NAME}: ${error.message}`);
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
if (context.outputMode === "json") {
|
|
2317
|
+
context.output.result(result);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
for (const line of renderFixSchema(result))
|
|
2321
|
+
context.output.info(line);
|
|
2322
|
+
}
|
|
2323
|
+
function createFixSchemaCommand(seams = {}) {
|
|
2324
|
+
const command = {
|
|
2325
|
+
name: "effort fix-schema",
|
|
2326
|
+
subject: "effort",
|
|
2327
|
+
action: "fix-schema",
|
|
2328
|
+
summary: "rebuild an effort store this rafa refuses at the migrations it knows, keeping the original",
|
|
2329
|
+
description: "Repairs `.rafa/effort/effort.sqlite`, or the store under `RAFA_EFFORT_DIR`, when this rafa refuses" + " it for a reason a rebuild repairs: no migration log past the legacy entries (pre-log-unreleased), a log" + " with a schema version it does not match (gate-mismatch), a logged checksum this rafa holds otherwise" + " (edited), or a logged migration this rafa does not know that breaks older readers or writers" + " (unknown-breaks-readers, unknown-breaks-writers). Installing a rafa that knows the store's migrations" + " comes first where one exists. It builds a parallel store beside it at this rafa's migrations, with a" + " migration log naming this rafa, copies every table and column this rafa knows, checks the row counts," + " SQLite's integrity_check and the schema plan, and lists what only the newer schema holds, unknown" + " migrations included. Then it renames the original to `effort.sqlite.v<user_version>-<stamp>.bak`," + " whole, and moves the rebuild into its place. A store this rafa uses, current or behind, or no store, is" + " left alone. It refuses, changing nothing, while a loop session under the project is running or paused," + " from a development build, while a journal beside the store shows a write in flight, when the newer" + " schema lacks a table or column this rafa writes, and on a refusal a rebuild does not repair. With" + " `--output=json` the outcome is the data of the terminal result event. Starts no session.",
|
|
2330
|
+
args: [],
|
|
2331
|
+
flags: [
|
|
2332
|
+
{
|
|
2333
|
+
name: DRY_RUN_FLAG,
|
|
2334
|
+
description: "Build and check the rebuild, print what it keeps and leaves behind, then delete it. The" + " live store is only read, so this runs beside a live loop and from a development build, and can be" + " repeated.",
|
|
2335
|
+
type: "boolean"
|
|
2336
|
+
}
|
|
2337
|
+
],
|
|
2338
|
+
examples: [
|
|
2339
|
+
{
|
|
2340
|
+
cmd: "rafa effort fix-schema --dry-run",
|
|
2341
|
+
note: "Prints what a rebuild would keep and leave behind, changing nothing."
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
cmd: "rafa effort fix-schema",
|
|
2345
|
+
note: "Swaps the rebuild in and prints where the original was kept."
|
|
2346
|
+
}
|
|
2347
|
+
],
|
|
2348
|
+
outputs: ["text", "json"],
|
|
2349
|
+
run: async (context) => {
|
|
2350
|
+
runFixSchema(context, seams);
|
|
2351
|
+
await Promise.resolve();
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
return Object.freeze(command);
|
|
2355
|
+
}
|
|
2356
|
+
var fix_schema_default = createFixSchemaCommand();
|
|
2357
|
+
|
|
2358
|
+
// src/effort/store/copy.ts
|
|
2359
|
+
import { copyFileSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync2, rmSync as rmSync5, statSync as statSync3 } from "fs";
|
|
2360
|
+
import { join as join10, resolve as resolve4 } from "path";
|
|
2361
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
2362
|
+
var NDJSON_KINDS = { sessions: true, commits: true };
|
|
2363
|
+
var EFFORT_COPY_FILE_NAMES = [
|
|
2364
|
+
SQLITE_STORE_FILE_NAME,
|
|
2365
|
+
...Object.keys(NDJSON_KINDS).map((kind) => effortStoreFileName(kind))
|
|
2366
|
+
];
|
|
2367
|
+
|
|
2368
|
+
class EffortCopyRefusal extends Error {
|
|
2369
|
+
name = "EffortCopyRefusal";
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
class EffortCopyFailure extends Error {
|
|
2373
|
+
name = "EffortCopyFailure";
|
|
2374
|
+
}
|
|
2375
|
+
function refuseUsedTarget(target) {
|
|
2376
|
+
if (!existsSync9(target))
|
|
2377
|
+
return;
|
|
2378
|
+
let entries;
|
|
2379
|
+
try {
|
|
2380
|
+
if (!statSync3(target).isDirectory()) {
|
|
2381
|
+
throw new EffortCopyRefusal(`${target} is a file, not a directory; name an empty or absent directory with --to=<dir>.`);
|
|
2382
|
+
}
|
|
2383
|
+
entries = readdirSync2(target);
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
if (error instanceof EffortCopyRefusal)
|
|
2386
|
+
throw error;
|
|
2387
|
+
throw new EffortCopyFailure(`${target} cannot be read: ${String(error)}`);
|
|
2388
|
+
}
|
|
2389
|
+
if (entries.length === 0)
|
|
2390
|
+
return;
|
|
2391
|
+
throw new EffortCopyRefusal(`${target} is not empty (${String(entries.length)} entries); nothing was copied.` + " Name an empty or absent directory with --to=<dir>.");
|
|
2392
|
+
}
|
|
2393
|
+
function vacuumInto(source, target) {
|
|
2394
|
+
const db = new Database3(source, { readonly: true });
|
|
2395
|
+
try {
|
|
2396
|
+
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
2397
|
+
db.run("VACUUM INTO ?", [target]);
|
|
2398
|
+
} finally {
|
|
2399
|
+
db.close();
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
function copyOne(name2, source, target) {
|
|
2403
|
+
if (name2 === SQLITE_STORE_FILE_NAME) {
|
|
2404
|
+
vacuumInto(join10(source, name2), join10(target, name2));
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
copyFileSync(join10(source, name2), join10(target, name2), constants.COPYFILE_EXCL);
|
|
2408
|
+
}
|
|
2409
|
+
function undo(target, written, madeDir) {
|
|
2410
|
+
try {
|
|
2411
|
+
if (madeDir === undefined) {
|
|
2412
|
+
for (const name2 of written)
|
|
2413
|
+
rmSync5(join10(target, name2), { force: true });
|
|
2414
|
+
} else {
|
|
2415
|
+
rmSync5(madeDir, { recursive: true, force: true });
|
|
2416
|
+
}
|
|
2417
|
+
return "";
|
|
2418
|
+
} catch (error) {
|
|
2419
|
+
return ` Removing what it made failed too: ${String(error)}`;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
function copyEffortStore(request) {
|
|
2423
|
+
const source = resolve4(request.source);
|
|
2424
|
+
const target = resolve4(request.target);
|
|
2425
|
+
guardTestProcess(source);
|
|
2426
|
+
guardTestProcess(target);
|
|
2427
|
+
const files = EFFORT_COPY_FILE_NAMES.filter((name2) => existsSync9(join10(source, name2)));
|
|
2428
|
+
if (files.length === 0) {
|
|
2429
|
+
throw new EffortCopyRefusal(`no effort store at ${source}: it holds none of ${EFFORT_COPY_FILE_NAMES.join(", ")}. Nothing to copy.`);
|
|
2430
|
+
}
|
|
2431
|
+
refuseUsedTarget(target);
|
|
2432
|
+
const written = [];
|
|
2433
|
+
let madeDir;
|
|
2434
|
+
try {
|
|
2435
|
+
madeDir = mkdirSync6(target, { recursive: true });
|
|
2436
|
+
for (const name2 of files) {
|
|
2437
|
+
written.push(name2);
|
|
2438
|
+
copyOne(name2, source, target);
|
|
2439
|
+
}
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
const left = undo(target, written, madeDir);
|
|
2442
|
+
throw new EffortCopyFailure(`copying ${source} to ${target} failed: ${String(error)}.${left}`);
|
|
2443
|
+
}
|
|
2444
|
+
return Object.freeze({ source, directory: target, files: Object.freeze(files) });
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// src/effort/store/merge-store.ts
|
|
2448
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2449
|
+
import { existsSync as existsSync11, mkdtempSync, rmSync as rmSync6 } from "fs";
|
|
2450
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2451
|
+
import { dirname as dirname10, join as join11 } from "path";
|
|
2452
|
+
import { Database as Database5, SQLiteError as SQLiteError2 } from "bun:sqlite";
|
|
2453
|
+
|
|
2454
|
+
// src/effort/commits.ts
|
|
2455
|
+
import { execFileSync } from "child_process";
|
|
2456
|
+
var COMMIT_RECORD_MARKER = "@@ralph-commit@@";
|
|
2457
|
+
var HEADER_PLACEHOLDERS = ["%H", "%aI", "%P", "%an", "%S", "%s"];
|
|
2458
|
+
var FIELD_SEPARATOR = "\t";
|
|
2459
|
+
var COMMIT_LOG_FORMAT = [
|
|
2460
|
+
COMMIT_RECORD_MARKER,
|
|
2461
|
+
...HEADER_PLACEHOLDERS
|
|
2462
|
+
].join("%x09");
|
|
2463
|
+
var MS_PER_MINUTE = 60000;
|
|
2464
|
+
var ELAPSED_DECIMALS = 3;
|
|
2465
|
+
function normaliseBranch(source) {
|
|
2466
|
+
if (source.length === 0 || source === "HEAD")
|
|
2467
|
+
return null;
|
|
2468
|
+
return source.startsWith("refs/heads/") ? source.slice("refs/heads/".length) : source;
|
|
2469
|
+
}
|
|
2470
|
+
function parseStatCount(field2) {
|
|
2471
|
+
return /^\d+$/.test(field2) ? Number(field2) : null;
|
|
2472
|
+
}
|
|
2473
|
+
function parseCommitHeader(line) {
|
|
2474
|
+
const parts = line.split(FIELD_SEPARATOR);
|
|
2475
|
+
if (parts[0] !== COMMIT_RECORD_MARKER)
|
|
2476
|
+
return null;
|
|
2477
|
+
if (parts.length <= HEADER_PLACEHOLDERS.length)
|
|
2478
|
+
return null;
|
|
2479
|
+
const [sha, timestamp, parents, author, source] = parts.slice(1);
|
|
2480
|
+
if (sha === undefined || sha.length === 0)
|
|
2481
|
+
return null;
|
|
2482
|
+
return {
|
|
2483
|
+
sha,
|
|
2484
|
+
timestamp: timestamp ?? "",
|
|
2485
|
+
subject: parts.slice(HEADER_PLACEHOLDERS.length).join(FIELD_SEPARATOR),
|
|
2486
|
+
author: author ?? "",
|
|
2487
|
+
branch: normaliseBranch(source ?? ""),
|
|
2488
|
+
filesChanged: 0,
|
|
2489
|
+
insertions: 0,
|
|
2490
|
+
deletions: 0,
|
|
2491
|
+
parentCount: countParents(parents ?? ""),
|
|
2492
|
+
minutesSincePrevious: null
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
function countParents(parents) {
|
|
2496
|
+
return parents.trim().length === 0 ? 0 : parents.trim().split(/ +/).length;
|
|
2497
|
+
}
|
|
2498
|
+
function applyStatLine(row, line) {
|
|
2499
|
+
const parts = line.split(FIELD_SEPARATOR);
|
|
2500
|
+
if (parts.length < 3)
|
|
2501
|
+
return false;
|
|
2502
|
+
const added = parts[0] ?? "";
|
|
2503
|
+
const removed = parts[1] ?? "";
|
|
2504
|
+
const isBinary = added === "-" && removed === "-";
|
|
2505
|
+
const addedCount = parseStatCount(added);
|
|
2506
|
+
const removedCount = parseStatCount(removed);
|
|
2507
|
+
if (!isBinary && (addedCount === null || removedCount === null)) {
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
row.filesChanged += 1;
|
|
2511
|
+
row.insertions += addedCount ?? 0;
|
|
2512
|
+
row.deletions += removedCount ?? 0;
|
|
2513
|
+
return true;
|
|
2514
|
+
}
|
|
2515
|
+
function minutesBetween(earlier, later) {
|
|
2516
|
+
const from = Date.parse(earlier);
|
|
2517
|
+
const to = Date.parse(later);
|
|
2518
|
+
if (Number.isNaN(from) || Number.isNaN(to))
|
|
2519
|
+
return null;
|
|
2520
|
+
const minutes = (to - from) / MS_PER_MINUTE;
|
|
2521
|
+
return Number(minutes.toFixed(ELAPSED_DECIMALS));
|
|
2522
|
+
}
|
|
2523
|
+
function assignElapsedMinutes(rows) {
|
|
2524
|
+
const ordered = rows.map((row, index) => ({ index, epoch: Date.parse(row.timestamp) })).filter((entry) => !Number.isNaN(entry.epoch)).sort((a, b) => a.epoch - b.epoch);
|
|
2525
|
+
for (let position = 1;position < ordered.length; position += 1) {
|
|
2526
|
+
const entry = ordered[position];
|
|
2527
|
+
const previous = ordered[position - 1];
|
|
2528
|
+
if (entry === undefined || previous === undefined)
|
|
2529
|
+
continue;
|
|
2530
|
+
const row = rows[entry.index];
|
|
2531
|
+
const earlier = rows[previous.index];
|
|
2532
|
+
if (row === undefined || earlier === undefined)
|
|
2533
|
+
continue;
|
|
2534
|
+
row.minutesSincePrevious = minutesBetween(earlier.timestamp, row.timestamp);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
function parseCommitLog(text) {
|
|
2538
|
+
const rows = [];
|
|
2539
|
+
let lineCount = 0;
|
|
2540
|
+
let unparsedLineCount = 0;
|
|
2541
|
+
let open = null;
|
|
2542
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
2543
|
+
const line = rawLine.trimEnd();
|
|
2544
|
+
if (line.trim().length === 0)
|
|
2545
|
+
continue;
|
|
2546
|
+
lineCount += 1;
|
|
2547
|
+
const header = parseCommitHeader(line);
|
|
2548
|
+
if (header !== null) {
|
|
2549
|
+
rows.push(header);
|
|
2550
|
+
open = header;
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
if (open === null || !applyStatLine(open, line)) {
|
|
2554
|
+
unparsedLineCount += 1;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
assignElapsedMinutes(rows);
|
|
2558
|
+
return { rows, lineCount, unparsedLineCount };
|
|
2559
|
+
}
|
|
2560
|
+
function commitLogArgs(options = {}) {
|
|
2561
|
+
const args = [
|
|
2562
|
+
"log",
|
|
2563
|
+
"--source",
|
|
2564
|
+
`--format=${COMMIT_LOG_FORMAT}`,
|
|
2565
|
+
"--numstat"
|
|
2566
|
+
];
|
|
2567
|
+
if (options.since !== undefined)
|
|
2568
|
+
args.push(`--since=${options.since}`);
|
|
2569
|
+
if (options.maxCount !== undefined) {
|
|
2570
|
+
args.push(`--max-count=${options.maxCount}`);
|
|
2571
|
+
}
|
|
2572
|
+
if (options.revisionRange !== undefined)
|
|
2573
|
+
args.push(options.revisionRange);
|
|
2574
|
+
return args;
|
|
2575
|
+
}
|
|
2576
|
+
function readCommitLog(options = {}) {
|
|
2577
|
+
const stdout = execFileSync("git", commitLogArgs(options), {
|
|
2578
|
+
cwd: options.cwd ?? process.cwd(),
|
|
2579
|
+
encoding: "utf8",
|
|
2580
|
+
maxBuffer: 64 * 1024 * 1024
|
|
2581
|
+
});
|
|
2582
|
+
return parseCommitLog(stdout);
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// src/effort/store/merge-commit-gaps.ts
|
|
2586
|
+
var GAP_FIELD = "minutesSincePrevious";
|
|
2587
|
+
var UPDATE_GAP = "UPDATE commits SET row_json = ? WHERE seq = ?";
|
|
2588
|
+
function parsedObject2(text) {
|
|
2589
|
+
try {
|
|
2590
|
+
const value = JSON.parse(text);
|
|
2591
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
2592
|
+
} catch {
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
function withoutGap(rowJson) {
|
|
2597
|
+
const row = parsedObject2(rowJson);
|
|
2598
|
+
if (row === null)
|
|
2599
|
+
return rowJson;
|
|
2600
|
+
const rest = Object.fromEntries(Object.entries(row).filter(([key]) => key !== GAP_FIELD));
|
|
2601
|
+
return JSON.stringify(rest);
|
|
2602
|
+
}
|
|
2603
|
+
function readCommits(db) {
|
|
2604
|
+
return db.query("SELECT seq, sha, row_json FROM commits").all().map(({ seq, sha, row_json: rowJson }) => {
|
|
2605
|
+
const row = parsedObject2(rowJson);
|
|
2606
|
+
const timestamp = typeof row?.timestamp === "string" ? row.timestamp : null;
|
|
2607
|
+
return { seq, sha, row, timestamp, epoch: timestamp === null ? Number.NaN : Date.parse(timestamp) };
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
function timeOrder(entries) {
|
|
2611
|
+
return entries.filter((entry) => !Number.isNaN(entry.epoch)).sort((left, right) => left.epoch - right.epoch || compareText(left.sha, right.sha));
|
|
2612
|
+
}
|
|
2613
|
+
function compareText(left, right) {
|
|
2614
|
+
if (left === right)
|
|
2615
|
+
return 0;
|
|
2616
|
+
return left < right ? -1 : 1;
|
|
2617
|
+
}
|
|
2618
|
+
function storedGap(entry) {
|
|
2619
|
+
const value = entry.row?.[GAP_FIELD];
|
|
2620
|
+
return typeof value === "number" ? value : null;
|
|
2621
|
+
}
|
|
2622
|
+
function namedRows(inserted, positions, order) {
|
|
2623
|
+
const position = positions.get(inserted.seq);
|
|
2624
|
+
if (position === undefined)
|
|
2625
|
+
return [inserted.seq];
|
|
2626
|
+
const next = order[position + 1];
|
|
2627
|
+
return next === undefined ? [inserted.seq] : [inserted.seq, next.seq];
|
|
2628
|
+
}
|
|
2629
|
+
function recomputedGap(entry, positions, order) {
|
|
2630
|
+
const position = positions.get(entry.seq);
|
|
2631
|
+
if (position === undefined || position === 0)
|
|
2632
|
+
return null;
|
|
2633
|
+
const previous = order[position - 1];
|
|
2634
|
+
if (previous?.timestamp == null || entry.timestamp === null)
|
|
2635
|
+
return null;
|
|
2636
|
+
return minutesBetween(previous.timestamp, entry.timestamp);
|
|
2637
|
+
}
|
|
2638
|
+
function recomputeCommitGaps(db, inserted) {
|
|
2639
|
+
return db.transaction(() => {
|
|
2640
|
+
const entries = readCommits(db);
|
|
2641
|
+
const bySeq = new Map(entries.map((entry) => [entry.seq, entry]));
|
|
2642
|
+
const order = timeOrder(entries);
|
|
2643
|
+
const positions = new Map(order.map((entry, position) => [entry.seq, position]));
|
|
2644
|
+
const named = new Set(inserted.flatMap((seq) => {
|
|
2645
|
+
const entry = bySeq.get(seq);
|
|
2646
|
+
return entry === undefined ? [] : namedRows(entry, positions, order);
|
|
2647
|
+
}));
|
|
2648
|
+
const targets = [...named].map((seq) => bySeq.get(seq)).filter((entry) => entry?.row != null).sort((left, right) => (positions.get(left.seq) ?? -1) - (positions.get(right.seq) ?? -1));
|
|
2649
|
+
return targets.flatMap((entry) => {
|
|
2650
|
+
const before = storedGap(entry);
|
|
2651
|
+
const after = recomputedGap(entry, positions, order);
|
|
2652
|
+
if (before === after && entry.row !== null && Object.hasOwn(entry.row, GAP_FIELD))
|
|
2653
|
+
return [];
|
|
2654
|
+
db.query(UPDATE_GAP).run(JSON.stringify({ ...entry.row, [GAP_FIELD]: after }), entry.seq);
|
|
2655
|
+
return [{ seq: entry.seq, sha: entry.sha, before, after }];
|
|
2656
|
+
});
|
|
2657
|
+
})();
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// src/effort/store/merge-rules.ts
|
|
2661
|
+
var SET_ONCE = "set-once";
|
|
2662
|
+
var RECOMPUTED = "recomputed";
|
|
2663
|
+
var APPEND_ONLY = {};
|
|
2664
|
+
function appendOnly(...identity) {
|
|
2665
|
+
return { scope: "merged", identity, edited: APPEND_ONLY };
|
|
2666
|
+
}
|
|
2667
|
+
var MERGE_RULES = {
|
|
2668
|
+
sessions: appendOnly("session_id"),
|
|
2669
|
+
commits: { scope: "merged", identity: ["sha"], edited: { row_json: RECOMPUTED } },
|
|
2670
|
+
findings: { scope: "merged", identity: ["id"], edited: { tracker_ref: SET_ONCE } },
|
|
2671
|
+
blockers: appendOnly("id"),
|
|
2672
|
+
out_of_scope_bugs: appendOnly("id"),
|
|
2673
|
+
report_absences: appendOnly("id"),
|
|
2674
|
+
task_reports: appendOnly("id"),
|
|
2675
|
+
preflight: appendOnly("run_id", "position"),
|
|
2676
|
+
dispatches: appendOnly("session_id"),
|
|
2677
|
+
changes: appendOnly("id"),
|
|
2678
|
+
skill_invocations: appendOnly("session_id", "name", "sidechain"),
|
|
2679
|
+
plan_ci: appendOnly("pr", "head_sha", "read_at"),
|
|
2680
|
+
schema_migrations: { scope: "local" },
|
|
2681
|
+
store_meta: { scope: "local" },
|
|
2682
|
+
merges: { scope: "local" },
|
|
2683
|
+
merge_conflicts: { scope: "local" }
|
|
2684
|
+
};
|
|
2685
|
+
|
|
2686
|
+
// src/effort/store/merge-union.ts
|
|
2687
|
+
import { SQLiteError } from "bun:sqlite";
|
|
2688
|
+
class UnionSchemaMismatch extends Error {
|
|
2689
|
+
constructor(message) {
|
|
2690
|
+
super(message);
|
|
2691
|
+
this.name = "UnionSchemaMismatch";
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
var UNIQUE_REFUSAL = "SQLITE_CONSTRAINT_UNIQUE";
|
|
2695
|
+
function mergedTables() {
|
|
2696
|
+
return Object.entries(MERGE_RULES).flatMap(([table, rule]) => rule.scope === "merged" ? [{ table, identity: rule.identity }] : []);
|
|
1085
2697
|
}
|
|
1086
|
-
function
|
|
1087
|
-
|
|
1088
|
-
return [...adoption, ...plan.pending.map(({ id }) => id)];
|
|
2698
|
+
function carriedColumns(db, table) {
|
|
2699
|
+
return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name: name2 }) => name2).filter((name2) => name2 !== "seq");
|
|
1089
2700
|
}
|
|
1090
|
-
function
|
|
1091
|
-
|
|
2701
|
+
function refuseColumnMismatch(local, other, tables) {
|
|
2702
|
+
const faults = tables.flatMap(({ table }) => {
|
|
2703
|
+
const here = new Set(carriedColumns(local, table));
|
|
2704
|
+
const there = new Set(carriedColumns(other, table));
|
|
2705
|
+
const onlyHere = [...here].filter((column) => !there.has(column)).map((column) => `${table}.${column} only here`);
|
|
2706
|
+
const onlyThere = [...there].filter((column) => !here.has(column)).map((column) => `${table}.${column} only there`);
|
|
2707
|
+
return [...onlyHere, ...onlyThere];
|
|
2708
|
+
});
|
|
2709
|
+
if (faults.length > 0) {
|
|
2710
|
+
throw new UnionSchemaMismatch(`the two stores hold different columns (${faults.join(", ")}); bring both to the same migrations first`);
|
|
2711
|
+
}
|
|
1092
2712
|
}
|
|
1093
|
-
function
|
|
1094
|
-
|
|
1095
|
-
return row?.user_version ?? 0;
|
|
2713
|
+
function valueOf(row, column) {
|
|
2714
|
+
return row[column] ?? null;
|
|
1096
2715
|
}
|
|
1097
|
-
function
|
|
1098
|
-
const
|
|
1099
|
-
|
|
2716
|
+
function byOrigin(local, table, row) {
|
|
2717
|
+
const store = valueOf(row, "origin_store");
|
|
2718
|
+
const seq = valueOf(row, "origin_seq");
|
|
2719
|
+
if (store === null || seq === null)
|
|
1100
2720
|
return null;
|
|
1101
|
-
return
|
|
1102
|
-
id: row.id,
|
|
1103
|
-
sha256: row.sha256,
|
|
1104
|
-
breaks: parseBreaks(row.breaks, row.id, path),
|
|
1105
|
-
appliedAt: row.applied_at,
|
|
1106
|
-
appliedBy: row.applied_by
|
|
1107
|
-
}));
|
|
2721
|
+
return local.query(`SELECT seq FROM ${quoted(table)} WHERE origin_store = ? AND origin_seq = ?`).get(store, seq)?.seq ?? null;
|
|
1108
2722
|
}
|
|
1109
|
-
function
|
|
1110
|
-
const
|
|
1111
|
-
|
|
2723
|
+
function byIdentity(local, { table, identity }, row) {
|
|
2724
|
+
const where = identity.map((column) => `${quoted(column)} IS ?`).join(" AND ");
|
|
2725
|
+
return local.query(`SELECT seq FROM ${quoted(table)} WHERE ${where} ORDER BY seq LIMIT 1`).get(...identity.map((column) => valueOf(row, column)))?.seq ?? null;
|
|
2726
|
+
}
|
|
2727
|
+
function insertRow(local, table, columns, row, incomingSeq) {
|
|
2728
|
+
const columnList = columns.map(quoted).join(", ");
|
|
2729
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
2730
|
+
const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${quoted(table)})`;
|
|
1112
2731
|
try {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
2732
|
+
const inserted = local.query(`INSERT INTO ${quoted(table)} (seq, ${columnList}) VALUES (${nextSeq}, ${placeholders}) RETURNING seq`).get(...columns.map((column) => valueOf(row, column)));
|
|
2733
|
+
if (inserted === null)
|
|
2734
|
+
throw new Error(`the insert into ${table} returned no seq`);
|
|
2735
|
+
return { kind: "added", localSeq: inserted.seq };
|
|
2736
|
+
} catch (error) {
|
|
2737
|
+
if (!(error instanceof SQLiteError) || error.code !== UNIQUE_REFUSAL)
|
|
2738
|
+
throw error;
|
|
2739
|
+
return { kind: "collided", collision: { incomingSeq, reason: error.message } };
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
function placeRow(local, merged, columns, row) {
|
|
2743
|
+
const incomingSeq = Number(valueOf(row, "seq"));
|
|
2744
|
+
const origin = byOrigin(local, merged.table, row);
|
|
2745
|
+
if (origin !== null)
|
|
2746
|
+
return { kind: "matched", match: { localSeq: origin, incomingSeq, by: "origin" } };
|
|
2747
|
+
const identity = byIdentity(local, merged, row);
|
|
2748
|
+
if (identity !== null)
|
|
2749
|
+
return { kind: "matched", match: { localSeq: identity, incomingSeq, by: "identity" } };
|
|
2750
|
+
return insertRow(local, merged.table, columns, row, incomingSeq);
|
|
2751
|
+
}
|
|
2752
|
+
function unionTable(local, other, merged) {
|
|
2753
|
+
const columns = carriedColumns(local, merged.table);
|
|
2754
|
+
const incoming = other.query(`SELECT seq, ${columns.map(quoted).join(", ")} FROM ${quoted(merged.table)} ORDER BY seq`).all();
|
|
2755
|
+
const outcomes = incoming.map((row) => placeRow(local, merged, columns, row));
|
|
2756
|
+
return {
|
|
2757
|
+
table: merged.table,
|
|
2758
|
+
added: outcomes.flatMap((outcome) => outcome.kind === "added" ? [outcome.localSeq] : []),
|
|
2759
|
+
matched: outcomes.flatMap((outcome) => outcome.kind === "matched" ? [outcome.match] : []),
|
|
2760
|
+
collided: outcomes.flatMap((outcome) => outcome.kind === "collided" ? [outcome.collision] : [])
|
|
2761
|
+
};
|
|
1120
2762
|
}
|
|
1121
|
-
function
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
return migration.sql;
|
|
2763
|
+
function unionStores(local, other) {
|
|
2764
|
+
const tables = mergedTables();
|
|
2765
|
+
refuseColumnMismatch(local, other, tables);
|
|
2766
|
+
return local.transaction(() => tables.map((merged) => unionTable(local, other, merged)))();
|
|
1126
2767
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
2768
|
+
|
|
2769
|
+
// src/effort/store/merge-conflicts.ts
|
|
2770
|
+
var SET_ONCE_FILLS = {
|
|
2771
|
+
"findings.tracker_ref": "UPDATE findings SET tracker_ref = ? WHERE seq = ? AND tracker_ref IS NULL"
|
|
2772
|
+
};
|
|
2773
|
+
var RECOMPUTED_COMPARISONS = {
|
|
2774
|
+
"commits.row_json": withoutGap
|
|
2775
|
+
};
|
|
2776
|
+
var NOT_CONTENT = new Set(["seq", "origin_store", "origin_seq"]);
|
|
2777
|
+
var INSERT_CONFLICT = `INSERT INTO merge_conflicts (merge_id, table_name, local_seq, field, incoming, recorded_at)
|
|
2778
|
+
VALUES (?, ?, ?, ?, ?, ?)`;
|
|
2779
|
+
function rowAt(db, table, columns, seq, side) {
|
|
2780
|
+
const row = db.query(`SELECT seq, ${columns.map(quoted).join(", ")} FROM ${quoted(table)} WHERE seq = ?`).get(seq);
|
|
2781
|
+
if (row === null)
|
|
2782
|
+
throw new Error(`the union matched ${table} row ${seq} ${side}, which that store does not hold`);
|
|
2783
|
+
return row;
|
|
2784
|
+
}
|
|
2785
|
+
function sameValue(left, right) {
|
|
2786
|
+
if (left instanceof Uint8Array && right instanceof Uint8Array)
|
|
2787
|
+
return Buffer.from(left).equals(right);
|
|
2788
|
+
return (left ?? null) === (right ?? null);
|
|
2789
|
+
}
|
|
2790
|
+
function fillStatement(table, field2) {
|
|
2791
|
+
const statement = SET_ONCE_FILLS[`${table}.${field2}`];
|
|
2792
|
+
if (statement === undefined) {
|
|
2793
|
+
throw new Error(`${table}.${field2} is set-once in MERGE_RULES and has no statement in SET_ONCE_FILLS`);
|
|
2794
|
+
}
|
|
2795
|
+
return statement;
|
|
2796
|
+
}
|
|
2797
|
+
function settleSetOnce(local, table, field2, here, there) {
|
|
2798
|
+
const incoming = there[field2] ?? null;
|
|
2799
|
+
if (incoming === null)
|
|
2800
|
+
return { skipped: true, filled: [], conflictFields: [] };
|
|
2801
|
+
if ((here[field2] ?? null) !== null)
|
|
2802
|
+
return { skipped: false, filled: [], conflictFields: [field2] };
|
|
2803
|
+
local.query(fillStatement(table, field2)).run(incoming, Number(here.seq));
|
|
2804
|
+
return { skipped: true, filled: [field2], conflictFields: [] };
|
|
2805
|
+
}
|
|
2806
|
+
function settleRecomputed(table, field2, here, there) {
|
|
2807
|
+
const comparable = RECOMPUTED_COMPARISONS[`${table}.${field2}`];
|
|
2808
|
+
if (comparable === undefined) {
|
|
2809
|
+
throw new Error(`${table}.${field2} is recomputed in MERGE_RULES and has no entry in RECOMPUTED_COMPARISONS`);
|
|
2810
|
+
}
|
|
2811
|
+
const hereValue = here[field2];
|
|
2812
|
+
const thereValue = there[field2];
|
|
2813
|
+
const same = typeof hereValue === "string" && typeof thereValue === "string" && comparable(hereValue) === comparable(thereValue);
|
|
2814
|
+
return same ? { skipped: true, filled: [], conflictFields: [] } : { skipped: false, filled: [], conflictFields: [null] };
|
|
2815
|
+
}
|
|
2816
|
+
function settlePair(local, table, edited, here, there) {
|
|
2817
|
+
const differing = Object.keys(here).filter((column) => !NOT_CONTENT.has(column) && !sameValue(here[column], there[column]));
|
|
2818
|
+
if (differing.length === 0)
|
|
2819
|
+
return { skipped: true, filled: [], conflictFields: [] };
|
|
2820
|
+
if (differing.some((column) => !Object.hasOwn(edited, column)))
|
|
2821
|
+
return { skipped: false, filled: [], conflictFields: [null] };
|
|
2822
|
+
const outcomes = differing.map((field2) => {
|
|
2823
|
+
if (edited[field2] === SET_ONCE)
|
|
2824
|
+
return settleSetOnce(local, table, field2, here, there);
|
|
2825
|
+
if (edited[field2] === RECOMPUTED)
|
|
2826
|
+
return settleRecomputed(table, field2, here, there);
|
|
2827
|
+
throw new Error(`${table}.${field2} has the rule ${edited[field2]}, which the merge does not apply`);
|
|
2828
|
+
});
|
|
2829
|
+
return {
|
|
2830
|
+
skipped: outcomes.every((outcome) => outcome.skipped),
|
|
2831
|
+
filled: outcomes.flatMap((outcome) => outcome.filled),
|
|
2832
|
+
conflictFields: outcomes.flatMap((outcome) => outcome.conflictFields)
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
function settleTable(local, other, union, trail) {
|
|
2836
|
+
const rule = MERGE_RULES[union.table];
|
|
2837
|
+
if (rule?.scope !== "merged")
|
|
2838
|
+
throw new Error(`${union.table} is not a merged table in MERGE_RULES`);
|
|
2839
|
+
const columns = carriedColumns(local, union.table);
|
|
2840
|
+
const settled = union.matched.map((match) => {
|
|
2841
|
+
const here = rowAt(local, union.table, columns, match.localSeq, "here");
|
|
2842
|
+
const there = rowAt(other, union.table, columns, match.incomingSeq, "in the other store");
|
|
2843
|
+
const outcome = settlePair(local, union.table, rule.edited, here, there);
|
|
2844
|
+
const conflicts = outcome.conflictFields.map((field2) => {
|
|
2845
|
+
local.query(INSERT_CONFLICT).run(trail.mergeId, union.table, match.localSeq, field2, JSON.stringify(there), trail.recordedAt);
|
|
2846
|
+
return { localSeq: match.localSeq, incomingSeq: match.incomingSeq, field: field2 };
|
|
2847
|
+
});
|
|
2848
|
+
return { match, outcome, conflicts };
|
|
2849
|
+
});
|
|
2850
|
+
return {
|
|
2851
|
+
table: union.table,
|
|
2852
|
+
skipped: settled.flatMap(({ match, outcome }) => outcome.skipped ? [match.localSeq] : []),
|
|
2853
|
+
filled: settled.flatMap(({ match, outcome }) => outcome.filled.map((field2) => ({ localSeq: match.localSeq, field: field2 }))),
|
|
2854
|
+
conflicts: settled.flatMap(({ conflicts }) => conflicts)
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
function settleMatches(local, other, unions, trail) {
|
|
2858
|
+
return local.transaction(() => unions.map((union) => settleTable(local, other, union, trail)))();
|
|
1129
2859
|
}
|
|
1130
2860
|
|
|
1131
|
-
// src/effort/store/
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
2861
|
+
// src/effort/store/migrate.ts
|
|
2862
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2863
|
+
import { Database as Database4 } from "bun:sqlite";
|
|
2864
|
+
var LIVE2 = "live";
|
|
2865
|
+
function asidePaths2(path, first, stamp) {
|
|
2866
|
+
return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
|
|
2867
|
+
}
|
|
2868
|
+
function liveLoop(path, options, nothingDone = "Nothing was migrated.") {
|
|
2869
|
+
const root = storeProjectRoot(path);
|
|
2870
|
+
if (root === null)
|
|
1139
2871
|
return;
|
|
2872
|
+
let records;
|
|
2873
|
+
try {
|
|
2874
|
+
records = readSessions(root, { isAlive: options.isAlive ?? isPidAlive });
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
throw new RebuildRefusal(`the loop's run records cannot be read, so a live loop on ${path} cannot be ruled out` + ` (${messageOf(error)}). ${nothingDone}`);
|
|
1140
2877
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
2878
|
+
return records.find((record) => record.state === "running" || record.state === "paused");
|
|
2879
|
+
}
|
|
2880
|
+
function refuseLiveLoop2(path, breaking, options) {
|
|
2881
|
+
const loop = liveLoop(path, options);
|
|
2882
|
+
if (loop === undefined)
|
|
2883
|
+
return;
|
|
2884
|
+
const named = `loop ${loop.sessionId} (pid ${String(loop.pid)}, plan ${loop.planStub ?? loop.plan}) is running on this store`;
|
|
2885
|
+
const [first] = breaking;
|
|
2886
|
+
const opening = first === undefined ? `REFUSED \u2014 ${named}, and swapping the migrated store in under it would lose what it writes.` : `REFUSED \u2014 migration ${first} breaks older runtimes, and ${named}.`;
|
|
2887
|
+
throw new RebuildRefusal(`${opening} Nothing was migrated. Finish or stop that loop, then run it again.`);
|
|
2888
|
+
}
|
|
2889
|
+
function buildMigrated(path, parallelPath, migrations, stamp) {
|
|
2890
|
+
vacuumInto(path, parallelPath);
|
|
2891
|
+
const db = new Database4(parallelPath, { readwrite: true });
|
|
2892
|
+
try {
|
|
2893
|
+
const brought = bringForward(db, parallelPath, "write", "migrate", { migrations, ...stamp, builtAside: true });
|
|
2894
|
+
db.run(`ATTACH DATABASE ? AS ${LIVE2}`, [path]);
|
|
2895
|
+
const live = new Set(tableNames(db, LIVE2));
|
|
2896
|
+
const counted = checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE2);
|
|
2897
|
+
db.run(`DETACH DATABASE ${LIVE2}`);
|
|
2898
|
+
refuseCorrupt(db);
|
|
2899
|
+
refuseUnusable(db, parallelPath, migrations);
|
|
2900
|
+
return { userVersion: brought.userVersion, counted };
|
|
2901
|
+
} finally {
|
|
2902
|
+
db.close();
|
|
1143
2903
|
}
|
|
1144
|
-
active = Object.freeze({ busyTimeoutMs: settings.busyTimeoutMs });
|
|
1145
2904
|
}
|
|
1146
|
-
function
|
|
1147
|
-
|
|
2905
|
+
function migrateStore(options) {
|
|
2906
|
+
const { path, dryRun, stamp } = options;
|
|
2907
|
+
guardTestProcess(path);
|
|
2908
|
+
const migrations = options.migrations ?? SQLITE_MIGRATIONS;
|
|
2909
|
+
const appliedBy = appliedByName(options.identity ?? readRuntimeIdentity());
|
|
2910
|
+
const nothing = { applying: [], breaking: [], unknown: [], userVersion: null, counted: [], backupPath: null };
|
|
2911
|
+
if (!existsSync10(path)) {
|
|
2912
|
+
return { path, status: "missing", storeVersion: null, logged: false, appliedBy, ...nothing };
|
|
2913
|
+
}
|
|
2914
|
+
refuseInFlight(path);
|
|
2915
|
+
const store = readOnlySchema(path);
|
|
2916
|
+
const found = { path, storeVersion: store.userVersion, logged: store.log !== null, appliedBy };
|
|
2917
|
+
const plan = planSchema(store, sqliteCatalogue(migrations), "write", "migrate");
|
|
2918
|
+
if (plan.verdict === "refuse")
|
|
2919
|
+
throw new RebuildRefusal(plan.message);
|
|
2920
|
+
const applying = writeNames(plan);
|
|
2921
|
+
const planned = {
|
|
2922
|
+
applying,
|
|
2923
|
+
breaking: plan.pending.filter(({ breaks }) => breaks.length > 0).map(({ id }) => id),
|
|
2924
|
+
unknown: plan.unknown
|
|
2925
|
+
};
|
|
2926
|
+
if (applying.length === 0)
|
|
2927
|
+
return { ...found, ...nothing, ...planned, status: "current" };
|
|
2928
|
+
if (!dryRun) {
|
|
2929
|
+
refuseUnownedDevelopmentWrite(path, applying, options);
|
|
2930
|
+
refuseLiveLoop2(path, planned.breaking, options);
|
|
2931
|
+
}
|
|
2932
|
+
const first = plan.pending[0]?.id ?? ADOPTION_NAME;
|
|
2933
|
+
const { built, backupPath } = rebuildAside({
|
|
2934
|
+
path,
|
|
2935
|
+
...asidePaths2(path, first, stamp),
|
|
2936
|
+
dryRun,
|
|
2937
|
+
build: (parallelPath) => buildMigrated(path, parallelPath, migrations, {
|
|
2938
|
+
appliedBy,
|
|
2939
|
+
now: options.now ?? (() => new Date)
|
|
2940
|
+
})
|
|
2941
|
+
});
|
|
2942
|
+
const status = dryRun ? "would-migrate" : "migrated";
|
|
2943
|
+
return { ...found, ...planned, ...built, status, backupPath };
|
|
1148
2944
|
}
|
|
1149
2945
|
|
|
1150
|
-
// src/effort/store/
|
|
1151
|
-
var
|
|
1152
|
-
|
|
1153
|
-
|
|
2946
|
+
// src/effort/store/merge-store.ts
|
|
2947
|
+
var MOVE_TO_SQLITE = "rafa effort move --to=sqlite";
|
|
2948
|
+
|
|
2949
|
+
class MergeRefusal extends RebuildRefusal {
|
|
2950
|
+
reason;
|
|
2951
|
+
constructor(reason, message) {
|
|
2952
|
+
super(message);
|
|
2953
|
+
this.name = "MergeRefusal";
|
|
2954
|
+
this.reason = reason;
|
|
2955
|
+
}
|
|
1154
2956
|
}
|
|
1155
|
-
var
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
var
|
|
1160
|
-
function
|
|
1161
|
-
|
|
2957
|
+
var LIVE3 = "live";
|
|
2958
|
+
var MERGES_TABLE = "merges";
|
|
2959
|
+
var INSERT_MERGE = `INSERT INTO merges (id, other_store, merged_at, rows_added, rows_skipped, rows_in_conflict)
|
|
2960
|
+
VALUES (?, ?, ?, ?, ?, ?)`;
|
|
2961
|
+
var NOTHING_MERGED = "Nothing was merged.";
|
|
2962
|
+
function refuseNdjson(backend) {
|
|
2963
|
+
if (backend !== "ndjson")
|
|
2964
|
+
return;
|
|
2965
|
+
throw new MergeRefusal("ndjson", `REFUSED \u2014 this project keeps its sessions and commits as NDJSON (store: ndjson), and a merge needs the SQLite store. ${NOTHING_MERGED}` + ` Next safe step: ${MOVE_TO_SQLITE}`);
|
|
1162
2966
|
}
|
|
1163
|
-
function
|
|
1164
|
-
|
|
2967
|
+
function refuseMissing(path, which) {
|
|
2968
|
+
if (existsSync11(path))
|
|
2969
|
+
return;
|
|
2970
|
+
throw new MergeRefusal("missing", `REFUSED \u2014 ${which} ${path} is not there. ${NOTHING_MERGED}`);
|
|
1165
2971
|
}
|
|
1166
|
-
function
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
2972
|
+
function refuseDamaged(path) {
|
|
2973
|
+
let answers;
|
|
2974
|
+
try {
|
|
2975
|
+
const db = new Database5(path, { readonly: true });
|
|
2976
|
+
try {
|
|
2977
|
+
answers = db.query("PRAGMA integrity_check").all().map((row) => row.integrity_check);
|
|
2978
|
+
} finally {
|
|
2979
|
+
db.close();
|
|
2980
|
+
}
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
if (!(error instanceof SQLiteError2))
|
|
2983
|
+
throw error;
|
|
2984
|
+
throw new MergeRefusal("damaged", `REFUSED \u2014 ${path} cannot be read as a SQLite store (${error.message}). ${NOTHING_MERGED}` + " Take a consistent copy on its device with rafa effort copy, and merge that.");
|
|
1178
2985
|
}
|
|
1179
|
-
|
|
2986
|
+
if (answers.length === 1 && answers[0] === "ok")
|
|
2987
|
+
return;
|
|
2988
|
+
throw new MergeRefusal("damaged", `REFUSED \u2014 ${path} failed integrity_check: ${answers.join("; ")}. ${NOTHING_MERGED}` + " Take a consistent copy on its device with rafa effort copy, and merge that.");
|
|
1180
2989
|
}
|
|
1181
|
-
function
|
|
1182
|
-
|
|
1183
|
-
if (
|
|
1184
|
-
|
|
1185
|
-
|
|
2990
|
+
function writablePlan(path, migrations) {
|
|
2991
|
+
const plan = planSchema(readOnlySchema(path), sqliteCatalogue(migrations), "write", "open");
|
|
2992
|
+
if (plan.verdict === "refuse")
|
|
2993
|
+
throw new MergeRefusal("schema", `REFUSED \u2014 ${path}: ${plan.message}`);
|
|
2994
|
+
return plan;
|
|
2995
|
+
}
|
|
2996
|
+
function recordedMeta(path) {
|
|
2997
|
+
const db = new Database5(path, { readonly: true });
|
|
1186
2998
|
try {
|
|
1187
|
-
db.
|
|
1188
|
-
|
|
1189
|
-
return use(db);
|
|
2999
|
+
const hasTable = db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
|
|
3000
|
+
return hasTable ? readStoreMeta(db) : null;
|
|
1190
3001
|
} finally {
|
|
1191
3002
|
db.close();
|
|
1192
3003
|
}
|
|
1193
3004
|
}
|
|
1194
|
-
function
|
|
1195
|
-
const
|
|
1196
|
-
const
|
|
1197
|
-
|
|
3005
|
+
function refuseOtherProject(options) {
|
|
3006
|
+
const other = recordedMeta(options.otherPath);
|
|
3007
|
+
const here = recordedMeta(options.path)?.projectRootCommit ?? (options.readProject ?? readProjectIdentity)(dirname10(options.path)).rootCommit;
|
|
3008
|
+
const there = other?.projectRootCommit ?? null;
|
|
3009
|
+
if (here !== null && there !== null && here !== there) {
|
|
3010
|
+
throw new MergeRefusal("other-project", `REFUSED \u2014 ${options.otherPath} is the effort store of another project (root commit ${there});` + ` this store's project has root commit ${here}. ${NOTHING_MERGED}`);
|
|
3011
|
+
}
|
|
3012
|
+
return other?.storeId ?? null;
|
|
1198
3013
|
}
|
|
1199
|
-
function
|
|
1200
|
-
const
|
|
1201
|
-
if (
|
|
1202
|
-
return
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
3014
|
+
function refuseLiveLoop3(path, options) {
|
|
3015
|
+
const loop = liveLoop(path, options, NOTHING_MERGED);
|
|
3016
|
+
if (loop === undefined)
|
|
3017
|
+
return;
|
|
3018
|
+
throw new MergeRefusal("live-loop", `REFUSED \u2014 loop ${loop.sessionId} (pid ${String(loop.pid)}, plan ${loop.planStub ?? loop.plan}) is running on this store,` + ` and swapping the merged store in under it would lose what it writes. ${NOTHING_MERGED}` + " Finish or stop that loop, then run it again.");
|
|
3019
|
+
}
|
|
3020
|
+
function openOther(otherPath, plan, stamp) {
|
|
3021
|
+
const needs = writeNames(plan);
|
|
3022
|
+
if (needs.length === 0) {
|
|
3023
|
+
const db = new Database5(otherPath, { readonly: true });
|
|
3024
|
+
return { db, broughtForward: [], release: () => db.close() };
|
|
3025
|
+
}
|
|
3026
|
+
const dir = mkdtempSync(join11(tmpdir3(), "rafa-merge-other-"));
|
|
3027
|
+
const release = (db) => {
|
|
3028
|
+
db?.close();
|
|
3029
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
3030
|
+
};
|
|
3031
|
+
try {
|
|
3032
|
+
const copyPath = join11(dir, "other.sqlite");
|
|
3033
|
+
vacuumInto(otherPath, copyPath);
|
|
3034
|
+
const db = new Database5(copyPath, { readwrite: true });
|
|
3035
|
+
try {
|
|
3036
|
+
bringForward(db, copyPath, "write", "open", { ...stamp, builtAside: true });
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
release(db);
|
|
3039
|
+
throw error;
|
|
3040
|
+
}
|
|
3041
|
+
return { db, broughtForward: needs, release: () => release(db) };
|
|
3042
|
+
} catch (error) {
|
|
3043
|
+
release();
|
|
3044
|
+
throw error;
|
|
3045
|
+
}
|
|
1208
3046
|
}
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
3047
|
+
function mergeInto(db, other, mergeId, otherStore, mergedAt) {
|
|
3048
|
+
return db.transaction(() => {
|
|
3049
|
+
const unions = unionStores(db, other);
|
|
3050
|
+
const settlements = settleMatches(db, other, unions, { mergeId, recordedAt: mergedAt });
|
|
3051
|
+
const gaps = recomputeCommitGaps(db, unions.find(({ table }) => table === "commits")?.added ?? []);
|
|
3052
|
+
const tables = unions.map((union, index) => {
|
|
3053
|
+
const settled = settlements[index];
|
|
3054
|
+
return {
|
|
3055
|
+
table: union.table,
|
|
3056
|
+
added: union.added.length,
|
|
3057
|
+
skipped: settled?.skipped.length ?? 0,
|
|
3058
|
+
inConflict: settled?.conflicts.length ?? 0,
|
|
3059
|
+
filled: settled?.filled.length ?? 0,
|
|
3060
|
+
collided: union.collided.length
|
|
3061
|
+
};
|
|
3062
|
+
});
|
|
3063
|
+
const sum = (field2) => tables.reduce((total, entry) => total + entry[field2], 0);
|
|
3064
|
+
const totals = { rowsAdded: sum("added"), rowsSkipped: sum("skipped"), rowsInConflict: sum("inConflict") };
|
|
3065
|
+
db.query(INSERT_MERGE).run(mergeId, otherStore, mergedAt, totals.rowsAdded, totals.rowsSkipped, totals.rowsInConflict);
|
|
3066
|
+
return { tables, ...totals, gapsRewritten: gaps.length };
|
|
3067
|
+
})();
|
|
1214
3068
|
}
|
|
1215
|
-
function
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
3069
|
+
function addedRows(merged) {
|
|
3070
|
+
return {
|
|
3071
|
+
...Object.fromEntries(merged.tables.map(({ table, added }) => [table, added])),
|
|
3072
|
+
[MERGES_TABLE]: 1,
|
|
3073
|
+
merge_conflicts: merged.rowsInConflict
|
|
3074
|
+
};
|
|
1221
3075
|
}
|
|
1222
|
-
function
|
|
3076
|
+
function buildMerged(path, parallelPath, other, merge2) {
|
|
3077
|
+
vacuumInto(path, parallelPath);
|
|
3078
|
+
const db = new Database5(parallelPath, { readwrite: true });
|
|
1223
3079
|
try {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
3080
|
+
bringForward(db, parallelPath, "write", "open", { ...merge2.stamp, builtAside: true });
|
|
3081
|
+
const merged = mergeInto(db, other, merge2.id, merge2.otherStore, merge2.stamp.now().toISOString());
|
|
3082
|
+
db.run(`ATTACH DATABASE ? AS ${LIVE3}`, [path]);
|
|
3083
|
+
const live = new Set(tableNames(db, LIVE3));
|
|
3084
|
+
checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE3, addedRows(merged));
|
|
3085
|
+
db.run(`DETACH DATABASE ${LIVE3}`);
|
|
3086
|
+
refuseCorrupt(db);
|
|
3087
|
+
refuseUnusable(db, parallelPath, merge2.stamp.migrations);
|
|
3088
|
+
return merged;
|
|
3089
|
+
} finally {
|
|
3090
|
+
db.close();
|
|
1228
3091
|
}
|
|
1229
3092
|
}
|
|
1230
|
-
function
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
3093
|
+
function refuseUnmergeable(options, migrations) {
|
|
3094
|
+
const { path, otherPath } = options;
|
|
3095
|
+
refuseNdjson(options.backend);
|
|
3096
|
+
refuseMissing(path, "this store");
|
|
3097
|
+
refuseMissing(otherPath, "the other store");
|
|
3098
|
+
refuseInFlight(path);
|
|
3099
|
+
refuseInFlight(otherPath);
|
|
3100
|
+
refuseDamaged(otherPath);
|
|
3101
|
+
const otherPlan = writablePlan(otherPath, migrations);
|
|
3102
|
+
const localPlan = writablePlan(path, migrations);
|
|
3103
|
+
const otherStore = refuseOtherProject(options);
|
|
3104
|
+
if (!options.dryRun) {
|
|
3105
|
+
refuseUnownedDevelopmentWrite(path, [...writeNames(localPlan), MERGES_TABLE], options);
|
|
3106
|
+
refuseLiveLoop3(path, options);
|
|
3107
|
+
}
|
|
3108
|
+
return { otherPlan, otherStore };
|
|
3109
|
+
}
|
|
3110
|
+
function mergeStore(options) {
|
|
3111
|
+
const { path, otherPath, dryRun, stamp } = options;
|
|
3112
|
+
guardTestProcess(path);
|
|
3113
|
+
guardTestProcess(otherPath);
|
|
3114
|
+
const migrations = options.migrations ?? SQLITE_MIGRATIONS;
|
|
3115
|
+
const { otherPlan, otherStore } = refuseUnmergeable(options, migrations);
|
|
3116
|
+
const mergeStamp = {
|
|
3117
|
+
migrations,
|
|
3118
|
+
appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
|
|
3119
|
+
now: options.now ?? (() => new Date)
|
|
3120
|
+
};
|
|
3121
|
+
const mergeId = (options.newMergeId ?? randomUUID4)();
|
|
3122
|
+
const other = openOther(otherPath, otherPlan, mergeStamp);
|
|
3123
|
+
try {
|
|
3124
|
+
const { built, backupPath } = rebuildAside({
|
|
3125
|
+
path,
|
|
3126
|
+
parallelPath: `${path}.merge-${stamp}`,
|
|
3127
|
+
backupPath: `${path}.before-merge-${stamp}.bak`,
|
|
3128
|
+
dryRun,
|
|
3129
|
+
build: (parallelPath) => buildMerged(path, parallelPath, other.db, { id: mergeId, otherStore, stamp: mergeStamp })
|
|
3130
|
+
});
|
|
3131
|
+
const status = dryRun ? "would-merge" : "merged";
|
|
3132
|
+
return { path, otherPath, status, mergeId, otherStore, otherBroughtForward: other.broughtForward, ...built, backupPath };
|
|
3133
|
+
} finally {
|
|
3134
|
+
other.release();
|
|
1234
3135
|
}
|
|
1235
|
-
return value;
|
|
1236
3136
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
3137
|
+
|
|
3138
|
+
// src/effort/sync/file.ts
|
|
3139
|
+
var REFUSAL = "effort sync (file)";
|
|
3140
|
+
var NOTHING_COPIED = "Nothing was copied.";
|
|
3141
|
+
|
|
3142
|
+
class FileSyncRefusal extends Error {
|
|
3143
|
+
name = "FileSyncRefusal";
|
|
1243
3144
|
}
|
|
1244
|
-
function
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
3145
|
+
function requirePath(field2, value) {
|
|
3146
|
+
if (typeof value === "string" && value !== "")
|
|
3147
|
+
return value;
|
|
3148
|
+
throw new TypeError(`${REFUSAL}: ${field2} is ${describeValue(value)}, expected a path`);
|
|
3149
|
+
}
|
|
3150
|
+
function refuseUncarriable(backend, source) {
|
|
3151
|
+
if (backend === "ndjson") {
|
|
3152
|
+
throw new FileSyncRefusal(`${REFUSAL}: this project keeps its sessions and commits as NDJSON (store: ndjson), and the file carried` + ` is effort.sqlite, which would hold none of them. ${NOTHING_COPIED} Next safe step: ${MOVE_TO_SQLITE}`);
|
|
3153
|
+
}
|
|
3154
|
+
if (!existsSync12(join12(source, SQLITE_STORE_FILE_NAME))) {
|
|
3155
|
+
throw new FileSyncRefusal(`${REFUSAL}: no ${SQLITE_STORE_FILE_NAME} in ${source}. ${NOTHING_COPIED}`);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
function mergeSeams(options) {
|
|
3159
|
+
const { identity, env, tempDir, isAlive, migrations, newMergeId, readProject } = options;
|
|
3160
|
+
const seams = { identity, env, tempDir, isAlive, migrations, newMergeId, readProject };
|
|
3161
|
+
return Object.fromEntries(Object.entries(seams).filter(([, value]) => value !== undefined));
|
|
3162
|
+
}
|
|
3163
|
+
function createFileSync(options) {
|
|
3164
|
+
const { repoRoot, backend } = options;
|
|
3165
|
+
const now = options.now ?? (() => new Date);
|
|
3166
|
+
const storeDir = () => effortStoreDir(repoRoot, options.env);
|
|
3167
|
+
const push = async ({ to }) => {
|
|
3168
|
+
const target = resolve5(repoRoot, requirePath("to", to));
|
|
3169
|
+
const source = storeDir();
|
|
3170
|
+
refuseUncarriable(backend, source);
|
|
3171
|
+
const copied = copyEffortStore({ source, target });
|
|
3172
|
+
return Object.freeze({ status: "pushed", path: join12(copied.directory, SQLITE_STORE_FILE_NAME) });
|
|
3173
|
+
};
|
|
3174
|
+
const pull = async ({ from, dryRun }) => {
|
|
3175
|
+
const otherPath = resolve5(repoRoot, requirePath("from", from));
|
|
3176
|
+
const path = join12(storeDir(), SQLITE_STORE_FILE_NAME);
|
|
3177
|
+
const merge2 = mergeStore({ ...mergeSeams(options), path, otherPath, backend, dryRun, stamp: fileStamp(now()), now });
|
|
3178
|
+
return Object.freeze({ status: "pulled", merge: merge2 });
|
|
1251
3179
|
};
|
|
3180
|
+
return Object.freeze({ kind: "file", push, pull });
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/effort/sync/select.ts
|
|
3184
|
+
var REFUSAL2 = "effort sync";
|
|
3185
|
+
var MODULE_SYNC_STRATEGIES = Object.freeze(["git", "service", "p2p"]);
|
|
3186
|
+
var MODULE_DIRECTORY_PLACEHOLDER = "<module directory>";
|
|
3187
|
+
var MODULE_NAME_PLACEHOLDER = "<module name>";
|
|
3188
|
+
var NOTHING_TO_SYNC = Object.freeze({ status: "nothing-to-sync" });
|
|
3189
|
+
function createLocalSync() {
|
|
3190
|
+
return Object.freeze({
|
|
3191
|
+
kind: "local",
|
|
3192
|
+
push: async () => NOTHING_TO_SYNC,
|
|
3193
|
+
pull: async () => NOTHING_TO_SYNC
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
function listed(registered) {
|
|
3197
|
+
return registered.length === 0 ? "none" : registered.join(", ");
|
|
3198
|
+
}
|
|
3199
|
+
function moduleMissingMessage(kind, registered) {
|
|
3200
|
+
return [
|
|
3201
|
+
`${REFUSAL2}: effort.sync is "${kind}", and no module registers a sync adapter of that kind` + ` (registered: ${listed(registered)}). Core ships no ${kind} strategy; load a module that provides` + " one with these lines in .rafa/config.yaml:",
|
|
3202
|
+
"modules:",
|
|
3203
|
+
` - path: ${MODULE_DIRECTORY_PLACEHOLDER}`,
|
|
3204
|
+
"allowList:",
|
|
3205
|
+
` - ${MODULE_NAME_PLACEHOLDER}`
|
|
3206
|
+
].join(`
|
|
3207
|
+
`);
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
class SyncModuleMissing extends Error {
|
|
3211
|
+
name = "SyncModuleMissing";
|
|
3212
|
+
kind;
|
|
3213
|
+
constructor(kind, registered) {
|
|
3214
|
+
super(moduleMissingMessage(kind, registered));
|
|
3215
|
+
this.kind = kind;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
function isModuleStrategy(kind) {
|
|
3219
|
+
return MODULE_SYNC_STRATEGIES.includes(kind);
|
|
3220
|
+
}
|
|
3221
|
+
function selectSync(repoRoot, config, registry = CORE_ADAPTER_REGISTRY) {
|
|
3222
|
+
const kind = config.effortSync;
|
|
3223
|
+
const adapter = typeof kind === "string" ? registry.find("sync", kind) : undefined;
|
|
3224
|
+
if (adapter !== undefined) {
|
|
3225
|
+
return adapter.create(config.store === undefined ? { repoRoot } : { repoRoot, store: config.store });
|
|
3226
|
+
}
|
|
3227
|
+
const registered = registry.kinds("sync");
|
|
3228
|
+
if (isModuleStrategy(kind))
|
|
3229
|
+
throw new SyncModuleMissing(kind, registered);
|
|
3230
|
+
const known = SYNC_STRATEGIES.includes(kind);
|
|
3231
|
+
const expected = known ? `registered: ${listed(registered)}` : `expected one of: ${SYNC_STRATEGIES.join(", ")}`;
|
|
3232
|
+
throw new TypeError(`${REFUSAL2}: effort.sync is ${describeValue(kind)}, and no sync adapter is registered under it; ${expected}`);
|
|
1252
3233
|
}
|
|
1253
3234
|
|
|
1254
3235
|
// src/adapters/learning/local.ts
|
|
1255
3236
|
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
1256
|
-
import { join as
|
|
3237
|
+
import { join as join15 } from "path";
|
|
1257
3238
|
|
|
1258
3239
|
// src/commands/instinct/instinct-records.ts
|
|
1259
|
-
import { readdirSync as
|
|
1260
|
-
import { join as
|
|
3240
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
|
|
3241
|
+
import { join as join14 } from "path";
|
|
1261
3242
|
|
|
1262
3243
|
// src/check/layout.ts
|
|
1263
|
-
import { readdirSync as
|
|
1264
|
-
import { join as
|
|
3244
|
+
import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
3245
|
+
import { join as join13, relative as relative2 } from "path";
|
|
1265
3246
|
var SKILL_FILE = "SKILL.md";
|
|
1266
3247
|
var MARKDOWN_EXTENSION = ".md";
|
|
1267
3248
|
var LAYOUT_SEVERITY = Object.freeze({
|
|
@@ -1279,7 +3260,7 @@ function layoutIssue(code, message) {
|
|
|
1279
3260
|
}
|
|
1280
3261
|
function entryKind(path) {
|
|
1281
3262
|
try {
|
|
1282
|
-
const stats =
|
|
3263
|
+
const stats = statSync4(path);
|
|
1283
3264
|
if (stats.isFile())
|
|
1284
3265
|
return "file";
|
|
1285
3266
|
if (stats.isDirectory())
|
|
@@ -1290,7 +3271,7 @@ function entryKind(path) {
|
|
|
1290
3271
|
}
|
|
1291
3272
|
}
|
|
1292
3273
|
function visibleNames(dir) {
|
|
1293
|
-
return
|
|
3274
|
+
return readdirSync3(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
|
|
1294
3275
|
}
|
|
1295
3276
|
function isMarkdown(name2) {
|
|
1296
3277
|
return name2.endsWith(MARKDOWN_EXTENSION);
|
|
@@ -1378,7 +3359,7 @@ function scanGroupMember(root, dir, name2) {
|
|
|
1378
3359
|
const children = visibleNames(dir);
|
|
1379
3360
|
if (children.length === 0)
|
|
1380
3361
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
1381
|
-
const skillPath =
|
|
3362
|
+
const skillPath = join13(dir, SKILL_FILE);
|
|
1382
3363
|
if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
|
|
1383
3364
|
return [nestedGroupEntry(root, skillPath, name2)];
|
|
1384
3365
|
}
|
|
@@ -1387,7 +3368,7 @@ function scanGroupMember(root, dir, name2) {
|
|
|
1387
3368
|
function scanSkillGroup(root, dir, children) {
|
|
1388
3369
|
const entries = [];
|
|
1389
3370
|
for (const child of children) {
|
|
1390
|
-
const path =
|
|
3371
|
+
const path = join13(dir, child);
|
|
1391
3372
|
const found = entryKind(path);
|
|
1392
3373
|
if (found === null) {
|
|
1393
3374
|
entries.push(unreadableEntry("skill", root, path));
|
|
@@ -1408,7 +3389,7 @@ function scanSkillDirectory(root, dir, name2) {
|
|
|
1408
3389
|
const children = visibleNames(dir);
|
|
1409
3390
|
if (children.length === 0)
|
|
1410
3391
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
1411
|
-
const skillPath =
|
|
3392
|
+
const skillPath = join13(dir, SKILL_FILE);
|
|
1412
3393
|
if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
|
|
1413
3394
|
return [skillFileEntry(skillPath, name2)];
|
|
1414
3395
|
}
|
|
@@ -1417,7 +3398,7 @@ function scanSkillDirectory(root, dir, name2) {
|
|
|
1417
3398
|
function scanSkillRoot(root) {
|
|
1418
3399
|
const entries = [];
|
|
1419
3400
|
for (const name2 of visibleNames(root)) {
|
|
1420
|
-
const path =
|
|
3401
|
+
const path = join13(root, name2);
|
|
1421
3402
|
const found = entryKind(path);
|
|
1422
3403
|
if (found === null) {
|
|
1423
3404
|
entries.push(unreadableEntry("skill", root, path));
|
|
@@ -1438,7 +3419,7 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
1438
3419
|
return [dotfileOnlyEntry("instinct", root, dir)];
|
|
1439
3420
|
const entries = [];
|
|
1440
3421
|
for (const child of children) {
|
|
1441
|
-
const path =
|
|
3422
|
+
const path = join13(dir, child);
|
|
1442
3423
|
const found = entryKind(path);
|
|
1443
3424
|
if (found === null) {
|
|
1444
3425
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -1456,7 +3437,7 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
1456
3437
|
function scanInstinctRoot(root) {
|
|
1457
3438
|
const entries = [];
|
|
1458
3439
|
for (const name2 of visibleNames(root)) {
|
|
1459
|
-
const path =
|
|
3440
|
+
const path = join13(root, name2);
|
|
1460
3441
|
const found = entryKind(path);
|
|
1461
3442
|
if (found === null) {
|
|
1462
3443
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -1515,7 +3496,7 @@ function scanLine(text, offset) {
|
|
|
1515
3496
|
next: index + 1
|
|
1516
3497
|
};
|
|
1517
3498
|
}
|
|
1518
|
-
function
|
|
3499
|
+
function isRecord3(value) {
|
|
1519
3500
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1520
3501
|
}
|
|
1521
3502
|
function parseBlock(yaml) {
|
|
@@ -1526,7 +3507,7 @@ function parseBlock(yaml) {
|
|
|
1526
3507
|
} catch {
|
|
1527
3508
|
return null;
|
|
1528
3509
|
}
|
|
1529
|
-
return
|
|
3510
|
+
return isRecord3(parsed) ? parsed : null;
|
|
1530
3511
|
}
|
|
1531
3512
|
function readFrontmatterDocument(text) {
|
|
1532
3513
|
const opening = scanLine(text, 0);
|
|
@@ -1583,32 +3564,6 @@ function writeFrontmatter(document, data) {
|
|
|
1583
3564
|
].join("");
|
|
1584
3565
|
}
|
|
1585
3566
|
|
|
1586
|
-
// src/schema/project-id.ts
|
|
1587
|
-
import { spawnSync } from "child_process";
|
|
1588
|
-
var PROJECT_ID_LENGTH = 12;
|
|
1589
|
-
var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
|
|
1590
|
-
function hasColonAfterAt(url) {
|
|
1591
|
-
const at = url.indexOf("@");
|
|
1592
|
-
return at !== -1 && url.lastIndexOf(":") > at;
|
|
1593
|
-
}
|
|
1594
|
-
function normalizeRemote(url) {
|
|
1595
|
-
const stripped = url.replace(/:\/\/[^@]+@/, "://");
|
|
1596
|
-
const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
|
|
1597
|
-
const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
1598
|
-
return network ? trimmed.toLowerCase() : trimmed;
|
|
1599
|
-
}
|
|
1600
|
-
function gitRemoteUrl(dir) {
|
|
1601
|
-
const result = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
1602
|
-
cwd: dir,
|
|
1603
|
-
encoding: "utf8",
|
|
1604
|
-
env: { ...process.env, LC_ALL: "C" }
|
|
1605
|
-
});
|
|
1606
|
-
if (result.error !== undefined || result.status !== 0)
|
|
1607
|
-
return null;
|
|
1608
|
-
const url = result.stdout.trim();
|
|
1609
|
-
return url === "" ? null : url;
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
3567
|
// src/schema/instinct.ts
|
|
1613
3568
|
var INSTINCT_KINDS = FINDING_KINDS;
|
|
1614
3569
|
var INSTINCT_SIGNALS = FINDING_SIGNALS;
|
|
@@ -1935,7 +3890,7 @@ function unreadableIssue(message) {
|
|
|
1935
3890
|
function readRecord(path, scope, id) {
|
|
1936
3891
|
let text;
|
|
1937
3892
|
try {
|
|
1938
|
-
text =
|
|
3893
|
+
text = readFileSync8(path, "utf8");
|
|
1939
3894
|
} catch (error) {
|
|
1940
3895
|
const message = error instanceof Error ? error.message : String(error);
|
|
1941
3896
|
return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
|
|
@@ -1943,15 +3898,15 @@ function readRecord(path, scope, id) {
|
|
|
1943
3898
|
const parsed = parseInstinct(text);
|
|
1944
3899
|
return { scope, path, id, instinct: parsed.instinct, issues: parsed.issues };
|
|
1945
3900
|
}
|
|
1946
|
-
function
|
|
3901
|
+
function isFile2(path) {
|
|
1947
3902
|
try {
|
|
1948
|
-
return
|
|
3903
|
+
return statSync5(path).isFile();
|
|
1949
3904
|
} catch {
|
|
1950
3905
|
return false;
|
|
1951
3906
|
}
|
|
1952
3907
|
}
|
|
1953
3908
|
function recordFiles(dir) {
|
|
1954
|
-
return
|
|
3909
|
+
return readdirSync4(dir).filter((name2) => isRecordFile(name2) && isFile2(join14(dir, name2))).sort((left, right) => left.localeCompare(right));
|
|
1955
3910
|
}
|
|
1956
3911
|
function readScope(scope, dir) {
|
|
1957
3912
|
if (!tierExists(dir))
|
|
@@ -1960,7 +3915,7 @@ function readScope(scope, dir) {
|
|
|
1960
3915
|
scope,
|
|
1961
3916
|
dir,
|
|
1962
3917
|
exists: true,
|
|
1963
|
-
records: recordFiles(dir).map((name2) => readRecord(
|
|
3918
|
+
records: recordFiles(dir).map((name2) => readRecord(join14(dir, name2), scope, recordId(name2)))
|
|
1964
3919
|
};
|
|
1965
3920
|
}
|
|
1966
3921
|
function readScopes(seams) {
|
|
@@ -1979,7 +3934,7 @@ function instinctProject(context, name2) {
|
|
|
1979
3934
|
}
|
|
1980
3935
|
|
|
1981
3936
|
// src/adapters/learning/held.ts
|
|
1982
|
-
function
|
|
3937
|
+
function compareText2(left, right) {
|
|
1983
3938
|
if (left < right) {
|
|
1984
3939
|
return -1;
|
|
1985
3940
|
}
|
|
@@ -1989,7 +3944,7 @@ function compareText(left, right) {
|
|
|
1989
3944
|
return 0;
|
|
1990
3945
|
}
|
|
1991
3946
|
function byAge(left, right) {
|
|
1992
|
-
return
|
|
3947
|
+
return compareText2(left.createdAt, right.createdAt) || compareText2(left.id, right.id);
|
|
1993
3948
|
}
|
|
1994
3949
|
function toHeldRecord(instinct) {
|
|
1995
3950
|
return {
|
|
@@ -2263,7 +4218,7 @@ function heldChanges(dir, held, result, described) {
|
|
|
2263
4218
|
if (issue2 !== undefined) {
|
|
2264
4219
|
throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue2.field}: ${issue2.message}`);
|
|
2265
4220
|
}
|
|
2266
|
-
const path =
|
|
4221
|
+
const path = join15(dir, file);
|
|
2267
4222
|
if (held.unreadable.has(path)) {
|
|
2268
4223
|
throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
|
|
2269
4224
|
}
|
|
@@ -2293,18 +4248,18 @@ function pushLogLines(sourceId, described, result) {
|
|
|
2293
4248
|
});
|
|
2294
4249
|
}
|
|
2295
4250
|
function localInstinctsDir(repoRoot) {
|
|
2296
|
-
return
|
|
4251
|
+
return join15(repoRoot, RAFA_INSTINCTS_PATH);
|
|
2297
4252
|
}
|
|
2298
4253
|
function userInstinctsDir(home) {
|
|
2299
|
-
return
|
|
4254
|
+
return join15(home, RAFA_INSTINCTS_PATH);
|
|
2300
4255
|
}
|
|
2301
4256
|
function createLocalLearning(options) {
|
|
2302
4257
|
const { instinctsDir, minConfidence } = options;
|
|
2303
4258
|
const userDir = userInstinctsDir(options.home);
|
|
2304
4259
|
const now = options.now ?? (() => new Date().toISOString());
|
|
2305
4260
|
const warn = options.warn ?? warnThroughActiveOutput;
|
|
2306
|
-
const instinctsPath =
|
|
2307
|
-
const flagsPath =
|
|
4261
|
+
const instinctsPath = join15(instinctsDir, INSTINCTS_FILE);
|
|
4262
|
+
const flagsPath = join15(instinctsDir, FLAGS_FILE);
|
|
2308
4263
|
function readHeld(scope, dir) {
|
|
2309
4264
|
const { records } = readScope(scope, dir);
|
|
2310
4265
|
const unreadable = new Set;
|
|
@@ -2421,9 +4376,9 @@ function createJsonOutput({
|
|
|
2421
4376
|
}
|
|
2422
4377
|
|
|
2423
4378
|
// src/adapters/planner/claude.ts
|
|
2424
|
-
import { existsSync as
|
|
4379
|
+
import { existsSync as existsSync13 } from "fs";
|
|
2425
4380
|
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
2426
|
-
import { posix, resolve as
|
|
4381
|
+
import { posix, resolve as resolve6 } from "path";
|
|
2427
4382
|
|
|
2428
4383
|
// src/board/spec-review.ts
|
|
2429
4384
|
var SPEC_REVIEW_KIND = "spec-review";
|
|
@@ -2459,8 +4414,8 @@ function describeValue3(value) {
|
|
|
2459
4414
|
function messageOf2(error) {
|
|
2460
4415
|
return error instanceof Error ? error.message : String(error);
|
|
2461
4416
|
}
|
|
2462
|
-
function counted(
|
|
2463
|
-
return
|
|
4417
|
+
function counted(count2, noun) {
|
|
4418
|
+
return count2 === 1 ? `1 ${noun}` : `${String(count2)} ${noun}s`;
|
|
2464
4419
|
}
|
|
2465
4420
|
function valueAt(mapping, key) {
|
|
2466
4421
|
return Object.hasOwn(mapping, key) ? mapping[key] ?? null : null;
|
|
@@ -2686,17 +4641,18 @@ function guardSpend() {
|
|
|
2686
4641
|
if (refusal !== null)
|
|
2687
4642
|
throw new UndeclaredSpendError(refusal);
|
|
2688
4643
|
}
|
|
2689
|
-
async function spawnClaude(args, prompt) {
|
|
4644
|
+
async function spawnClaude(args, prompt, options = {}) {
|
|
2690
4645
|
guardSpend();
|
|
2691
4646
|
if (activeOutputMode() === "json") {
|
|
2692
|
-
const { exitCode } = await spawnCaptured(args, prompt);
|
|
4647
|
+
const { exitCode } = await spawnCaptured(args, prompt, options);
|
|
2693
4648
|
return exitCode;
|
|
2694
4649
|
}
|
|
2695
4650
|
const proc = Bun.spawn([CLAUDE_BIN, ...args], {
|
|
2696
4651
|
stdin: new TextEncoder().encode(prompt),
|
|
2697
4652
|
stdout: "inherit",
|
|
2698
4653
|
stderr: "inherit",
|
|
2699
|
-
env: sessionSpawnEnv(process.env)
|
|
4654
|
+
env: sessionSpawnEnv(process.env),
|
|
4655
|
+
...cwdOption(options)
|
|
2700
4656
|
});
|
|
2701
4657
|
liveSessions.add(proc);
|
|
2702
4658
|
try {
|
|
@@ -2705,8 +4661,11 @@ async function spawnClaude(args, prompt) {
|
|
|
2705
4661
|
liveSessions.delete(proc);
|
|
2706
4662
|
}
|
|
2707
4663
|
}
|
|
2708
|
-
function runClaude(prompt, settingSources, flags = [], spawn = spawnClaude, served = []) {
|
|
2709
|
-
return spawn(claudeArgs(settingSources, flags, served), prompt);
|
|
4664
|
+
function runClaude(prompt, settingSources, flags = [], spawn = spawnClaude, served = [], options = {}) {
|
|
4665
|
+
return spawn(claudeArgs(settingSources, flags, served), prompt, options);
|
|
4666
|
+
}
|
|
4667
|
+
function cwdOption(options) {
|
|
4668
|
+
return options.cwd === undefined ? {} : { cwd: options.cwd };
|
|
2710
4669
|
}
|
|
2711
4670
|
function writeWholeLines(text, write) {
|
|
2712
4671
|
const lines = text.split(`
|
|
@@ -2750,7 +4709,7 @@ async function spawnCaptured(args, prompt, options = {}) {
|
|
|
2750
4709
|
stdout: "pipe",
|
|
2751
4710
|
stderr: "inherit",
|
|
2752
4711
|
env: sessionSpawnEnv(process.env),
|
|
2753
|
-
...options
|
|
4712
|
+
...cwdOption(options)
|
|
2754
4713
|
});
|
|
2755
4714
|
liveSessions.add(proc);
|
|
2756
4715
|
let stdout;
|
|
@@ -2762,8 +4721,8 @@ async function spawnCaptured(args, prompt, options = {}) {
|
|
|
2762
4721
|
}
|
|
2763
4722
|
return { exitCode: await proc.exited, stdout };
|
|
2764
4723
|
}
|
|
2765
|
-
function runClaudeCaptured(prompt, settingSources, flags = [], spawn = spawnClaudeCaptured, served = []) {
|
|
2766
|
-
return spawn(claudeArgs(settingSources, flags, served), prompt);
|
|
4724
|
+
function runClaudeCaptured(prompt, settingSources, flags = [], spawn = spawnClaudeCaptured, served = [], options = {}) {
|
|
4725
|
+
return spawn(claudeArgs(settingSources, flags, served), prompt, options);
|
|
2767
4726
|
}
|
|
2768
4727
|
|
|
2769
4728
|
// src/adapters/planner/claude.ts
|
|
@@ -2784,23 +4743,24 @@ class ClaudePlannerError extends Error {
|
|
|
2784
4743
|
}
|
|
2785
4744
|
function createClaudePlanner(options) {
|
|
2786
4745
|
const { repoRoot, planDir, settingSources, buildPrompt, spawn = spawnClaudeCaptured } = options;
|
|
4746
|
+
const root = resolve6(repoRoot);
|
|
2787
4747
|
const create = async ({ specPath, stub }) => {
|
|
2788
4748
|
const planPath = planFilePath(planDir, `PLAN-${stub}.md`);
|
|
2789
4749
|
const prerequisitesPath = planFilePath(planDir, `PREREQUISITES-${stub}.md`);
|
|
2790
|
-
const isWritten = (path) =>
|
|
4750
|
+
const isWritten = (path) => existsSync13(resolve6(root, path));
|
|
2791
4751
|
if (isWritten(planPath)) {
|
|
2792
4752
|
throw new ClaudePlannerError(`${planPath} already exists`, FAILURE_EXIT_CODE);
|
|
2793
4753
|
}
|
|
2794
|
-
const specContent = await readFile2(
|
|
2795
|
-
await mkdir2(
|
|
2796
|
-
const session = await
|
|
4754
|
+
const specContent = await readFile2(resolve6(root, specPath), "utf8");
|
|
4755
|
+
await mkdir2(resolve6(root, planDir), { recursive: true });
|
|
4756
|
+
const session = await spawn(claudeArgs(settingSources), buildPrompt(specContent, stub), { cwd: root });
|
|
2797
4757
|
const review = parseSpecReview(session.stdout);
|
|
2798
4758
|
const { exitCode } = session;
|
|
2799
4759
|
if (exitCode !== 0) {
|
|
2800
4760
|
throw new ClaudePlannerError(`Plan generation failed (exit ${exitCode}).`, exitCode, review);
|
|
2801
4761
|
}
|
|
2802
4762
|
if (!isWritten(planPath)) {
|
|
2803
|
-
throw new ClaudePlannerError(`The session finished but ${planPath} was not created \u2014 inspect the output above.`, FAILURE_EXIT_CODE, review);
|
|
4763
|
+
throw new ClaudePlannerError(`The session finished but ${planPath} was not created under ${root} \u2014 inspect the output above.`, FAILURE_EXIT_CODE, review);
|
|
2804
4764
|
}
|
|
2805
4765
|
return {
|
|
2806
4766
|
planPath,
|
|
@@ -2903,8 +4863,8 @@ async function withDeadline(work, timeoutMs) {
|
|
|
2903
4863
|
if (timeoutMs === undefined)
|
|
2904
4864
|
return work;
|
|
2905
4865
|
let timer;
|
|
2906
|
-
const deadline = new Promise((
|
|
2907
|
-
timer = setTimeout(() =>
|
|
4866
|
+
const deadline = new Promise((resolve7) => {
|
|
4867
|
+
timer = setTimeout(() => resolve7(TIMED_OUT), timeoutMs);
|
|
2908
4868
|
});
|
|
2909
4869
|
try {
|
|
2910
4870
|
return await Promise.race([work, deadline]);
|
|
@@ -2919,7 +4879,7 @@ function notOnPath(command, cwd, path) {
|
|
|
2919
4879
|
function isOneOf(members, value) {
|
|
2920
4880
|
return typeof value === "string" && members.includes(value);
|
|
2921
4881
|
}
|
|
2922
|
-
function
|
|
4882
|
+
function isIssueNumber3(value) {
|
|
2923
4883
|
return typeof value === "string" && ISSUE_NUMBER.test(value) && Number.isSafeInteger(Number(value));
|
|
2924
4884
|
}
|
|
2925
4885
|
function isRepoName(value) {
|
|
@@ -2974,7 +4934,7 @@ function issueNumberOfRef(ref) {
|
|
|
2974
4934
|
if (ref.kind !== "github") {
|
|
2975
4935
|
throw new Error(`${PREFIX2}: refused a ref of kind ${describeValue(ref.kind)}; this tracker reads github refs only`);
|
|
2976
4936
|
}
|
|
2977
|
-
if (!
|
|
4937
|
+
if (!isIssueNumber3(ref.externalId)) {
|
|
2978
4938
|
throw new Error(`${PREFIX2}: externalId ${describeValue(ref.externalId)} is not a GitHub issue number,` + " expected a positive whole number with no leading zero");
|
|
2979
4939
|
}
|
|
2980
4940
|
if (ref.repo !== undefined && !isRepoName(ref.repo)) {
|
|
@@ -3036,7 +4996,7 @@ function listedIssues(stdout) {
|
|
|
3036
4996
|
throw refuse2(`${describeValue(row)}, expected a mapping`);
|
|
3037
4997
|
const { number, url } = row;
|
|
3038
4998
|
const labels = labelNames(row["labels"]);
|
|
3039
|
-
if (typeof number !== "number" || !
|
|
4999
|
+
if (typeof number !== "number" || !isIssueNumber3(String(number))) {
|
|
3040
5000
|
throw refuse2(`with number ${describeValue(number)}, expected an issue number`);
|
|
3041
5001
|
}
|
|
3042
5002
|
if (typeof url !== "string")
|
|
@@ -3071,7 +5031,7 @@ function issueOf(ref, viewed) {
|
|
|
3071
5031
|
module: moduleOfLabels(viewed.labels),
|
|
3072
5032
|
priority: isOneOf(ISSUE_PRIORITIES, priority) ? priority : null,
|
|
3073
5033
|
project: null,
|
|
3074
|
-
blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(
|
|
5034
|
+
blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(isIssueNumber3).map(Number),
|
|
3075
5035
|
ref: { ...ref, url: viewed.url },
|
|
3076
5036
|
state: stateOf(viewed)
|
|
3077
5037
|
};
|
|
@@ -3209,7 +5169,7 @@ function createGithubTracker(options) {
|
|
|
3209
5169
|
|
|
3210
5170
|
// src/adapters/tracker/local.ts
|
|
3211
5171
|
import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
3212
|
-
import { join as
|
|
5172
|
+
import { join as join16 } from "path";
|
|
3213
5173
|
var PREFIX3 = "local tracker";
|
|
3214
5174
|
var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
3215
5175
|
var ISSUE_NUMBER2 = /^[1-9]\d*$/;
|
|
@@ -3266,7 +5226,7 @@ function issueNumberOfFile(name2) {
|
|
|
3266
5226
|
return name2.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name2.slice(0, -ISSUE_EXTENSION.length)) : null;
|
|
3267
5227
|
}
|
|
3268
5228
|
function issuePath(issuesDir, number) {
|
|
3269
|
-
return
|
|
5229
|
+
return join16(issuesDir, `${number}${ISSUE_EXTENSION}`);
|
|
3270
5230
|
}
|
|
3271
5231
|
async function issueNumbers(issuesDir) {
|
|
3272
5232
|
let names;
|
|
@@ -3280,7 +5240,7 @@ async function issueNumbers(issuesDir) {
|
|
|
3280
5240
|
return names.map(issueNumberOfFile).filter((number) => number !== null).sort((a, b) => a - b);
|
|
3281
5241
|
}
|
|
3282
5242
|
function localIssuesDir(repoRoot) {
|
|
3283
|
-
return
|
|
5243
|
+
return join16(repoRoot, ".rafa", "issues");
|
|
3284
5244
|
}
|
|
3285
5245
|
function renderLocalIssue(record) {
|
|
3286
5246
|
const { draft } = record;
|
|
@@ -3459,40 +5419,41 @@ function createLocalTracker(options) {
|
|
|
3459
5419
|
}
|
|
3460
5420
|
|
|
3461
5421
|
// src/adapters/registry.ts
|
|
3462
|
-
var
|
|
5422
|
+
var REFUSAL3 = "adapter registry";
|
|
3463
5423
|
var PORT_VERSIONS = Object.freeze({
|
|
3464
5424
|
tracker: 1,
|
|
3465
5425
|
store: 1,
|
|
3466
5426
|
learning: 1,
|
|
3467
5427
|
output: 1,
|
|
3468
|
-
planner: 1
|
|
5428
|
+
planner: 1,
|
|
5429
|
+
sync: 1
|
|
3469
5430
|
});
|
|
3470
5431
|
var SERVED_VERSIONS = new Map(Object.entries(PORT_VERSIONS));
|
|
3471
5432
|
var PORT_TYPE_LIST = [...SERVED_VERSIONS.keys()].join(", ");
|
|
3472
5433
|
function checkPortType(port) {
|
|
3473
5434
|
if (!SERVED_VERSIONS.has(port)) {
|
|
3474
|
-
throw new TypeError(`${
|
|
5435
|
+
throw new TypeError(`${REFUSAL3}: port is ${describeValue(port)}, expected one of: ${PORT_TYPE_LIST}`);
|
|
3475
5436
|
}
|
|
3476
5437
|
}
|
|
3477
5438
|
function accept(adapter, held) {
|
|
3478
5439
|
if (typeof adapter !== "object" || adapter === null) {
|
|
3479
|
-
throw new TypeError(`${
|
|
5440
|
+
throw new TypeError(`${REFUSAL3}: an adapter is ${describeValue(adapter)}, expected a mapping`);
|
|
3480
5441
|
}
|
|
3481
5442
|
const { port, kind, portVersion, create } = adapter;
|
|
3482
5443
|
checkPortType(port);
|
|
3483
5444
|
if (typeof kind !== "string" || kind === "") {
|
|
3484
|
-
throw new TypeError(`${
|
|
5445
|
+
throw new TypeError(`${REFUSAL3}: a ${port} adapter has kind ${describeValue(kind)}, expected a non-empty name`);
|
|
3485
5446
|
}
|
|
3486
5447
|
const key = `${port}/${kind}`;
|
|
3487
5448
|
if (typeof create !== "function") {
|
|
3488
|
-
throw new TypeError(`${
|
|
5449
|
+
throw new TypeError(`${REFUSAL3}: ${key} has create ${describeValue(create)}, expected a function`);
|
|
3489
5450
|
}
|
|
3490
5451
|
const served = SERVED_VERSIONS.get(port);
|
|
3491
5452
|
if (portVersion !== served) {
|
|
3492
|
-
throw new Error(`${
|
|
5453
|
+
throw new Error(`${REFUSAL3}: ${key} implements ${port} port version ${describeValue(portVersion)},` + ` and core serves ${port} port version ${served}`);
|
|
3493
5454
|
}
|
|
3494
5455
|
if (held.some((other) => other.port === port && other.kind === kind)) {
|
|
3495
|
-
throw new Error(`${
|
|
5456
|
+
throw new Error(`${REFUSAL3}: ${key} is already registered`);
|
|
3496
5457
|
}
|
|
3497
5458
|
return Object.freeze({ port, kind, portVersion, create });
|
|
3498
5459
|
}
|
|
@@ -3503,7 +5464,8 @@ function indexByPort(adapters) {
|
|
|
3503
5464
|
store: only("store"),
|
|
3504
5465
|
learning: only("learning"),
|
|
3505
5466
|
output: only("output"),
|
|
3506
|
-
planner: only("planner")
|
|
5467
|
+
planner: only("planner"),
|
|
5468
|
+
sync: only("sync")
|
|
3507
5469
|
};
|
|
3508
5470
|
}
|
|
3509
5471
|
function createAdapterRegistry(adapters = []) {
|
|
@@ -3518,8 +5480,8 @@ function createAdapterRegistry(adapters = []) {
|
|
|
3518
5480
|
if (adapter !== undefined)
|
|
3519
5481
|
return adapter;
|
|
3520
5482
|
const registered = [...byPort[port].keys()];
|
|
3521
|
-
const
|
|
3522
|
-
throw new Error(`${
|
|
5483
|
+
const listed2 = registered.length === 0 ? "none" : registered.join(", ");
|
|
5484
|
+
throw new Error(`${REFUSAL3}: no ${port} adapter is registered as ${describeValue(kind)};` + ` registered: ${listed2}`);
|
|
3523
5485
|
},
|
|
3524
5486
|
kinds: (port) => {
|
|
3525
5487
|
checkPortType(port);
|
|
@@ -3586,16 +5548,33 @@ var CORE_ADAPTERS = [
|
|
|
3586
5548
|
portVersion: PORT_VERSIONS.planner,
|
|
3587
5549
|
create: ({ repoRoot, settingSources, planPrompt, planDir, claude }) => {
|
|
3588
5550
|
if (!Array.isArray(settingSources)) {
|
|
3589
|
-
throw new TypeError(`${
|
|
5551
|
+
throw new TypeError(`${REFUSAL3}: planner/claude has settingSources ${describeValue(settingSources)} in its context,` + " expected a list of setting sources");
|
|
3590
5552
|
}
|
|
3591
5553
|
if (typeof planPrompt !== "function") {
|
|
3592
|
-
throw new TypeError(`${
|
|
5554
|
+
throw new TypeError(`${REFUSAL3}: planner/claude has planPrompt ${describeValue(planPrompt)} in its context,` + " expected a function");
|
|
3593
5555
|
}
|
|
3594
5556
|
if (typeof planDir !== "string") {
|
|
3595
|
-
throw new TypeError(`${
|
|
5557
|
+
throw new TypeError(`${REFUSAL3}: planner/claude has planDir ${describeValue(planDir)} in its context,` + " expected a directory path");
|
|
3596
5558
|
}
|
|
3597
5559
|
return createClaudePlanner({ repoRoot, planDir, settingSources, buildPrompt: planPrompt, spawn: claude });
|
|
3598
5560
|
}
|
|
5561
|
+
},
|
|
5562
|
+
{
|
|
5563
|
+
port: "sync",
|
|
5564
|
+
kind: "local",
|
|
5565
|
+
portVersion: PORT_VERSIONS.sync,
|
|
5566
|
+
create: () => createLocalSync()
|
|
5567
|
+
},
|
|
5568
|
+
{
|
|
5569
|
+
port: "sync",
|
|
5570
|
+
kind: "file",
|
|
5571
|
+
portVersion: PORT_VERSIONS.sync,
|
|
5572
|
+
create: ({ repoRoot, store }) => {
|
|
5573
|
+
if (!STORE_BACKENDS.includes(store)) {
|
|
5574
|
+
throw new TypeError(`${REFUSAL3}: sync/file has store ${describeValue(store)} in its context,` + ` expected one of: ${STORE_BACKENDS.join(", ")}`);
|
|
5575
|
+
}
|
|
5576
|
+
return createFileSync({ repoRoot, backend: store });
|
|
5577
|
+
}
|
|
3599
5578
|
}
|
|
3600
5579
|
];
|
|
3601
5580
|
var CORE_ADAPTER_REGISTRY = createAdapterRegistry(CORE_ADAPTERS);
|
|
@@ -3617,4 +5596,4 @@ function selectEffortStore(repoRoot, config) {
|
|
|
3617
5596
|
return adapter.create({ repoRoot });
|
|
3618
5597
|
}
|
|
3619
5598
|
|
|
3620
|
-
export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, SessionRecordError, SessionConflictError, SessionStateError, isSessionId, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, beginSession, updateSession,
|
|
5599
|
+
export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, createGitRunner, gitSaid, NOT_A_REPOSITORY, mainCheckoutOf, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, positionAt, rehome, hop, goHome, HOP_FILE, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, SessionConflictError, SessionStateError, isSessionId, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, beginSession, updateSession, DEVELOPMENT_NEXT_STEP, DEVELOPMENT_MARK, DevelopmentBuildRefusedError, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, SchemaRefusedError, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, activeStoreSettings, normalizeRemote, gitRemoteUrl, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, RebuildRefusal, readConfigFile, loadConfig, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, stubOfPlanFile, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, fileStamp, fix_schema_default, EffortCopyRefusal, EffortCopyFailure, copyEffortStore, minutesBetween, readCommitLog, UnionSchemaMismatch, migrateStore, MOVE_TO_SQLITE, MergeRefusal, mergeStore, createFileSync, SyncModuleMissing, selectSync, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, sessionSpawnEnv, getClaudeUsagePercent, checkUsage, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, spawnClaude, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };
|