@open-tomato/rafa 0.26.0 → 0.28.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 +35 -1
- package/dist/cli.js +4235 -1503
- package/dist/effort/store/index.js +1 -1
- package/dist/{index-azptzvp3.js → index-4rwvr6t4.js} +593 -92
- package/dist/{index-59x3zsh4.js → index-6jkj0dcx.js} +1570 -284
- package/dist/{index-2yd8bber.js → index-em1vd7kd.js} +1424 -596
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -238,13 +238,13 @@ function openNdjsonStore(repoRoot) {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
// src/effort/store/sqlite.ts
|
|
241
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
242
|
-
import { dirname as
|
|
241
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
242
|
+
import { dirname as dirname9, join as join9 } from "path";
|
|
243
243
|
import { Database } from "bun:sqlite";
|
|
244
244
|
|
|
245
245
|
// package.json
|
|
246
246
|
var name = "@open-tomato/rafa";
|
|
247
|
-
var version = "0.
|
|
247
|
+
var version = "0.28.0";
|
|
248
248
|
|
|
249
249
|
// src/cli/version.ts
|
|
250
250
|
var RAFA_VERSION = version;
|
|
@@ -254,19 +254,29 @@ function versionLine() {
|
|
|
254
254
|
|
|
255
255
|
// src/effort/store/development-build.ts
|
|
256
256
|
import { tmpdir as tmpdir2 } from "os";
|
|
257
|
-
import { basename as basename2, dirname as
|
|
257
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute3 } from "path";
|
|
258
258
|
|
|
259
259
|
// src/loop/sessions.ts
|
|
260
260
|
import {
|
|
261
261
|
linkSync,
|
|
262
|
-
mkdirSync as
|
|
262
|
+
mkdirSync as mkdirSync4,
|
|
263
263
|
readdirSync,
|
|
264
|
-
readFileSync as
|
|
265
|
-
renameSync,
|
|
266
|
-
rmSync,
|
|
267
|
-
writeFileSync
|
|
264
|
+
readFileSync as readFileSync4,
|
|
265
|
+
renameSync as renameSync3,
|
|
266
|
+
rmSync as rmSync3,
|
|
267
|
+
writeFileSync as writeFileSync3
|
|
268
268
|
} from "fs";
|
|
269
|
-
import { basename, dirname as
|
|
269
|
+
import { basename, dirname as dirname5, join as join7 } from "path";
|
|
270
|
+
|
|
271
|
+
// src/next/hop-record.ts
|
|
272
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
273
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
274
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
275
|
+
|
|
276
|
+
// src/project/position.ts
|
|
277
|
+
import { randomUUID } from "crypto";
|
|
278
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
|
|
279
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
270
280
|
|
|
271
281
|
// src/project/scope.ts
|
|
272
282
|
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
|
|
@@ -344,6 +354,185 @@ function resolveScope(start, seams) {
|
|
|
344
354
|
return { found: true, root: walk.root, home, project: scopeAt(walk.root), user };
|
|
345
355
|
}
|
|
346
356
|
|
|
357
|
+
// src/project/position.ts
|
|
358
|
+
var POSITION_FILE = "position.json";
|
|
359
|
+
function positionFilePath(root) {
|
|
360
|
+
return join5(scopeAt(root).dir, POSITION_FILE);
|
|
361
|
+
}
|
|
362
|
+
function isRecord(value) {
|
|
363
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
364
|
+
}
|
|
365
|
+
function isIssueNumber(value) {
|
|
366
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
367
|
+
}
|
|
368
|
+
function asPlace(value) {
|
|
369
|
+
if (!isRecord(value) || !isIssueNumber(value.board))
|
|
370
|
+
return null;
|
|
371
|
+
if (value.epic !== null && !isIssueNumber(value.epic))
|
|
372
|
+
return null;
|
|
373
|
+
return { board: value.board, epic: value.epic };
|
|
374
|
+
}
|
|
375
|
+
function asPosition(value) {
|
|
376
|
+
if (!isRecord(value))
|
|
377
|
+
return null;
|
|
378
|
+
const current = asPlace(value.current);
|
|
379
|
+
const home = asPlace(value.home);
|
|
380
|
+
const previous = value.previous === null ? null : asPlace(value.previous);
|
|
381
|
+
if (current === null || home === null)
|
|
382
|
+
return null;
|
|
383
|
+
if (previous === null && value.previous !== null)
|
|
384
|
+
return null;
|
|
385
|
+
return { current, previous, home };
|
|
386
|
+
}
|
|
387
|
+
function readPositionFile(root) {
|
|
388
|
+
const file = positionFilePath(root);
|
|
389
|
+
let text;
|
|
390
|
+
try {
|
|
391
|
+
text = readFileSync2(file, "utf8");
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const code = error.code;
|
|
394
|
+
return code === "ENOENT" ? { set: false, reason: "absent", detail: `No position file at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
|
|
395
|
+
}
|
|
396
|
+
let parsed;
|
|
397
|
+
try {
|
|
398
|
+
parsed = JSON.parse(text);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return { set: false, reason: "invalid-json", detail: `${file} holds no JSON: ${messageOf(error)}` };
|
|
401
|
+
}
|
|
402
|
+
const position = asPosition(parsed);
|
|
403
|
+
return position === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a current, previous and home place` } : { set: true, position };
|
|
404
|
+
}
|
|
405
|
+
function writePositionFile(root, position) {
|
|
406
|
+
const file = positionFilePath(root);
|
|
407
|
+
mkdirSync2(dirname3(file), { recursive: true });
|
|
408
|
+
const temporary = `${file}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
409
|
+
const { current, previous, home } = position;
|
|
410
|
+
try {
|
|
411
|
+
writeFileSync(temporary, `${JSON.stringify({ current, previous, home }, null, 2)}
|
|
412
|
+
`);
|
|
413
|
+
renameSync(temporary, file);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
rmSync(temporary, { force: true });
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function positionAt(place) {
|
|
420
|
+
return { current: place, previous: null, home: place };
|
|
421
|
+
}
|
|
422
|
+
function rehome(position, place) {
|
|
423
|
+
return { current: place, previous: position.current, home: place };
|
|
424
|
+
}
|
|
425
|
+
function hop(position, place) {
|
|
426
|
+
return { current: place, previous: position.current, home: position.home };
|
|
427
|
+
}
|
|
428
|
+
function goHome(position) {
|
|
429
|
+
return { current: position.home, previous: position.current, home: position.home };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/next/hop-record.ts
|
|
433
|
+
var HOP_FILE = "hop.json";
|
|
434
|
+
var HOP_KINDS = ["blocker", "dry"];
|
|
435
|
+
var HOP_STATES = ["away", "waiting", "merged", "halted"];
|
|
436
|
+
function hopFilePath(root) {
|
|
437
|
+
return join6(scopeAt(root).dir, HOP_FILE);
|
|
438
|
+
}
|
|
439
|
+
function isRecord2(value) {
|
|
440
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
441
|
+
}
|
|
442
|
+
function isIssueNumber2(value) {
|
|
443
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
444
|
+
}
|
|
445
|
+
function isIssueOrNull(value) {
|
|
446
|
+
return value === null || isIssueNumber2(value);
|
|
447
|
+
}
|
|
448
|
+
function isTimestamp(value) {
|
|
449
|
+
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
450
|
+
}
|
|
451
|
+
function issuesMatchKind(kind, blocked, target) {
|
|
452
|
+
return kind === "blocker" ? blocked !== null && target !== null : blocked === null && target === null;
|
|
453
|
+
}
|
|
454
|
+
function asHopRecord(value) {
|
|
455
|
+
if (!isRecord2(value))
|
|
456
|
+
return null;
|
|
457
|
+
const { kind, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = value;
|
|
458
|
+
const home = asPlace(value.home);
|
|
459
|
+
const from = asPlace(value.from);
|
|
460
|
+
if (typeof kind !== "string" || !HOP_KINDS.includes(kind))
|
|
461
|
+
return null;
|
|
462
|
+
if (typeof state !== "string" || !HOP_STATES.includes(state))
|
|
463
|
+
return null;
|
|
464
|
+
if (home === null || from === null)
|
|
465
|
+
return null;
|
|
466
|
+
if (!isIssueOrNull(blocked) || !isIssueOrNull(target) || !isIssueOrNull(pullRequest))
|
|
467
|
+
return null;
|
|
468
|
+
if (!isIssueNumber2(targetEpic) || !isIssueNumber2(targetBoard) || !isTimestamp(startedAt))
|
|
469
|
+
return null;
|
|
470
|
+
const hopKind = kind;
|
|
471
|
+
if (!issuesMatchKind(hopKind, blocked, target))
|
|
472
|
+
return null;
|
|
473
|
+
return {
|
|
474
|
+
kind: hopKind,
|
|
475
|
+
home,
|
|
476
|
+
from,
|
|
477
|
+
blocked,
|
|
478
|
+
target,
|
|
479
|
+
targetEpic,
|
|
480
|
+
targetBoard,
|
|
481
|
+
state,
|
|
482
|
+
pullRequest,
|
|
483
|
+
startedAt
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function readHopRecord(root) {
|
|
487
|
+
const file = hopFilePath(root);
|
|
488
|
+
let text;
|
|
489
|
+
try {
|
|
490
|
+
text = readFileSync3(file, "utf8");
|
|
491
|
+
} catch (error) {
|
|
492
|
+
const code = error.code;
|
|
493
|
+
return code === "ENOENT" ? { set: false, reason: "absent", detail: `No hop record at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
|
|
494
|
+
}
|
|
495
|
+
let parsed;
|
|
496
|
+
try {
|
|
497
|
+
parsed = JSON.parse(text);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
return { set: false, reason: "wrong-shape", detail: `${file} holds no JSON: ${messageOf(error)}` };
|
|
500
|
+
}
|
|
501
|
+
const record = asHopRecord(parsed);
|
|
502
|
+
return record === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a hop record` } : { set: true, record };
|
|
503
|
+
}
|
|
504
|
+
function ownKeys(record) {
|
|
505
|
+
const { kind, home, from, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = record;
|
|
506
|
+
return {
|
|
507
|
+
kind,
|
|
508
|
+
home: { board: home.board, epic: home.epic },
|
|
509
|
+
from: { board: from.board, epic: from.epic },
|
|
510
|
+
blocked,
|
|
511
|
+
target,
|
|
512
|
+
targetEpic,
|
|
513
|
+
targetBoard,
|
|
514
|
+
state,
|
|
515
|
+
pullRequest,
|
|
516
|
+
startedAt
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function writeHopRecord(root, record) {
|
|
520
|
+
const file = hopFilePath(root);
|
|
521
|
+
mkdirSync3(dirname4(file), { recursive: true });
|
|
522
|
+
const temporary = `${file}.${String(process.pid)}.${randomUUID2()}.tmp`;
|
|
523
|
+
try {
|
|
524
|
+
writeFileSync2(temporary, `${JSON.stringify(ownKeys(record), null, 2)}
|
|
525
|
+
`);
|
|
526
|
+
renameSync2(temporary, file);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
rmSync2(temporary, { force: true });
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function staleAgainst(record, position) {
|
|
533
|
+
return position.home.board !== record.home.board || position.home.epic !== record.home.epic;
|
|
534
|
+
}
|
|
535
|
+
|
|
347
536
|
// src/loop/sessions.ts
|
|
348
537
|
var SESSION_STATES = Object.freeze(["running", "paused", "stopped", "done"]);
|
|
349
538
|
var RUNS_DIR = "runs";
|
|
@@ -384,13 +573,13 @@ function isSessionId(value) {
|
|
|
384
573
|
return SESSION_ID_PATTERN.test(value);
|
|
385
574
|
}
|
|
386
575
|
function runsDir(root) {
|
|
387
|
-
return
|
|
576
|
+
return join7(scopeAt(root).dir, RUNS_DIR);
|
|
388
577
|
}
|
|
389
578
|
function sessionFilePath(root, sessionId) {
|
|
390
579
|
if (!isSessionId(sessionId)) {
|
|
391
580
|
throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
|
|
392
581
|
}
|
|
393
|
-
return
|
|
582
|
+
return join7(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
|
|
394
583
|
}
|
|
395
584
|
function describeValue2(value) {
|
|
396
585
|
return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
|
|
@@ -417,6 +606,12 @@ function taskProblem(task) {
|
|
|
417
606
|
}
|
|
418
607
|
return isText(field(task, "text")) ? null : `task.text is ${describeValue2(field(task, "text"))}, expected a non-empty string`;
|
|
419
608
|
}
|
|
609
|
+
function hopProblem(fields) {
|
|
610
|
+
if (!Object.hasOwn(fields, "hop"))
|
|
611
|
+
return null;
|
|
612
|
+
const hop2 = field(fields, "hop");
|
|
613
|
+
return asHopRecord(hop2) === null ? `hop is ${describeValue2(hop2)}, expected a hop record` : null;
|
|
614
|
+
}
|
|
420
615
|
function sessionIdProblem(sessionId, file) {
|
|
421
616
|
if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
|
|
422
617
|
return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
|
|
@@ -440,12 +635,18 @@ function recordProblems(fields, file) {
|
|
|
440
635
|
isPositiveWhole(pid) ? null : `pid is ${describeValue2(pid)}, expected a whole number from 1`,
|
|
441
636
|
typeof startedAt === "string" && !Number.isNaN(Date.parse(startedAt)) ? null : `startedAt is ${describeValue2(startedAt)}, expected a timestamp`,
|
|
442
637
|
SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
|
|
443
|
-
taskProblem(field(fields, "task"))
|
|
638
|
+
taskProblem(field(fields, "task")),
|
|
639
|
+
hopProblem(fields)
|
|
444
640
|
];
|
|
445
641
|
return problems.filter((problem) => problem !== null);
|
|
446
642
|
}
|
|
643
|
+
function freezeHop(value) {
|
|
644
|
+
const hop2 = asHopRecord(value);
|
|
645
|
+
return Object.freeze({ ...hop2, home: Object.freeze(hop2.home), from: Object.freeze(hop2.from) });
|
|
646
|
+
}
|
|
447
647
|
function freezeRecord(fields) {
|
|
448
648
|
const task = field(fields, "task");
|
|
649
|
+
const hop2 = field(fields, "hop");
|
|
449
650
|
return Object.freeze({
|
|
450
651
|
sessionId: field(fields, "sessionId"),
|
|
451
652
|
planStub: field(fields, "planStub"),
|
|
@@ -454,7 +655,8 @@ function freezeRecord(fields) {
|
|
|
454
655
|
pid: field(fields, "pid"),
|
|
455
656
|
startedAt: field(fields, "startedAt"),
|
|
456
657
|
state: field(fields, "state"),
|
|
457
|
-
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null
|
|
658
|
+
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
|
|
659
|
+
...hop2 === undefined ? {} : { hop: freezeHop(hop2) }
|
|
458
660
|
});
|
|
459
661
|
}
|
|
460
662
|
function parseSessionRecord(text, file) {
|
|
@@ -492,7 +694,7 @@ function readState(record, isAlive = isPidAlive) {
|
|
|
492
694
|
function readRecordFile(file) {
|
|
493
695
|
let text;
|
|
494
696
|
try {
|
|
495
|
-
text =
|
|
697
|
+
text = readFileSync4(file, "utf8");
|
|
496
698
|
} catch (error) {
|
|
497
699
|
throw new SessionRecordError(file, `cannot be read: ${messageOf(error)}`);
|
|
498
700
|
}
|
|
@@ -513,7 +715,7 @@ function byStart(a, b) {
|
|
|
513
715
|
function readSessions(root, seams = {}) {
|
|
514
716
|
const dir = runsDir(root);
|
|
515
717
|
const isAlive = seams.isAlive ?? isPidAlive;
|
|
516
|
-
const records = recordNames(dir).filter((name2) => name2.endsWith(RECORD_EXTENSION)).map((name2) => readRecordFile(
|
|
718
|
+
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
719
|
return Object.freeze(records.sort(byStart));
|
|
518
720
|
}
|
|
519
721
|
function readSession(root, sessionId, seams = {}) {
|
|
@@ -539,17 +741,17 @@ function checkRecord(record, file) {
|
|
|
539
741
|
}
|
|
540
742
|
function writeRecordFile(file, record, isNew) {
|
|
541
743
|
checkRecord(record, file);
|
|
542
|
-
|
|
744
|
+
mkdirSync4(dirname5(file), { recursive: true });
|
|
543
745
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
544
|
-
|
|
746
|
+
writeFileSync3(temporary, `${JSON.stringify(record, null, 2)}
|
|
545
747
|
`);
|
|
546
748
|
try {
|
|
547
749
|
if (isNew)
|
|
548
750
|
linkSync(temporary, file);
|
|
549
751
|
else
|
|
550
|
-
|
|
752
|
+
renameSync3(temporary, file);
|
|
551
753
|
} finally {
|
|
552
|
-
|
|
754
|
+
rmSync3(temporary, { force: true });
|
|
553
755
|
}
|
|
554
756
|
}
|
|
555
757
|
function beginSession(root, draft, seams = {}) {
|
|
@@ -577,10 +779,10 @@ function updateSession(root, sessionId, change) {
|
|
|
577
779
|
}
|
|
578
780
|
|
|
579
781
|
// src/runtime/identity.ts
|
|
580
|
-
import { existsSync as existsSync4, readFileSync as
|
|
581
|
-
import { dirname as
|
|
782
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3 } from "fs";
|
|
783
|
+
import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
|
|
582
784
|
var MANIFEST = "package.json";
|
|
583
|
-
var CHECKOUT_ENTRY =
|
|
785
|
+
var CHECKOUT_ENTRY = join8("src", "rafa.ts");
|
|
584
786
|
function realOrResolved(path) {
|
|
585
787
|
const resolved = resolve2(path);
|
|
586
788
|
return existsSync4(resolved) ? realpathSync3(resolved) : resolved;
|
|
@@ -588,7 +790,7 @@ function realOrResolved(path) {
|
|
|
588
790
|
function manifestName(file) {
|
|
589
791
|
let parsed;
|
|
590
792
|
try {
|
|
591
|
-
parsed = JSON.parse(
|
|
793
|
+
parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
592
794
|
} catch {
|
|
593
795
|
return null;
|
|
594
796
|
}
|
|
@@ -600,10 +802,10 @@ function manifestName(file) {
|
|
|
600
802
|
function nearestRafaPackage(start) {
|
|
601
803
|
let dir = start;
|
|
602
804
|
for (;; ) {
|
|
603
|
-
const manifest =
|
|
805
|
+
const manifest = join8(dir, MANIFEST);
|
|
604
806
|
if (existsSync4(manifest) && manifestName(manifest) === name)
|
|
605
807
|
return dir;
|
|
606
|
-
const parent =
|
|
808
|
+
const parent = dirname6(dir);
|
|
607
809
|
if (parent === dir)
|
|
608
810
|
return null;
|
|
609
811
|
dir = parent;
|
|
@@ -611,8 +813,8 @@ function nearestRafaPackage(start) {
|
|
|
611
813
|
}
|
|
612
814
|
function readRuntimeIdentity(entry = Bun.main) {
|
|
613
815
|
const real = realOrResolved(entry);
|
|
614
|
-
const packageDir = nearestRafaPackage(
|
|
615
|
-
if (packageDir !== null && existsSync4(
|
|
816
|
+
const packageDir = nearestRafaPackage(dirname6(real));
|
|
817
|
+
if (packageDir !== null && existsSync4(join8(packageDir, CHECKOUT_ENTRY))) {
|
|
616
818
|
return { kind: "development", entry: real, checkout: packageDir };
|
|
617
819
|
}
|
|
618
820
|
return { kind: "installed", entry: real };
|
|
@@ -638,11 +840,11 @@ class DevelopmentBuildRefusedError extends Error {
|
|
|
638
840
|
}
|
|
639
841
|
}
|
|
640
842
|
function storeProjectRoot(path) {
|
|
641
|
-
const storeDir =
|
|
642
|
-
const scopeDir =
|
|
843
|
+
const storeDir = dirname7(path);
|
|
844
|
+
const scopeDir = dirname7(storeDir);
|
|
643
845
|
if (basename2(storeDir) !== EFFORT_DIR_NAME || basename2(scopeDir) !== SCOPE_DIR)
|
|
644
846
|
return null;
|
|
645
|
-
return
|
|
847
|
+
return dirname7(scopeDir);
|
|
646
848
|
}
|
|
647
849
|
function isOwnedStore(path, env, tempDir) {
|
|
648
850
|
if (isUnderTempDir(path, tempDir))
|
|
@@ -926,6 +1128,120 @@ var SQLITE_MIGRATIONS = [
|
|
|
926
1128
|
CHECK ((verdict = 'red') = (json_array_length(failing) > 0))
|
|
927
1129
|
);
|
|
928
1130
|
`
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
id: "row-origins",
|
|
1134
|
+
breaks: [],
|
|
1135
|
+
sql: `
|
|
1136
|
+
ALTER TABLE sessions ADD COLUMN origin_store TEXT;
|
|
1137
|
+
ALTER TABLE sessions ADD COLUMN origin_seq INTEGER;
|
|
1138
|
+
CREATE UNIQUE INDEX sessions_by_origin
|
|
1139
|
+
ON sessions (origin_store, origin_seq)
|
|
1140
|
+
WHERE origin_store IS NOT NULL;
|
|
1141
|
+
|
|
1142
|
+
ALTER TABLE commits ADD COLUMN origin_store TEXT;
|
|
1143
|
+
ALTER TABLE commits ADD COLUMN origin_seq INTEGER;
|
|
1144
|
+
CREATE UNIQUE INDEX commits_by_origin
|
|
1145
|
+
ON commits (origin_store, origin_seq)
|
|
1146
|
+
WHERE origin_store IS NOT NULL;
|
|
1147
|
+
|
|
1148
|
+
ALTER TABLE findings ADD COLUMN origin_store TEXT;
|
|
1149
|
+
ALTER TABLE findings ADD COLUMN origin_seq INTEGER;
|
|
1150
|
+
CREATE UNIQUE INDEX findings_by_origin
|
|
1151
|
+
ON findings (origin_store, origin_seq)
|
|
1152
|
+
WHERE origin_store IS NOT NULL;
|
|
1153
|
+
|
|
1154
|
+
ALTER TABLE blockers ADD COLUMN origin_store TEXT;
|
|
1155
|
+
ALTER TABLE blockers ADD COLUMN origin_seq INTEGER;
|
|
1156
|
+
CREATE UNIQUE INDEX blockers_by_origin
|
|
1157
|
+
ON blockers (origin_store, origin_seq)
|
|
1158
|
+
WHERE origin_store IS NOT NULL;
|
|
1159
|
+
|
|
1160
|
+
ALTER TABLE out_of_scope_bugs ADD COLUMN origin_store TEXT;
|
|
1161
|
+
ALTER TABLE out_of_scope_bugs ADD COLUMN origin_seq INTEGER;
|
|
1162
|
+
CREATE UNIQUE INDEX out_of_scope_bugs_by_origin
|
|
1163
|
+
ON out_of_scope_bugs (origin_store, origin_seq)
|
|
1164
|
+
WHERE origin_store IS NOT NULL;
|
|
1165
|
+
|
|
1166
|
+
ALTER TABLE report_absences ADD COLUMN origin_store TEXT;
|
|
1167
|
+
ALTER TABLE report_absences ADD COLUMN origin_seq INTEGER;
|
|
1168
|
+
CREATE UNIQUE INDEX report_absences_by_origin
|
|
1169
|
+
ON report_absences (origin_store, origin_seq)
|
|
1170
|
+
WHERE origin_store IS NOT NULL;
|
|
1171
|
+
|
|
1172
|
+
ALTER TABLE task_reports ADD COLUMN origin_store TEXT;
|
|
1173
|
+
ALTER TABLE task_reports ADD COLUMN origin_seq INTEGER;
|
|
1174
|
+
CREATE UNIQUE INDEX task_reports_by_origin
|
|
1175
|
+
ON task_reports (origin_store, origin_seq)
|
|
1176
|
+
WHERE origin_store IS NOT NULL;
|
|
1177
|
+
|
|
1178
|
+
ALTER TABLE preflight ADD COLUMN origin_store TEXT;
|
|
1179
|
+
ALTER TABLE preflight ADD COLUMN origin_seq INTEGER;
|
|
1180
|
+
CREATE UNIQUE INDEX preflight_by_origin
|
|
1181
|
+
ON preflight (origin_store, origin_seq)
|
|
1182
|
+
WHERE origin_store IS NOT NULL;
|
|
1183
|
+
|
|
1184
|
+
ALTER TABLE dispatches ADD COLUMN origin_store TEXT;
|
|
1185
|
+
ALTER TABLE dispatches ADD COLUMN origin_seq INTEGER;
|
|
1186
|
+
CREATE UNIQUE INDEX dispatches_by_origin
|
|
1187
|
+
ON dispatches (origin_store, origin_seq)
|
|
1188
|
+
WHERE origin_store IS NOT NULL;
|
|
1189
|
+
|
|
1190
|
+
ALTER TABLE changes ADD COLUMN origin_store TEXT;
|
|
1191
|
+
ALTER TABLE changes ADD COLUMN origin_seq INTEGER;
|
|
1192
|
+
CREATE UNIQUE INDEX changes_by_origin
|
|
1193
|
+
ON changes (origin_store, origin_seq)
|
|
1194
|
+
WHERE origin_store IS NOT NULL;
|
|
1195
|
+
|
|
1196
|
+
ALTER TABLE skill_invocations ADD COLUMN origin_store TEXT;
|
|
1197
|
+
ALTER TABLE skill_invocations ADD COLUMN origin_seq INTEGER;
|
|
1198
|
+
CREATE UNIQUE INDEX skill_invocations_by_origin
|
|
1199
|
+
ON skill_invocations (origin_store, origin_seq)
|
|
1200
|
+
WHERE origin_store IS NOT NULL;
|
|
1201
|
+
|
|
1202
|
+
ALTER TABLE plan_ci ADD COLUMN origin_store TEXT;
|
|
1203
|
+
ALTER TABLE plan_ci ADD COLUMN origin_seq INTEGER;
|
|
1204
|
+
CREATE UNIQUE INDEX plan_ci_by_origin
|
|
1205
|
+
ON plan_ci (origin_store, origin_seq)
|
|
1206
|
+
WHERE origin_store IS NOT NULL;
|
|
1207
|
+
`
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
id: "store-meta",
|
|
1211
|
+
breaks: [],
|
|
1212
|
+
sql: `
|
|
1213
|
+
CREATE TABLE store_meta (
|
|
1214
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1215
|
+
store_id TEXT NOT NULL CHECK (store_id <> ''),
|
|
1216
|
+
project_root_commit TEXT NOT NULL CHECK (project_root_commit <> ''),
|
|
1217
|
+
project_remote TEXT CHECK (project_remote <> ''),
|
|
1218
|
+
host_id TEXT NOT NULL CHECK (host_id <> ''),
|
|
1219
|
+
store_path TEXT NOT NULL CHECK (store_path <> ''),
|
|
1220
|
+
file_dev INTEGER NOT NULL CHECK (typeof(file_dev) = 'integer'),
|
|
1221
|
+
file_ino INTEGER NOT NULL CHECK (typeof(file_ino) = 'integer'),
|
|
1222
|
+
minted_at TEXT NOT NULL CHECK (minted_at <> '')
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
CREATE TABLE merges (
|
|
1226
|
+
seq INTEGER PRIMARY KEY,
|
|
1227
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1228
|
+
other_store TEXT CHECK (other_store <> ''),
|
|
1229
|
+
merged_at TEXT NOT NULL CHECK (merged_at <> ''),
|
|
1230
|
+
rows_added INTEGER NOT NULL CHECK (typeof(rows_added) = 'integer' AND rows_added >= 0),
|
|
1231
|
+
rows_skipped INTEGER NOT NULL CHECK (typeof(rows_skipped) = 'integer' AND rows_skipped >= 0),
|
|
1232
|
+
rows_in_conflict INTEGER NOT NULL CHECK (typeof(rows_in_conflict) = 'integer' AND rows_in_conflict >= 0)
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
CREATE TABLE merge_conflicts (
|
|
1236
|
+
seq INTEGER PRIMARY KEY,
|
|
1237
|
+
merge_id TEXT NOT NULL CHECK (merge_id <> ''),
|
|
1238
|
+
table_name TEXT NOT NULL CHECK (table_name <> ''),
|
|
1239
|
+
local_seq INTEGER NOT NULL CHECK (typeof(local_seq) = 'integer'),
|
|
1240
|
+
field TEXT CHECK (field <> ''),
|
|
1241
|
+
incoming TEXT NOT NULL CHECK (json_valid(incoming) AND json_type(incoming) = 'object'),
|
|
1242
|
+
recorded_at TEXT NOT NULL CHECK (recorded_at <> '')
|
|
1243
|
+
);
|
|
1244
|
+
`
|
|
929
1245
|
}
|
|
930
1246
|
];
|
|
931
1247
|
var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
|
|
@@ -1128,6 +1444,15 @@ function logMigration(db, entry, stamp) {
|
|
|
1128
1444
|
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]);
|
|
1129
1445
|
}
|
|
1130
1446
|
|
|
1447
|
+
// src/effort/store/origins.ts
|
|
1448
|
+
var ACROSS_STORES_ORDER = "collected_at, origin_store, origin_seq, seq";
|
|
1449
|
+
var STAMPED_COLUMNS = "seq, origin_store, origin_seq";
|
|
1450
|
+
var STORE_ID = "(SELECT store_id FROM store_meta WHERE id = 1)";
|
|
1451
|
+
function stampedValues(table) {
|
|
1452
|
+
const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${table})`;
|
|
1453
|
+
return `${nextSeq}, ${STORE_ID}, CASE WHEN ${STORE_ID} IS NULL THEN NULL ELSE ${nextSeq} END`;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1131
1456
|
// src/effort/store/settings.ts
|
|
1132
1457
|
var DEFAULT_STORE_SETTINGS = Object.freeze({
|
|
1133
1458
|
busyTimeoutMs: CONFIG_DEFAULTS.effortBusyTimeoutMs
|
|
@@ -1147,10 +1472,211 @@ function activeStoreSettings() {
|
|
|
1147
1472
|
return active;
|
|
1148
1473
|
}
|
|
1149
1474
|
|
|
1475
|
+
// src/effort/store/store-meta.ts
|
|
1476
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1477
|
+
import { dirname as dirname8 } from "path";
|
|
1478
|
+
|
|
1479
|
+
// src/effort/store/store-identity.ts
|
|
1480
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1481
|
+
import { createHmac } from "crypto";
|
|
1482
|
+
import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync } from "fs";
|
|
1483
|
+
import { hostname, platform as currentPlatform } from "os";
|
|
1484
|
+
|
|
1485
|
+
// src/pr/git.ts
|
|
1486
|
+
import { spawnSync } from "child_process";
|
|
1487
|
+
function createGitRunner(cwd) {
|
|
1488
|
+
return (args) => {
|
|
1489
|
+
const result = spawnSync("git", [...args], {
|
|
1490
|
+
cwd,
|
|
1491
|
+
encoding: "utf8",
|
|
1492
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
1493
|
+
});
|
|
1494
|
+
if (result.error !== undefined) {
|
|
1495
|
+
return { ok: false, stdout: "", stderr: `could not run git in ${cwd}: ${messageOf(result.error)}` };
|
|
1496
|
+
}
|
|
1497
|
+
return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function gitSaid(result) {
|
|
1501
|
+
return [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream !== "").join(`
|
|
1502
|
+
`);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// src/schema/project-id.ts
|
|
1506
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1507
|
+
var PROJECT_ID_LENGTH = 12;
|
|
1508
|
+
var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
|
|
1509
|
+
function hasColonAfterAt(url) {
|
|
1510
|
+
const at = url.indexOf("@");
|
|
1511
|
+
return at !== -1 && url.lastIndexOf(":") > at;
|
|
1512
|
+
}
|
|
1513
|
+
function normalizeRemote(url) {
|
|
1514
|
+
const stripped = url.replace(/:\/\/[^@]+@/, "://");
|
|
1515
|
+
const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
|
|
1516
|
+
const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
1517
|
+
return network ? trimmed.toLowerCase() : trimmed;
|
|
1518
|
+
}
|
|
1519
|
+
function gitRemoteUrl(dir) {
|
|
1520
|
+
const result = spawnSync2("git", ["remote", "get-url", "origin"], {
|
|
1521
|
+
cwd: dir,
|
|
1522
|
+
encoding: "utf8",
|
|
1523
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
1524
|
+
});
|
|
1525
|
+
if (result.error !== undefined || result.status !== 0)
|
|
1526
|
+
return null;
|
|
1527
|
+
const url = result.stdout.trim();
|
|
1528
|
+
return url === "" ? null : url;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// src/effort/store/store-identity.ts
|
|
1532
|
+
var HOST_ID_KEY = "rafa effort store host id";
|
|
1533
|
+
var MACHINE_ID_PATH = "/etc/machine-id";
|
|
1534
|
+
var IOREG_ARGS = ["-rd1", "-c", "IOPlatformExpertDevice"];
|
|
1535
|
+
function hostIdFrom(source, value) {
|
|
1536
|
+
return createHmac("sha256", HOST_ID_KEY).update(`${source}:${value}`).digest("hex");
|
|
1537
|
+
}
|
|
1538
|
+
function readMachineIdFile() {
|
|
1539
|
+
try {
|
|
1540
|
+
const value = readFileSync6(MACHINE_ID_PATH, "utf8").trim();
|
|
1541
|
+
return value === "" ? null : value;
|
|
1542
|
+
} catch {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function parseIoregPlatformUuid(output) {
|
|
1547
|
+
const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(output);
|
|
1548
|
+
return match?.[1] ?? null;
|
|
1549
|
+
}
|
|
1550
|
+
function readIoregPlatformUuid() {
|
|
1551
|
+
const result = spawnSync3("ioreg", [...IOREG_ARGS], { encoding: "utf8" });
|
|
1552
|
+
if (result.error !== undefined || result.status !== 0)
|
|
1553
|
+
return null;
|
|
1554
|
+
return parseIoregPlatformUuid(result.stdout);
|
|
1555
|
+
}
|
|
1556
|
+
function readHostId(seams = {}) {
|
|
1557
|
+
const platform = seams.platform ?? currentPlatform();
|
|
1558
|
+
if (platform === "linux") {
|
|
1559
|
+
const machineId = (seams.readMachineId ?? readMachineIdFile)();
|
|
1560
|
+
if (machineId !== null)
|
|
1561
|
+
return hostIdFrom("machine-id", machineId);
|
|
1562
|
+
}
|
|
1563
|
+
if (platform === "darwin") {
|
|
1564
|
+
const platformUuid = (seams.readPlatformUuid ?? readIoregPlatformUuid)();
|
|
1565
|
+
if (platformUuid !== null)
|
|
1566
|
+
return hostIdFrom("platform-uuid", platformUuid);
|
|
1567
|
+
}
|
|
1568
|
+
return hostIdFrom("hostname", (seams.readHostname ?? hostname)());
|
|
1569
|
+
}
|
|
1570
|
+
function readStoreFileFacts(path) {
|
|
1571
|
+
const storePath = realpathSync4(path);
|
|
1572
|
+
const stats = statSync(storePath, { bigint: true });
|
|
1573
|
+
return { storePath, fileDev: stats.dev, fileIno: stats.ino };
|
|
1574
|
+
}
|
|
1575
|
+
function observeStore(path, readHost = readHostId) {
|
|
1576
|
+
return { hostId: readHost(), ...readStoreFileFacts(path) };
|
|
1577
|
+
}
|
|
1578
|
+
function movedFacts(recorded, facts) {
|
|
1579
|
+
const sameFile = BigInt(recorded.fileDev) === facts.fileDev && BigInt(recorded.fileIno) === facts.fileIno;
|
|
1580
|
+
const moved = [
|
|
1581
|
+
["host", recorded.hostId !== facts.hostId],
|
|
1582
|
+
["path", recorded.storePath !== facts.storePath],
|
|
1583
|
+
["file", !sameFile]
|
|
1584
|
+
];
|
|
1585
|
+
return moved.filter(([, differs]) => differs).map(([reason]) => reason);
|
|
1586
|
+
}
|
|
1587
|
+
function decideStoreIdentity(access, recorded, observe) {
|
|
1588
|
+
if (access === "read")
|
|
1589
|
+
return { action: "none" };
|
|
1590
|
+
const facts = observe();
|
|
1591
|
+
if (recorded === null)
|
|
1592
|
+
return { action: "mint", reasons: ["unminted"], facts };
|
|
1593
|
+
const reasons = movedFacts(recorded, facts);
|
|
1594
|
+
return reasons.length === 0 ? { action: "keep", storeId: recorded.storeId } : { action: "mint", reasons, facts };
|
|
1595
|
+
}
|
|
1596
|
+
function smallestRootCommit(git) {
|
|
1597
|
+
const result = git(["rev-list", "--max-parents=0", "HEAD"]);
|
|
1598
|
+
if (!result.ok)
|
|
1599
|
+
return null;
|
|
1600
|
+
const roots = result.stdout.split(`
|
|
1601
|
+
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
1602
|
+
return [...roots].sort()[0] ?? null;
|
|
1603
|
+
}
|
|
1604
|
+
function normalisedRemote(git) {
|
|
1605
|
+
const result = git(["remote", "get-url", "origin"]);
|
|
1606
|
+
if (!result.ok)
|
|
1607
|
+
return null;
|
|
1608
|
+
const remote = normalizeRemote(result.stdout.trim());
|
|
1609
|
+
return remote === "" ? null : remote;
|
|
1610
|
+
}
|
|
1611
|
+
function readProjectIdentity(dir, git = createGitRunner(dir)) {
|
|
1612
|
+
return { rootCommit: smallestRootCommit(git), remote: normalisedRemote(git) };
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/effort/store/store-meta.ts
|
|
1616
|
+
var STORED_INTEGER_BITS = 64;
|
|
1617
|
+
function toStoredInteger(value) {
|
|
1618
|
+
return BigInt.asIntN(STORED_INTEGER_BITS, value);
|
|
1619
|
+
}
|
|
1620
|
+
function fromStoredInteger(text) {
|
|
1621
|
+
return BigInt.asUintN(STORED_INTEGER_BITS, BigInt(text));
|
|
1622
|
+
}
|
|
1623
|
+
function readStoreMeta(db) {
|
|
1624
|
+
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();
|
|
1625
|
+
if (row === null)
|
|
1626
|
+
return null;
|
|
1627
|
+
return {
|
|
1628
|
+
storeId: row.store_id,
|
|
1629
|
+
projectRootCommit: row.project_root_commit,
|
|
1630
|
+
projectRemote: row.project_remote,
|
|
1631
|
+
hostId: row.host_id,
|
|
1632
|
+
storePath: row.store_path,
|
|
1633
|
+
fileDev: fromStoredInteger(row.file_dev),
|
|
1634
|
+
fileIno: fromStoredInteger(row.file_ino),
|
|
1635
|
+
mintedAt: row.minted_at
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function projectFor(storePath, recorded, seams) {
|
|
1639
|
+
const read = (seams.readProject ?? readProjectIdentity)(dirname8(storePath));
|
|
1640
|
+
if (read.rootCommit !== null)
|
|
1641
|
+
return { rootCommit: read.rootCommit, remote: read.remote };
|
|
1642
|
+
if (recorded === null)
|
|
1643
|
+
return null;
|
|
1644
|
+
return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
|
|
1645
|
+
}
|
|
1646
|
+
function writeStoreMeta(db, facts, project, seams) {
|
|
1647
|
+
const storeId = (seams.newStoreId ?? randomUUID3)();
|
|
1648
|
+
const mintedAt = (seams.now?.() ?? new Date).toISOString();
|
|
1649
|
+
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);
|
|
1650
|
+
return storeId;
|
|
1651
|
+
}
|
|
1652
|
+
function unminted(decision) {
|
|
1653
|
+
return decision.action === "keep" ? { action: "keep", storeId: decision.storeId } : { action: "none" };
|
|
1654
|
+
}
|
|
1655
|
+
function settleStoreIdentity(db, path, access, seams = {}) {
|
|
1656
|
+
if (access === "read")
|
|
1657
|
+
return { action: "none" };
|
|
1658
|
+
const observe = () => observeStore(path, seams.readHostId ?? readHostId);
|
|
1659
|
+
const recorded = readStoreMeta(db);
|
|
1660
|
+
const first = decideStoreIdentity(access, recorded, observe);
|
|
1661
|
+
if (first.action !== "mint")
|
|
1662
|
+
return unminted(first);
|
|
1663
|
+
const project = projectFor(first.facts.storePath, recorded, seams);
|
|
1664
|
+
if (project === null)
|
|
1665
|
+
return { action: "no-project", reasons: first.reasons };
|
|
1666
|
+
const mintUnderLock = db.transaction(() => {
|
|
1667
|
+
const second = decideStoreIdentity(access, readStoreMeta(db), observe);
|
|
1668
|
+
if (second.action !== "mint")
|
|
1669
|
+
return unminted(second);
|
|
1670
|
+
const storeId = writeStoreMeta(db, second.facts, project, seams);
|
|
1671
|
+
return { action: "mint", storeId, reasons: second.reasons };
|
|
1672
|
+
});
|
|
1673
|
+
return mintUnderLock.immediate();
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1150
1676
|
// src/effort/store/sqlite.ts
|
|
1151
1677
|
var SQLITE_STORE_FILE_NAME = "effort.sqlite";
|
|
1152
1678
|
function sqliteStorePath(repoRoot) {
|
|
1153
|
-
return
|
|
1679
|
+
return join9(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
|
|
1154
1680
|
}
|
|
1155
1681
|
var KIND_TABLES = {
|
|
1156
1682
|
sessions: { name: "sessions", keyColumn: "session_id" },
|
|
@@ -1178,21 +1704,22 @@ function batchEntries(kind, rows) {
|
|
|
1178
1704
|
}
|
|
1179
1705
|
return keyed.map(({ key, row }) => ({ key, body: JSON.stringify(row) }));
|
|
1180
1706
|
}
|
|
1181
|
-
function withSqliteStore(path, access, create, use) {
|
|
1707
|
+
function withSqliteStore(path, access, create, use, identity = {}) {
|
|
1182
1708
|
guardTestProcess(path);
|
|
1183
1709
|
if (create)
|
|
1184
|
-
|
|
1710
|
+
mkdirSync5(dirname9(path), { recursive: true });
|
|
1185
1711
|
const db = new Database(path, { readwrite: true, create });
|
|
1186
1712
|
try {
|
|
1187
1713
|
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
1188
1714
|
bringForward(db, path, access, "open");
|
|
1715
|
+
settleStoreIdentity(db, path, access, identity);
|
|
1189
1716
|
return use(db);
|
|
1190
1717
|
} finally {
|
|
1191
1718
|
db.close();
|
|
1192
1719
|
}
|
|
1193
1720
|
}
|
|
1194
1721
|
function insertBatch(db, table, entries) {
|
|
1195
|
-
const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json) VALUES (?,
|
|
1722
|
+
const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json, ${STAMPED_COLUMNS})` + ` VALUES (?, ?, ${stampedValues(table.name)})` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
|
|
1196
1723
|
const insertAll = db.transaction(() => entries.reduce((appended, { key, body }) => appended + insert.run(key, body).changes, 0));
|
|
1197
1724
|
return insertAll.immediate();
|
|
1198
1725
|
}
|
|
@@ -1253,15 +1780,15 @@ function openSqliteStore(repoRoot) {
|
|
|
1253
1780
|
|
|
1254
1781
|
// src/adapters/learning/local.ts
|
|
1255
1782
|
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
1256
|
-
import { join as
|
|
1783
|
+
import { join as join12 } from "path";
|
|
1257
1784
|
|
|
1258
1785
|
// src/commands/instinct/instinct-records.ts
|
|
1259
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
1260
|
-
import { join as
|
|
1786
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
|
|
1787
|
+
import { join as join11 } from "path";
|
|
1261
1788
|
|
|
1262
1789
|
// src/check/layout.ts
|
|
1263
|
-
import { readdirSync as readdirSync2, statSync } from "fs";
|
|
1264
|
-
import { join as
|
|
1790
|
+
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1791
|
+
import { join as join10, relative as relative2 } from "path";
|
|
1265
1792
|
var SKILL_FILE = "SKILL.md";
|
|
1266
1793
|
var MARKDOWN_EXTENSION = ".md";
|
|
1267
1794
|
var LAYOUT_SEVERITY = Object.freeze({
|
|
@@ -1279,7 +1806,7 @@ function layoutIssue(code, message) {
|
|
|
1279
1806
|
}
|
|
1280
1807
|
function entryKind(path) {
|
|
1281
1808
|
try {
|
|
1282
|
-
const stats =
|
|
1809
|
+
const stats = statSync2(path);
|
|
1283
1810
|
if (stats.isFile())
|
|
1284
1811
|
return "file";
|
|
1285
1812
|
if (stats.isDirectory())
|
|
@@ -1378,7 +1905,7 @@ function scanGroupMember(root, dir, name2) {
|
|
|
1378
1905
|
const children = visibleNames(dir);
|
|
1379
1906
|
if (children.length === 0)
|
|
1380
1907
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
1381
|
-
const skillPath =
|
|
1908
|
+
const skillPath = join10(dir, SKILL_FILE);
|
|
1382
1909
|
if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
|
|
1383
1910
|
return [nestedGroupEntry(root, skillPath, name2)];
|
|
1384
1911
|
}
|
|
@@ -1387,7 +1914,7 @@ function scanGroupMember(root, dir, name2) {
|
|
|
1387
1914
|
function scanSkillGroup(root, dir, children) {
|
|
1388
1915
|
const entries = [];
|
|
1389
1916
|
for (const child of children) {
|
|
1390
|
-
const path =
|
|
1917
|
+
const path = join10(dir, child);
|
|
1391
1918
|
const found = entryKind(path);
|
|
1392
1919
|
if (found === null) {
|
|
1393
1920
|
entries.push(unreadableEntry("skill", root, path));
|
|
@@ -1408,7 +1935,7 @@ function scanSkillDirectory(root, dir, name2) {
|
|
|
1408
1935
|
const children = visibleNames(dir);
|
|
1409
1936
|
if (children.length === 0)
|
|
1410
1937
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
1411
|
-
const skillPath =
|
|
1938
|
+
const skillPath = join10(dir, SKILL_FILE);
|
|
1412
1939
|
if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
|
|
1413
1940
|
return [skillFileEntry(skillPath, name2)];
|
|
1414
1941
|
}
|
|
@@ -1417,7 +1944,7 @@ function scanSkillDirectory(root, dir, name2) {
|
|
|
1417
1944
|
function scanSkillRoot(root) {
|
|
1418
1945
|
const entries = [];
|
|
1419
1946
|
for (const name2 of visibleNames(root)) {
|
|
1420
|
-
const path =
|
|
1947
|
+
const path = join10(root, name2);
|
|
1421
1948
|
const found = entryKind(path);
|
|
1422
1949
|
if (found === null) {
|
|
1423
1950
|
entries.push(unreadableEntry("skill", root, path));
|
|
@@ -1438,7 +1965,7 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
1438
1965
|
return [dotfileOnlyEntry("instinct", root, dir)];
|
|
1439
1966
|
const entries = [];
|
|
1440
1967
|
for (const child of children) {
|
|
1441
|
-
const path =
|
|
1968
|
+
const path = join10(dir, child);
|
|
1442
1969
|
const found = entryKind(path);
|
|
1443
1970
|
if (found === null) {
|
|
1444
1971
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -1456,7 +1983,7 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
1456
1983
|
function scanInstinctRoot(root) {
|
|
1457
1984
|
const entries = [];
|
|
1458
1985
|
for (const name2 of visibleNames(root)) {
|
|
1459
|
-
const path =
|
|
1986
|
+
const path = join10(root, name2);
|
|
1460
1987
|
const found = entryKind(path);
|
|
1461
1988
|
if (found === null) {
|
|
1462
1989
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -1515,7 +2042,7 @@ function scanLine(text, offset) {
|
|
|
1515
2042
|
next: index + 1
|
|
1516
2043
|
};
|
|
1517
2044
|
}
|
|
1518
|
-
function
|
|
2045
|
+
function isRecord3(value) {
|
|
1519
2046
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1520
2047
|
}
|
|
1521
2048
|
function parseBlock(yaml) {
|
|
@@ -1526,7 +2053,7 @@ function parseBlock(yaml) {
|
|
|
1526
2053
|
} catch {
|
|
1527
2054
|
return null;
|
|
1528
2055
|
}
|
|
1529
|
-
return
|
|
2056
|
+
return isRecord3(parsed) ? parsed : null;
|
|
1530
2057
|
}
|
|
1531
2058
|
function readFrontmatterDocument(text) {
|
|
1532
2059
|
const opening = scanLine(text, 0);
|
|
@@ -1583,32 +2110,6 @@ function writeFrontmatter(document, data) {
|
|
|
1583
2110
|
].join("");
|
|
1584
2111
|
}
|
|
1585
2112
|
|
|
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
2113
|
// src/schema/instinct.ts
|
|
1613
2114
|
var INSTINCT_KINDS = FINDING_KINDS;
|
|
1614
2115
|
var INSTINCT_SIGNALS = FINDING_SIGNALS;
|
|
@@ -1935,7 +2436,7 @@ function unreadableIssue(message) {
|
|
|
1935
2436
|
function readRecord(path, scope, id) {
|
|
1936
2437
|
let text;
|
|
1937
2438
|
try {
|
|
1938
|
-
text =
|
|
2439
|
+
text = readFileSync7(path, "utf8");
|
|
1939
2440
|
} catch (error) {
|
|
1940
2441
|
const message = error instanceof Error ? error.message : String(error);
|
|
1941
2442
|
return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
|
|
@@ -1945,13 +2446,13 @@ function readRecord(path, scope, id) {
|
|
|
1945
2446
|
}
|
|
1946
2447
|
function isFile(path) {
|
|
1947
2448
|
try {
|
|
1948
|
-
return
|
|
2449
|
+
return statSync3(path).isFile();
|
|
1949
2450
|
} catch {
|
|
1950
2451
|
return false;
|
|
1951
2452
|
}
|
|
1952
2453
|
}
|
|
1953
2454
|
function recordFiles(dir) {
|
|
1954
|
-
return readdirSync3(dir).filter((name2) => isRecordFile(name2) && isFile(
|
|
2455
|
+
return readdirSync3(dir).filter((name2) => isRecordFile(name2) && isFile(join11(dir, name2))).sort((left, right) => left.localeCompare(right));
|
|
1955
2456
|
}
|
|
1956
2457
|
function readScope(scope, dir) {
|
|
1957
2458
|
if (!tierExists(dir))
|
|
@@ -1960,7 +2461,7 @@ function readScope(scope, dir) {
|
|
|
1960
2461
|
scope,
|
|
1961
2462
|
dir,
|
|
1962
2463
|
exists: true,
|
|
1963
|
-
records: recordFiles(dir).map((name2) => readRecord(
|
|
2464
|
+
records: recordFiles(dir).map((name2) => readRecord(join11(dir, name2), scope, recordId(name2)))
|
|
1964
2465
|
};
|
|
1965
2466
|
}
|
|
1966
2467
|
function readScopes(seams) {
|
|
@@ -2263,7 +2764,7 @@ function heldChanges(dir, held, result, described) {
|
|
|
2263
2764
|
if (issue2 !== undefined) {
|
|
2264
2765
|
throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue2.field}: ${issue2.message}`);
|
|
2265
2766
|
}
|
|
2266
|
-
const path =
|
|
2767
|
+
const path = join12(dir, file);
|
|
2267
2768
|
if (held.unreadable.has(path)) {
|
|
2268
2769
|
throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
|
|
2269
2770
|
}
|
|
@@ -2293,18 +2794,18 @@ function pushLogLines(sourceId, described, result) {
|
|
|
2293
2794
|
});
|
|
2294
2795
|
}
|
|
2295
2796
|
function localInstinctsDir(repoRoot) {
|
|
2296
|
-
return
|
|
2797
|
+
return join12(repoRoot, RAFA_INSTINCTS_PATH);
|
|
2297
2798
|
}
|
|
2298
2799
|
function userInstinctsDir(home) {
|
|
2299
|
-
return
|
|
2800
|
+
return join12(home, RAFA_INSTINCTS_PATH);
|
|
2300
2801
|
}
|
|
2301
2802
|
function createLocalLearning(options) {
|
|
2302
2803
|
const { instinctsDir, minConfidence } = options;
|
|
2303
2804
|
const userDir = userInstinctsDir(options.home);
|
|
2304
2805
|
const now = options.now ?? (() => new Date().toISOString());
|
|
2305
2806
|
const warn = options.warn ?? warnThroughActiveOutput;
|
|
2306
|
-
const instinctsPath =
|
|
2307
|
-
const flagsPath =
|
|
2807
|
+
const instinctsPath = join12(instinctsDir, INSTINCTS_FILE);
|
|
2808
|
+
const flagsPath = join12(instinctsDir, FLAGS_FILE);
|
|
2308
2809
|
function readHeld(scope, dir) {
|
|
2309
2810
|
const { records } = readScope(scope, dir);
|
|
2310
2811
|
const unreadable = new Set;
|
|
@@ -2919,7 +3420,7 @@ function notOnPath(command, cwd, path) {
|
|
|
2919
3420
|
function isOneOf(members, value) {
|
|
2920
3421
|
return typeof value === "string" && members.includes(value);
|
|
2921
3422
|
}
|
|
2922
|
-
function
|
|
3423
|
+
function isIssueNumber3(value) {
|
|
2923
3424
|
return typeof value === "string" && ISSUE_NUMBER.test(value) && Number.isSafeInteger(Number(value));
|
|
2924
3425
|
}
|
|
2925
3426
|
function isRepoName(value) {
|
|
@@ -2974,7 +3475,7 @@ function issueNumberOfRef(ref) {
|
|
|
2974
3475
|
if (ref.kind !== "github") {
|
|
2975
3476
|
throw new Error(`${PREFIX2}: refused a ref of kind ${describeValue(ref.kind)}; this tracker reads github refs only`);
|
|
2976
3477
|
}
|
|
2977
|
-
if (!
|
|
3478
|
+
if (!isIssueNumber3(ref.externalId)) {
|
|
2978
3479
|
throw new Error(`${PREFIX2}: externalId ${describeValue(ref.externalId)} is not a GitHub issue number,` + " expected a positive whole number with no leading zero");
|
|
2979
3480
|
}
|
|
2980
3481
|
if (ref.repo !== undefined && !isRepoName(ref.repo)) {
|
|
@@ -3036,7 +3537,7 @@ function listedIssues(stdout) {
|
|
|
3036
3537
|
throw refuse2(`${describeValue(row)}, expected a mapping`);
|
|
3037
3538
|
const { number, url } = row;
|
|
3038
3539
|
const labels = labelNames(row["labels"]);
|
|
3039
|
-
if (typeof number !== "number" || !
|
|
3540
|
+
if (typeof number !== "number" || !isIssueNumber3(String(number))) {
|
|
3040
3541
|
throw refuse2(`with number ${describeValue(number)}, expected an issue number`);
|
|
3041
3542
|
}
|
|
3042
3543
|
if (typeof url !== "string")
|
|
@@ -3071,7 +3572,7 @@ function issueOf(ref, viewed) {
|
|
|
3071
3572
|
module: moduleOfLabels(viewed.labels),
|
|
3072
3573
|
priority: isOneOf(ISSUE_PRIORITIES, priority) ? priority : null,
|
|
3073
3574
|
project: null,
|
|
3074
|
-
blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(
|
|
3575
|
+
blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(isIssueNumber3).map(Number),
|
|
3075
3576
|
ref: { ...ref, url: viewed.url },
|
|
3076
3577
|
state: stateOf(viewed)
|
|
3077
3578
|
};
|
|
@@ -3209,7 +3710,7 @@ function createGithubTracker(options) {
|
|
|
3209
3710
|
|
|
3210
3711
|
// src/adapters/tracker/local.ts
|
|
3211
3712
|
import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
3212
|
-
import { join as
|
|
3713
|
+
import { join as join13 } from "path";
|
|
3213
3714
|
var PREFIX3 = "local tracker";
|
|
3214
3715
|
var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
3215
3716
|
var ISSUE_NUMBER2 = /^[1-9]\d*$/;
|
|
@@ -3266,7 +3767,7 @@ function issueNumberOfFile(name2) {
|
|
|
3266
3767
|
return name2.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name2.slice(0, -ISSUE_EXTENSION.length)) : null;
|
|
3267
3768
|
}
|
|
3268
3769
|
function issuePath(issuesDir, number) {
|
|
3269
|
-
return
|
|
3770
|
+
return join13(issuesDir, `${number}${ISSUE_EXTENSION}`);
|
|
3270
3771
|
}
|
|
3271
3772
|
async function issueNumbers(issuesDir) {
|
|
3272
3773
|
let names;
|
|
@@ -3280,7 +3781,7 @@ async function issueNumbers(issuesDir) {
|
|
|
3280
3781
|
return names.map(issueNumberOfFile).filter((number) => number !== null).sort((a, b) => a - b);
|
|
3281
3782
|
}
|
|
3282
3783
|
function localIssuesDir(repoRoot) {
|
|
3283
|
-
return
|
|
3784
|
+
return join13(repoRoot, ".rafa", "issues");
|
|
3284
3785
|
}
|
|
3285
3786
|
function renderLocalIssue(record) {
|
|
3286
3787
|
const { draft } = record;
|
|
@@ -3617,4 +4118,4 @@ function selectEffortStore(repoRoot, config) {
|
|
|
3617
4118
|
return adapter.create({ repoRoot });
|
|
3618
4119
|
}
|
|
3619
4120
|
|
|
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, readRuntimeIdentity, DEVELOPMENT_NEXT_STEP, ADOPTION_NAME, DEVELOPMENT_MARK, appliedByName, DevelopmentBuildRefusedError, storeProjectRoot, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, MIGRATION_LOG_TABLE, bringForward, readStoreSchema, writeNames, setActiveStoreSettings, activeStoreSettings, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter,
|
|
4121
|
+
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, 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, readRuntimeIdentity, DEVELOPMENT_NEXT_STEP, ADOPTION_NAME, DEVELOPMENT_MARK, appliedByName, DevelopmentBuildRefusedError, storeProjectRoot, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, MIGRATION_LOG_TABLE, SchemaRefusedError, bringForward, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, setActiveStoreSettings, activeStoreSettings, createGitRunner, gitSaid, normalizeRemote, gitRemoteUrl, readProjectIdentity, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, 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, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };
|