@open-tomato/rafa 0.34.1 → 0.35.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 +18 -18
- package/dist/bundled/agents/doc-updater.md +1 -1
- package/dist/bundled/operators/.claude-plugin/plugin.json +5 -0
- package/dist/bundled/operators/agents/rafa-stretch-engineer.md +36 -15
- package/dist/bundled/operators/skills/rafa-stretch-pit-stop/SKILL.md +2 -2
- package/dist/cli.js +11408 -10529
- package/dist/describe-59899b0p.js +11 -0
- package/dist/effort/store/index.js +33 -36
- package/dist/index-21jcfatr.js +16 -0
- package/dist/{index-vp0xghk2.js → index-2tjmxwvp.js} +519 -114
- package/dist/{index-v8d7c6sw.js → index-41vzahew.js} +69 -33
- package/dist/{index-bebv92ha.js → index-ckqfhynj.js} +824 -547
- package/dist/{index-s8ff86xt.js → index-dvrq9nfb.js} +4646 -3954
- package/dist/{index-d6d0942q.js → index-hgcvjke7.js} +17 -17
- package/dist/{index-nr4hdb35.js → index-se13pvag.js} +2471 -2303
- package/dist/index.js +147 -154
- package/dist/learning/index.js +20 -20
- package/dist/plan/index.js +42 -45
- package/package.json +1 -1
- package/dist/describe-5hqfwbjc.js +0 -14
- package/dist/index-3mzw3kzh.js +0 -129
- package/dist/index-fzhd3d4y.js +0 -446
- package/dist/index-k6sd4ym5.js +0 -243
- package/dist/index-wp8c28fd.js +0 -18
|
@@ -1,49 +1,43 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
CONFIG_DEFAULTS,
|
|
4
|
-
CONFIG_FILE,
|
|
5
|
-
ConfigError,
|
|
6
|
-
FINDING_DOMAINS,
|
|
7
|
-
FINDING_KINDS,
|
|
8
|
-
FINDING_SIGNALS,
|
|
9
|
-
activeOutput,
|
|
10
|
-
activeOutputMode,
|
|
11
|
-
configFilePath,
|
|
12
|
-
createTextOutput,
|
|
13
|
-
hubContextOf,
|
|
14
|
-
isStampableStub,
|
|
15
|
-
parseConfigText,
|
|
16
|
-
readRafaBlocks,
|
|
17
|
-
resolveConfig
|
|
18
|
-
} from "./index-vp0xghk2.js";
|
|
19
|
-
import {
|
|
20
|
-
bless,
|
|
21
|
-
merge
|
|
22
|
-
} from "./index-d6d0942q.js";
|
|
23
|
-
import {
|
|
24
|
-
CommandExit,
|
|
25
|
-
commandSpelling
|
|
26
|
-
} from "./index-3mzw3kzh.js";
|
|
27
|
-
import {
|
|
28
|
-
BUSY_TIMEOUT_MAX_MS,
|
|
29
|
-
BUSY_TIMEOUT_MIN_MS,
|
|
30
3
|
RAFA_INSTINCTS_PATH,
|
|
31
|
-
STORE_BACKENDS,
|
|
32
|
-
SYNC_STRATEGIES,
|
|
33
|
-
TEST_SCOPES,
|
|
34
4
|
bundledBinDirectory,
|
|
5
|
+
resolveInstinctScopes,
|
|
6
|
+
tierExists,
|
|
7
|
+
TEST_SCOPES,
|
|
8
|
+
isMapping,
|
|
35
9
|
describeValue,
|
|
10
|
+
messageOf,
|
|
11
|
+
STORE_BACKENDS2,
|
|
12
|
+
SYNC_STRATEGIES2,
|
|
13
|
+
BUSY_TIMEOUT_MIN_MS,
|
|
14
|
+
BUSY_TIMEOUT_MAX_MS,
|
|
36
15
|
isBusyTimeoutMs,
|
|
37
16
|
isGitHubLogin,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
17
|
+
hubContextOf,
|
|
18
|
+
CONFIG_FILE2,
|
|
19
|
+
CONFIG_DEFAULTS2,
|
|
20
|
+
ConfigError2,
|
|
21
|
+
configFilePath2,
|
|
22
|
+
parseConfigText2,
|
|
23
|
+
resolveConfig2,
|
|
24
|
+
isStampableStub,
|
|
25
|
+
createTextOutput,
|
|
26
|
+
activeOutput,
|
|
27
|
+
activeOutputMode,
|
|
28
|
+
readRafaBlocks2,
|
|
29
|
+
FINDING_KINDS2,
|
|
30
|
+
FINDING_SIGNALS2,
|
|
31
|
+
FINDING_DOMAINS
|
|
32
|
+
} from "./index-2tjmxwvp.js";
|
|
43
33
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} from "./index-
|
|
34
|
+
bless2,
|
|
35
|
+
merge2
|
|
36
|
+
} from "./index-hgcvjke7.js";
|
|
37
|
+
import {
|
|
38
|
+
actionHash2,
|
|
39
|
+
triggerKey2
|
|
40
|
+
} from "./index-21jcfatr.js";
|
|
47
41
|
|
|
48
42
|
// src/adapters/registry.ts
|
|
49
43
|
import { homedir } from "os";
|
|
@@ -217,7 +211,7 @@ function guardTestProcess(path, probe = currentProbe()) {
|
|
|
217
211
|
function keyOrNull(value) {
|
|
218
212
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
219
213
|
}
|
|
220
|
-
var
|
|
214
|
+
var EFFORT_KEY_PROJECTIONS2 = {
|
|
221
215
|
sessions: (row) => keyOrNull(row.sessionId),
|
|
222
216
|
commits: (row) => keyOrNull(row.sha)
|
|
223
217
|
};
|
|
@@ -236,13 +230,13 @@ function readKind(repoRoot, kind) {
|
|
|
236
230
|
}
|
|
237
231
|
function keysOfKind(repoRoot, kind) {
|
|
238
232
|
const path = openedKindPath(repoRoot, kind);
|
|
239
|
-
return collectedKeys(path,
|
|
233
|
+
return collectedKeys(path, EFFORT_KEY_PROJECTIONS2[kind]);
|
|
240
234
|
}
|
|
241
235
|
function appendKind(repoRoot, kind, rows) {
|
|
242
236
|
const path = openedKindPath(repoRoot, kind);
|
|
243
|
-
return appendRows(path, rows,
|
|
237
|
+
return appendRows(path, rows, EFFORT_KEY_PROJECTIONS2[kind]);
|
|
244
238
|
}
|
|
245
|
-
function
|
|
239
|
+
function openNdjsonStore2(repoRoot) {
|
|
246
240
|
return {
|
|
247
241
|
append: (kind, rows) => appendKind(repoRoot, kind, rows),
|
|
248
242
|
keys: (kind) => keysOfKind(repoRoot, kind),
|
|
@@ -255,11 +249,11 @@ function openNdjsonStore(repoRoot) {
|
|
|
255
249
|
// src/effort/store/sqlite.ts
|
|
256
250
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
257
251
|
import { dirname as dirname10, join as join9 } from "path";
|
|
258
|
-
import { Database } from "bun:sqlite";
|
|
252
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
259
253
|
|
|
260
254
|
// package.json
|
|
261
255
|
var name = "@open-tomato/rafa";
|
|
262
|
-
var version = "0.
|
|
256
|
+
var version = "0.35.0";
|
|
263
257
|
|
|
264
258
|
// src/cli/version.ts
|
|
265
259
|
var RAFA_VERSION = version;
|
|
@@ -352,36 +346,36 @@ function mainCheckoutOf(dir, seams = {}) {
|
|
|
352
346
|
}
|
|
353
347
|
|
|
354
348
|
// src/project/scope.ts
|
|
355
|
-
var
|
|
356
|
-
var
|
|
357
|
-
var
|
|
349
|
+
var SCOPE_DIR2 = dirname3(CONFIG_FILE2);
|
|
350
|
+
var INIT_COMMAND2 = "rafa init";
|
|
351
|
+
var DISK_FILE_SYSTEM2 = Object.freeze({
|
|
358
352
|
exists: (path) => existsSync3(path),
|
|
359
353
|
realpath: (path) => realpathSync2(path)
|
|
360
354
|
});
|
|
361
355
|
|
|
362
|
-
class
|
|
356
|
+
class ScopeError2 extends Error {
|
|
363
357
|
constructor(message, options) {
|
|
364
358
|
super(`rafa scope: ${message}`, options);
|
|
365
359
|
this.name = "ScopeError";
|
|
366
360
|
}
|
|
367
361
|
}
|
|
368
|
-
function
|
|
369
|
-
return { dir: join4(base,
|
|
362
|
+
function scopeAt2(base) {
|
|
363
|
+
return { dir: join4(base, SCOPE_DIR2), configFile: configFilePath2(base) };
|
|
370
364
|
}
|
|
371
|
-
function
|
|
365
|
+
function initHint2(start, passedUserConfig = null, probedMainCheckout = null) {
|
|
372
366
|
const passed = passedUserConfig === null ? [] : [`${passedUserConfig} is the user scope's config and marks no project`];
|
|
373
367
|
const main = probedMainCheckout === null ? [] : [`nor in ${probedMainCheckout}, the main checkout of the repository holding it`];
|
|
374
368
|
return [
|
|
375
|
-
`not inside a rafa project: no ${
|
|
369
|
+
`not inside a rafa project: no ${CONFIG_FILE2} in ${start} or any directory above it`,
|
|
376
370
|
...main,
|
|
377
371
|
...passed,
|
|
378
|
-
`run \`${
|
|
372
|
+
`run \`${INIT_COMMAND2}\` to set one up`
|
|
379
373
|
].join(`
|
|
380
374
|
`);
|
|
381
375
|
}
|
|
382
376
|
function requireAbsolute(label, path) {
|
|
383
377
|
if (!isAbsolute3(path)) {
|
|
384
|
-
throw new
|
|
378
|
+
throw new ScopeError2(`${label} is ${describeValue(path)}, expected an absolute path`);
|
|
385
379
|
}
|
|
386
380
|
}
|
|
387
381
|
function realStartOf(start, fs) {
|
|
@@ -389,7 +383,7 @@ function realStartOf(start, fs) {
|
|
|
389
383
|
return fs.realpath(start);
|
|
390
384
|
} catch (error) {
|
|
391
385
|
const message = `start directory ${start} does not resolve (${messageOf(error)})`;
|
|
392
|
-
throw new
|
|
386
|
+
throw new ScopeError2(message, { cause: error });
|
|
393
387
|
}
|
|
394
388
|
}
|
|
395
389
|
function realHomeOf(home, fs) {
|
|
@@ -399,15 +393,15 @@ function realHomeOf(home, fs) {
|
|
|
399
393
|
return null;
|
|
400
394
|
}
|
|
401
395
|
}
|
|
402
|
-
function
|
|
396
|
+
function selfAndAncestors2(path) {
|
|
403
397
|
const parent = dirname3(path);
|
|
404
|
-
return parent === path ? [path] : [path, ...
|
|
398
|
+
return parent === path ? [path] : [path, ...selfAndAncestors2(parent)];
|
|
405
399
|
}
|
|
406
400
|
var GIT_MARKER = ".git";
|
|
407
401
|
function walkUp(dirs, realHome, fs, stopAtTopLevel) {
|
|
408
402
|
let passedUserConfig = false;
|
|
409
403
|
for (const dir of dirs) {
|
|
410
|
-
if (fs.exists(
|
|
404
|
+
if (fs.exists(configFilePath2(dir))) {
|
|
411
405
|
if (dir !== realHome)
|
|
412
406
|
return { root: dir, passedUserConfig, topLevel: null };
|
|
413
407
|
passedUserConfig = true;
|
|
@@ -424,39 +418,39 @@ function mainCheckoutAt(realStart, mainCheckout) {
|
|
|
424
418
|
} catch (error) {
|
|
425
419
|
if (!(error instanceof WorktreeRootError))
|
|
426
420
|
throw error;
|
|
427
|
-
throw new
|
|
421
|
+
throw new ScopeError2(`cannot read the main checkout of ${realStart} (${error.message})`, { cause: error });
|
|
428
422
|
}
|
|
429
423
|
}
|
|
430
424
|
function fallBack(realStart, realHome, fs, mainCheckout) {
|
|
431
425
|
const main = mainCheckoutAt(realStart, mainCheckout);
|
|
432
|
-
if (main === null || main === realHome ||
|
|
426
|
+
if (main === null || main === realHome || selfAndAncestors2(realStart).includes(main)) {
|
|
433
427
|
return { root: null, probed: null };
|
|
434
428
|
}
|
|
435
|
-
return fs.exists(
|
|
429
|
+
return fs.exists(configFilePath2(main)) ? { root: main, probed: main } : { root: null, probed: main };
|
|
436
430
|
}
|
|
437
431
|
function walkAbove(lower, realHome, fs) {
|
|
438
432
|
if (lower.topLevel === null)
|
|
439
433
|
return lower;
|
|
440
|
-
const above =
|
|
434
|
+
const above = selfAndAncestors2(lower.topLevel).slice(1);
|
|
441
435
|
const upper = walkUp(above, realHome, fs, false);
|
|
442
436
|
return { ...upper, passedUserConfig: lower.passedUserConfig || upper.passedUserConfig };
|
|
443
437
|
}
|
|
444
|
-
function
|
|
445
|
-
const { home, fs =
|
|
438
|
+
function resolveScope2(start, seams) {
|
|
439
|
+
const { home, fs = DISK_FILE_SYSTEM2, mainCheckout = mainCheckoutOf } = seams;
|
|
446
440
|
requireAbsolute("start directory", start);
|
|
447
441
|
requireAbsolute("home directory", home);
|
|
448
|
-
const user =
|
|
442
|
+
const user = scopeAt2(home);
|
|
449
443
|
const realStart = realStartOf(start, fs);
|
|
450
444
|
const realHome = realHomeOf(home, fs);
|
|
451
|
-
const lower = walkUp(
|
|
445
|
+
const lower = walkUp(selfAndAncestors2(realStart), realHome, fs, true);
|
|
452
446
|
const fallback = lower.root === null ? fallBack(realStart, realHome, fs, mainCheckout) : { root: lower.root, probed: null };
|
|
453
447
|
const walk = fallback.root === null ? walkAbove(lower, realHome, fs) : lower;
|
|
454
448
|
const root = fallback.root ?? walk.root;
|
|
455
449
|
if (root === null) {
|
|
456
450
|
const passed = walk.passedUserConfig ? user.configFile : null;
|
|
457
|
-
return { found: false, start, home, user, hint:
|
|
451
|
+
return { found: false, start, home, user, hint: initHint2(start, passed, fallback.probed) };
|
|
458
452
|
}
|
|
459
|
-
return { found: true, root, home, project:
|
|
453
|
+
return { found: true, root, home, project: scopeAt2(root), user };
|
|
460
454
|
}
|
|
461
455
|
|
|
462
456
|
// src/loop/session-record-parse.ts
|
|
@@ -473,7 +467,7 @@ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmS
|
|
|
473
467
|
import { dirname as dirname4, join as join5 } from "path";
|
|
474
468
|
var POSITION_FILE = "position.json";
|
|
475
469
|
function positionFilePath(root) {
|
|
476
|
-
return join5(
|
|
470
|
+
return join5(scopeAt2(root).dir, POSITION_FILE);
|
|
477
471
|
}
|
|
478
472
|
function isRecord(value) {
|
|
479
473
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -550,7 +544,7 @@ var HOP_FILE = "hop.json";
|
|
|
550
544
|
var HOP_KINDS = ["blocker", "dry"];
|
|
551
545
|
var HOP_STATES = ["away", "waiting", "merged", "halted"];
|
|
552
546
|
function hopFilePath(root) {
|
|
553
|
-
return join6(
|
|
547
|
+
return join6(scopeAt2(root).dir, HOP_FILE);
|
|
554
548
|
}
|
|
555
549
|
function isRecord2(value) {
|
|
556
550
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -696,8 +690,8 @@ function taskProblem(task) {
|
|
|
696
690
|
function hopProblem(fields) {
|
|
697
691
|
if (!Object.hasOwn(fields, "hop"))
|
|
698
692
|
return null;
|
|
699
|
-
const
|
|
700
|
-
return asHopRecord(
|
|
693
|
+
const hop = field(fields, "hop");
|
|
694
|
+
return asHopRecord(hop) === null ? `hop is ${describeValue2(hop)}, expected a hop record` : null;
|
|
701
695
|
}
|
|
702
696
|
function worktreeProblem(fields) {
|
|
703
697
|
if (!Object.hasOwn(fields, "worktree"))
|
|
@@ -799,8 +793,8 @@ function phaseEntry(phase) {
|
|
|
799
793
|
return isSessionPhase(phase) ? { phase } : {};
|
|
800
794
|
}
|
|
801
795
|
function freezeHop(value) {
|
|
802
|
-
const
|
|
803
|
-
return Object.freeze({ ...
|
|
796
|
+
const hop = asHopRecord(value);
|
|
797
|
+
return Object.freeze({ ...hop, home: Object.freeze(hop.home), from: Object.freeze(hop.from) });
|
|
804
798
|
}
|
|
805
799
|
function freezeFailures(failures) {
|
|
806
800
|
return Object.freeze(failures.map((failure) => Object.freeze({ file: failure.file, name: failure.name })));
|
|
@@ -824,7 +818,7 @@ function stepsEntry(steps) {
|
|
|
824
818
|
}
|
|
825
819
|
function freezeRecord(fields) {
|
|
826
820
|
const task = field(fields, "task");
|
|
827
|
-
const
|
|
821
|
+
const hop = field(fields, "hop");
|
|
828
822
|
const worktree = field(fields, "worktree");
|
|
829
823
|
return Object.freeze({
|
|
830
824
|
sessionId: field(fields, "sessionId"),
|
|
@@ -836,7 +830,7 @@ function freezeRecord(fields) {
|
|
|
836
830
|
state: field(fields, "state"),
|
|
837
831
|
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
|
|
838
832
|
...phaseEntry(field(fields, "phase")),
|
|
839
|
-
...
|
|
833
|
+
...hop === undefined ? {} : { hop: freezeHop(hop) },
|
|
840
834
|
...worktree === undefined ? {} : { worktree },
|
|
841
835
|
...stepsEntry(field(fields, "steps"))
|
|
842
836
|
});
|
|
@@ -887,7 +881,7 @@ class SessionStateError extends Error {
|
|
|
887
881
|
}
|
|
888
882
|
}
|
|
889
883
|
function runsDir(root) {
|
|
890
|
-
return join7(
|
|
884
|
+
return join7(scopeAt2(root).dir, RUNS_DIR);
|
|
891
885
|
}
|
|
892
886
|
function sessionFilePath(root, sessionId) {
|
|
893
887
|
if (!isSessionId(sessionId)) {
|
|
@@ -937,7 +931,7 @@ function byStart(a, b) {
|
|
|
937
931
|
function readSessions(root, seams = {}) {
|
|
938
932
|
const dir = runsDir(root);
|
|
939
933
|
const isAlive = seams.isAlive ?? isPidAlive;
|
|
940
|
-
const records = recordNames(dir).filter((
|
|
934
|
+
const records = recordNames(dir).filter((name) => name.endsWith(RECORD_EXTENSION)).map((name) => readRecordFile(join7(dir, name))).map((record) => Object.freeze({ ...record, state: readState(record, isAlive) }));
|
|
941
935
|
return Object.freeze(records.sort(byStart));
|
|
942
936
|
}
|
|
943
937
|
function readSession(root, sessionId, seams = {}) {
|
|
@@ -1023,8 +1017,8 @@ function manifestName(file) {
|
|
|
1023
1017
|
}
|
|
1024
1018
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
1025
1019
|
return null;
|
|
1026
|
-
const
|
|
1027
|
-
return typeof
|
|
1020
|
+
const name = parsed["name"];
|
|
1021
|
+
return typeof name === "string" ? name : null;
|
|
1028
1022
|
}
|
|
1029
1023
|
function nearestRafaPackage(start) {
|
|
1030
1024
|
let dir = start;
|
|
@@ -1069,7 +1063,7 @@ class DevelopmentBuildRefusedError extends Error {
|
|
|
1069
1063
|
function storeProjectRoot(path) {
|
|
1070
1064
|
const storeDir = dirname8(path);
|
|
1071
1065
|
const scopeDir = dirname8(storeDir);
|
|
1072
|
-
if (basename3(storeDir) !== EFFORT_DIR_NAME || basename3(scopeDir) !==
|
|
1066
|
+
if (basename3(storeDir) !== EFFORT_DIR_NAME || basename3(scopeDir) !== SCOPE_DIR2)
|
|
1073
1067
|
return null;
|
|
1074
1068
|
return dirname8(scopeDir);
|
|
1075
1069
|
}
|
|
@@ -1469,6 +1463,13 @@ var SQLITE_MIGRATIONS = [
|
|
|
1469
1463
|
recorded_at TEXT NOT NULL CHECK (recorded_at <> '')
|
|
1470
1464
|
);
|
|
1471
1465
|
`
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
id: "store-meta-generation",
|
|
1469
|
+
breaks: [],
|
|
1470
|
+
sql: `
|
|
1471
|
+
ALTER TABLE store_meta ADD COLUMN generation TEXT CHECK (generation <> '');
|
|
1472
|
+
`
|
|
1472
1473
|
}
|
|
1473
1474
|
];
|
|
1474
1475
|
var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
|
|
@@ -1682,7 +1683,7 @@ function stampedValues(table) {
|
|
|
1682
1683
|
|
|
1683
1684
|
// src/effort/store/settings.ts
|
|
1684
1685
|
var DEFAULT_STORE_SETTINGS = Object.freeze({
|
|
1685
|
-
busyTimeoutMs:
|
|
1686
|
+
busyTimeoutMs: CONFIG_DEFAULTS2.effortBusyTimeoutMs
|
|
1686
1687
|
});
|
|
1687
1688
|
var active = DEFAULT_STORE_SETTINGS;
|
|
1688
1689
|
function setActiveStoreSettings(settings) {
|
|
@@ -1700,13 +1701,55 @@ function activeStoreSettings() {
|
|
|
1700
1701
|
}
|
|
1701
1702
|
|
|
1702
1703
|
// src/effort/store/store-meta.ts
|
|
1703
|
-
import { randomUUID as
|
|
1704
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1704
1705
|
import { dirname as dirname9 } from "path";
|
|
1706
|
+
import { Database } from "bun:sqlite";
|
|
1707
|
+
|
|
1708
|
+
// src/effort/store/store-generation.ts
|
|
1709
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1710
|
+
import { readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1711
|
+
var STORE_GENERATION_SUFFIX = ".generation";
|
|
1712
|
+
function storeGenerationPath(storePath) {
|
|
1713
|
+
return `${storePath}${STORE_GENERATION_SUFFIX}`;
|
|
1714
|
+
}
|
|
1715
|
+
function isMissingFile(error) {
|
|
1716
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
1717
|
+
}
|
|
1718
|
+
function readStoreGeneration(storePath) {
|
|
1719
|
+
let text;
|
|
1720
|
+
try {
|
|
1721
|
+
text = readFileSync6(storeGenerationPath(storePath), "utf8");
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
if (isMissingFile(error))
|
|
1724
|
+
return null;
|
|
1725
|
+
throw error;
|
|
1726
|
+
}
|
|
1727
|
+
const generation = text.trim();
|
|
1728
|
+
return generation === "" ? null : generation;
|
|
1729
|
+
}
|
|
1730
|
+
function assertWritableGeneration(generation) {
|
|
1731
|
+
if (generation === "" || generation !== generation.trim() || /[\r\n]/.test(generation)) {
|
|
1732
|
+
throw new Error(`store generation must be non-empty, unpadded and on one line, got ${JSON.stringify(generation)}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function writeStoreGeneration(storePath, generation) {
|
|
1736
|
+
assertWritableGeneration(generation);
|
|
1737
|
+
const file = storeGenerationPath(storePath);
|
|
1738
|
+
const temporary = `${file}.${String(process.pid)}.${randomUUID3()}.tmp`;
|
|
1739
|
+
try {
|
|
1740
|
+
writeFileSync4(temporary, `${generation}
|
|
1741
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
1742
|
+
renameSync4(temporary, file);
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
rmSync4(temporary, { force: true });
|
|
1745
|
+
throw error;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1705
1748
|
|
|
1706
1749
|
// src/effort/store/store-identity.ts
|
|
1707
1750
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1708
1751
|
import { createHmac } from "crypto";
|
|
1709
|
-
import { readFileSync as
|
|
1752
|
+
import { readFileSync as readFileSync7, realpathSync as realpathSync4, statSync } from "fs";
|
|
1710
1753
|
import { hostname, platform as currentPlatform } from "os";
|
|
1711
1754
|
|
|
1712
1755
|
// src/schema/project-id.ts
|
|
@@ -1744,7 +1787,7 @@ function hostIdFrom(source, value) {
|
|
|
1744
1787
|
}
|
|
1745
1788
|
function readMachineIdFile() {
|
|
1746
1789
|
try {
|
|
1747
|
-
const value =
|
|
1790
|
+
const value = readFileSync7(MACHINE_ID_PATH, "utf8").trim();
|
|
1748
1791
|
return value === "" ? null : value;
|
|
1749
1792
|
} catch {
|
|
1750
1793
|
return null;
|
|
@@ -1780,14 +1823,19 @@ function readStoreFileFacts(path) {
|
|
|
1780
1823
|
return { storePath, fileDev: stats.dev, fileIno: stats.ino };
|
|
1781
1824
|
}
|
|
1782
1825
|
function observeStore(path, readHost = readHostId) {
|
|
1783
|
-
|
|
1826
|
+
const fileFacts = readStoreFileFacts(path);
|
|
1827
|
+
return { hostId: readHost(), ...fileFacts, generation: readStoreGeneration(fileFacts.storePath) };
|
|
1828
|
+
}
|
|
1829
|
+
function staleGeneration(recorded, facts) {
|
|
1830
|
+
return recorded.generation !== null && recorded.generation !== facts.generation;
|
|
1784
1831
|
}
|
|
1785
1832
|
function movedFacts(recorded, facts) {
|
|
1786
1833
|
const sameFile = BigInt(recorded.fileDev) === facts.fileDev && BigInt(recorded.fileIno) === facts.fileIno;
|
|
1787
1834
|
const moved = [
|
|
1788
1835
|
["host", recorded.hostId !== facts.hostId],
|
|
1789
1836
|
["path", recorded.storePath !== facts.storePath],
|
|
1790
|
-
["file", !sameFile]
|
|
1837
|
+
["file", !sameFile],
|
|
1838
|
+
["generation", staleGeneration(recorded, facts)]
|
|
1791
1839
|
];
|
|
1792
1840
|
return moved.filter(([, differs]) => differs).map(([reason]) => reason);
|
|
1793
1841
|
}
|
|
@@ -1827,8 +1875,12 @@ function toStoredInteger(value) {
|
|
|
1827
1875
|
function fromStoredInteger(text) {
|
|
1828
1876
|
return BigInt.asUintN(STORED_INTEGER_BITS, BigInt(text));
|
|
1829
1877
|
}
|
|
1878
|
+
function hasGenerationColumn(db) {
|
|
1879
|
+
return db.query("SELECT count(*) AS n FROM pragma_table_info('store_meta') WHERE name = 'generation'").get()?.n === 1;
|
|
1880
|
+
}
|
|
1830
1881
|
function readStoreMeta(db) {
|
|
1831
|
-
const
|
|
1882
|
+
const generation = hasGenerationColumn(db) ? "generation" : "NULL AS generation";
|
|
1883
|
+
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," + ` ${generation} FROM store_meta WHERE id = 1`).get();
|
|
1832
1884
|
if (row === null)
|
|
1833
1885
|
return null;
|
|
1834
1886
|
return {
|
|
@@ -1839,7 +1891,8 @@ function readStoreMeta(db) {
|
|
|
1839
1891
|
storePath: row.store_path,
|
|
1840
1892
|
fileDev: fromStoredInteger(row.file_dev),
|
|
1841
1893
|
fileIno: fromStoredInteger(row.file_ino),
|
|
1842
|
-
mintedAt: row.minted_at
|
|
1894
|
+
mintedAt: row.minted_at,
|
|
1895
|
+
generation: row.generation
|
|
1843
1896
|
};
|
|
1844
1897
|
}
|
|
1845
1898
|
function projectFor(storePath, recorded, seams) {
|
|
@@ -1850,14 +1903,30 @@ function projectFor(storePath, recorded, seams) {
|
|
|
1850
1903
|
return null;
|
|
1851
1904
|
return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
|
|
1852
1905
|
}
|
|
1853
|
-
function writeStoreMeta(db, facts, project, seams) {
|
|
1854
|
-
const storeId = (seams.newStoreId ??
|
|
1906
|
+
function writeStoreMeta(db, facts, project, generation, seams) {
|
|
1907
|
+
const storeId = (seams.newStoreId ?? randomUUID4)();
|
|
1855
1908
|
const mintedAt = (seams.now?.() ?? new Date).toISOString();
|
|
1856
|
-
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);
|
|
1909
|
+
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, generation) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(storeId, project.rootCommit, project.remote, facts.hostId, facts.storePath, toStoredInteger(facts.fileDev), toStoredInteger(facts.fileIno), mintedAt, generation);
|
|
1857
1910
|
return storeId;
|
|
1858
1911
|
}
|
|
1859
|
-
function
|
|
1860
|
-
|
|
1912
|
+
function settleUnderLock(db, observe, prefetched, seams) {
|
|
1913
|
+
const current = readStoreMeta(db);
|
|
1914
|
+
const facts = observe();
|
|
1915
|
+
const decision = decideStoreIdentity("write", current, () => facts);
|
|
1916
|
+
if (decision.action === "none")
|
|
1917
|
+
return { action: "none" };
|
|
1918
|
+
const generation = (seams.newGeneration ?? randomUUID4)();
|
|
1919
|
+
if (decision.action === "keep") {
|
|
1920
|
+
db.query("UPDATE store_meta SET generation = ? WHERE id = 1").run(generation);
|
|
1921
|
+
writeStoreGeneration(facts.storePath, generation);
|
|
1922
|
+
return { action: "keep", storeId: decision.storeId };
|
|
1923
|
+
}
|
|
1924
|
+
const project = prefetched ?? projectFor(facts.storePath, current, seams);
|
|
1925
|
+
if (project === null)
|
|
1926
|
+
return { action: "no-project", reasons: decision.reasons };
|
|
1927
|
+
const storeId = writeStoreMeta(db, facts, project, generation, seams);
|
|
1928
|
+
writeStoreGeneration(facts.storePath, generation);
|
|
1929
|
+
return { action: "mint", storeId, reasons: decision.reasons };
|
|
1861
1930
|
}
|
|
1862
1931
|
function settleStoreIdentity(db, path, access, seams = {}) {
|
|
1863
1932
|
if (access === "read")
|
|
@@ -1865,19 +1934,72 @@ function settleStoreIdentity(db, path, access, seams = {}) {
|
|
|
1865
1934
|
const observe = () => observeStore(path, seams.readHostId ?? readHostId);
|
|
1866
1935
|
const recorded = readStoreMeta(db);
|
|
1867
1936
|
const first = decideStoreIdentity(access, recorded, observe);
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
const project = projectFor(first.facts.storePath, recorded, seams);
|
|
1871
|
-
if (project === null)
|
|
1937
|
+
const prefetched = first.action === "mint" ? projectFor(first.facts.storePath, recorded, seams) : undefined;
|
|
1938
|
+
if (first.action === "mint" && prefetched === null)
|
|
1872
1939
|
return { action: "no-project", reasons: first.reasons };
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1940
|
+
const underLock = db.transaction(() => settleUnderLock(db, observe, prefetched ?? undefined, seams));
|
|
1941
|
+
return underLock.immediate();
|
|
1942
|
+
}
|
|
1943
|
+
function hasStoreMetaTable(db) {
|
|
1944
|
+
return db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
|
|
1945
|
+
}
|
|
1946
|
+
function decideLiveWrite(livePath, readHost) {
|
|
1947
|
+
const live = new Database(livePath, { readonly: true });
|
|
1948
|
+
let recorded;
|
|
1949
|
+
try {
|
|
1950
|
+
if (!hasStoreMetaTable(live))
|
|
1951
|
+
return { action: "no-table" };
|
|
1952
|
+
recorded = readStoreMeta(live);
|
|
1953
|
+
} finally {
|
|
1954
|
+
live.close();
|
|
1955
|
+
}
|
|
1956
|
+
if (recorded === null)
|
|
1957
|
+
return { action: "no-row" };
|
|
1958
|
+
const facts = observeStore(livePath, readHost);
|
|
1959
|
+
const decision = decideStoreIdentity("write", recorded, () => facts);
|
|
1960
|
+
if (decision.action === "keep")
|
|
1961
|
+
return { action: "keep", storeId: decision.storeId };
|
|
1962
|
+
const reasons = decision.action === "mint" ? decision.reasons : [];
|
|
1963
|
+
return { action: "mint", reasons };
|
|
1964
|
+
}
|
|
1965
|
+
function holdsRowOf(db, storeId) {
|
|
1966
|
+
return hasStoreMetaTable(db) && readStoreMeta(db)?.storeId === storeId;
|
|
1967
|
+
}
|
|
1968
|
+
function spoilGeneration(db, newGeneration) {
|
|
1969
|
+
if (!hasStoreMetaTable(db) || !hasGenerationColumn(db))
|
|
1970
|
+
return null;
|
|
1971
|
+
const rows = db.query("SELECT count(*) AS n FROM store_meta WHERE id = 1").get()?.n;
|
|
1972
|
+
if (rows !== 1)
|
|
1973
|
+
return null;
|
|
1974
|
+
const generation = newGeneration();
|
|
1975
|
+
db.query("UPDATE store_meta SET generation = ? WHERE id = 1").run(generation);
|
|
1976
|
+
return generation;
|
|
1977
|
+
}
|
|
1978
|
+
function writeCarried(db, storeId, parallel, newGeneration) {
|
|
1979
|
+
const dev = toStoredInteger(parallel.fileDev);
|
|
1980
|
+
const ino = toStoredInteger(parallel.fileIno);
|
|
1981
|
+
if (hasGenerationColumn(db)) {
|
|
1982
|
+
const generation = newGeneration();
|
|
1983
|
+
db.query("UPDATE store_meta SET file_dev = ?, file_ino = ?, generation = ? WHERE id = 1").run(dev, ino, generation);
|
|
1984
|
+
return { action: "carried", storeId, generation };
|
|
1985
|
+
}
|
|
1986
|
+
db.query("UPDATE store_meta SET file_dev = ?, file_ino = ? WHERE id = 1").run(dev, ino);
|
|
1987
|
+
return { action: "carried", storeId, generation: null };
|
|
1988
|
+
}
|
|
1989
|
+
function carryStoreIdentity(livePath, parallelPath, seams = {}) {
|
|
1990
|
+
const parallel = readStoreFileFacts(parallelPath);
|
|
1991
|
+
const live = decideLiveWrite(livePath, seams.readHostId ?? readHostId);
|
|
1992
|
+
const newGeneration = seams.newGeneration ?? randomUUID4;
|
|
1993
|
+
const db = new Database(parallelPath, { readwrite: true });
|
|
1994
|
+
try {
|
|
1995
|
+
if (live.action === "keep" && holdsRowOf(db, live.storeId)) {
|
|
1996
|
+
return writeCarried(db, live.storeId, parallel, newGeneration);
|
|
1997
|
+
}
|
|
1998
|
+
const spoiled = spoilGeneration(db, newGeneration);
|
|
1999
|
+
return live.action === "keep" ? { action: "other-row", spoiled } : { ...live, spoiled };
|
|
2000
|
+
} finally {
|
|
2001
|
+
db.close();
|
|
2002
|
+
}
|
|
1881
2003
|
}
|
|
1882
2004
|
|
|
1883
2005
|
// src/effort/store/sqlite.ts
|
|
@@ -1897,7 +2019,7 @@ function refused(index, length, reason) {
|
|
|
1897
2019
|
return new Error(`effort store: row ${index} of ${length} ${reason}; batch refused`);
|
|
1898
2020
|
}
|
|
1899
2021
|
function batchEntries(kind, rows) {
|
|
1900
|
-
const keyOf =
|
|
2022
|
+
const keyOf = EFFORT_KEY_PROJECTIONS2[kind];
|
|
1901
2023
|
const projected = rows.map((row) => ({ key: keyOf(row), row }));
|
|
1902
2024
|
const keyed = projected.filter(hasKey);
|
|
1903
2025
|
if (keyed.length < projected.length) {
|
|
@@ -1915,7 +2037,7 @@ function withSqliteStore(path, access, create, use, identity = {}) {
|
|
|
1915
2037
|
guardTestProcess(path);
|
|
1916
2038
|
if (create)
|
|
1917
2039
|
mkdirSync5(dirname10(path), { recursive: true });
|
|
1918
|
-
const db = new
|
|
2040
|
+
const db = new Database2(path, { readwrite: true, create });
|
|
1919
2041
|
try {
|
|
1920
2042
|
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
1921
2043
|
bringForward(db, path, access, "open");
|
|
@@ -1949,8 +2071,8 @@ function appendKind2(path, kind, rows) {
|
|
|
1949
2071
|
function keysOfKind2(path, kind) {
|
|
1950
2072
|
if (!existsSync5(path))
|
|
1951
2073
|
return new Set;
|
|
1952
|
-
const { name
|
|
1953
|
-
const keys = withSqliteStore(path, "read", false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${
|
|
2074
|
+
const { name, keyColumn } = KIND_TABLES[kind];
|
|
2075
|
+
const keys = withSqliteStore(path, "read", false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${name} ORDER BY seq`).all());
|
|
1954
2076
|
return new Set(keys.map(({ key }) => key));
|
|
1955
2077
|
}
|
|
1956
2078
|
function parsedObject(body) {
|
|
@@ -1971,11 +2093,11 @@ function rowOf(body, where) {
|
|
|
1971
2093
|
function readKind2(path, kind) {
|
|
1972
2094
|
if (!existsSync5(path))
|
|
1973
2095
|
return [];
|
|
1974
|
-
const { name
|
|
1975
|
-
const stored = withSqliteStore(path, "read", false, (db) => db.query(`SELECT seq, row_json AS body FROM ${
|
|
1976
|
-
return stored.map(({ seq, body }) => rowOf(body, `${path}: ${
|
|
2096
|
+
const { name } = KIND_TABLES[kind];
|
|
2097
|
+
const stored = withSqliteStore(path, "read", false, (db) => db.query(`SELECT seq, row_json AS body FROM ${name} ORDER BY seq`).all());
|
|
2098
|
+
return stored.map(({ seq, body }) => rowOf(body, `${path}: ${name} row ${seq}`));
|
|
1977
2099
|
}
|
|
1978
|
-
function
|
|
2100
|
+
function openSqliteStore2(repoRoot) {
|
|
1979
2101
|
const path = sqliteStorePath(repoRoot);
|
|
1980
2102
|
return {
|
|
1981
2103
|
append: (kind, rows) => appendKind2(path, kind, rows),
|
|
@@ -1989,28 +2111,241 @@ function openSqliteStore(repoRoot) {
|
|
|
1989
2111
|
import { existsSync as existsSync12 } from "fs";
|
|
1990
2112
|
import { join as join12, resolve as resolve5 } from "path";
|
|
1991
2113
|
|
|
2114
|
+
// src/cli/spends.ts
|
|
2115
|
+
var SPENDS_GLYPH = "\uD83E\uDE99";
|
|
2116
|
+
var SPENDS_WHEN = ["always", "with", "unless", "through"];
|
|
2117
|
+
var FLAGGED = ["with", "unless"];
|
|
2118
|
+
var FLAG_SPELLING = /^--[^\s-]\S*$/;
|
|
2119
|
+
function flagProblem(when, flag) {
|
|
2120
|
+
if (FLAGGED.includes(when)) {
|
|
2121
|
+
return typeof flag === "string" && FLAG_SPELLING.test(flag) ? null : `spends.flag is ${describeValue(flag)}, expected a flag as typed, starting with --`;
|
|
2122
|
+
}
|
|
2123
|
+
return flag === undefined ? null : `spends.flag is ${describeValue(flag)}, expected absent when spends.when is "${when}"`;
|
|
2124
|
+
}
|
|
2125
|
+
function spendsProblem(value) {
|
|
2126
|
+
if (value === undefined)
|
|
2127
|
+
return null;
|
|
2128
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2129
|
+
return `spends is ${describeValue(value)}, expected absent or a mapping`;
|
|
2130
|
+
}
|
|
2131
|
+
const { when, what, flag } = value;
|
|
2132
|
+
if (!SPENDS_WHEN.includes(when)) {
|
|
2133
|
+
return `spends.when is ${describeValue(when)}, expected one of ${SPENDS_WHEN.join(", ")}`;
|
|
2134
|
+
}
|
|
2135
|
+
if (typeof what !== "string") {
|
|
2136
|
+
return `spends.what is ${describeValue(what)}, expected a string`;
|
|
2137
|
+
}
|
|
2138
|
+
return flagProblem(when, flag);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// src/cli/command.ts
|
|
2142
|
+
var COMMAND_OUTPUTS = ["text", "json", "events", "tui"];
|
|
2143
|
+
var MAX_EXIT_CODE = 255;
|
|
2144
|
+
|
|
2145
|
+
class CommandExit extends Error {
|
|
2146
|
+
exitCode;
|
|
2147
|
+
constructor(exitCode, message = "") {
|
|
2148
|
+
super(message);
|
|
2149
|
+
if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > MAX_EXIT_CODE) {
|
|
2150
|
+
throw new TypeError(`CommandExit: exit code is ${describeValue(exitCode)}, expected a whole number from 0 to ${MAX_EXIT_CODE}`);
|
|
2151
|
+
}
|
|
2152
|
+
this.name = "CommandExit";
|
|
2153
|
+
this.exitCode = exitCode;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
function isListOf(value, check) {
|
|
2157
|
+
return Array.isArray(value) && value.every(check);
|
|
2158
|
+
}
|
|
2159
|
+
function hasStrings(value, keys) {
|
|
2160
|
+
if (typeof value !== "object" || value === null)
|
|
2161
|
+
return false;
|
|
2162
|
+
const record = value;
|
|
2163
|
+
return keys.every((key) => typeof record[key] === "string");
|
|
2164
|
+
}
|
|
2165
|
+
function isRoutingWord(value) {
|
|
2166
|
+
return typeof value === "string" && /^[^\s/-][^\s/]*$/.test(value);
|
|
2167
|
+
}
|
|
2168
|
+
function isFlag(item) {
|
|
2169
|
+
if (!hasStrings(item, ["name"]))
|
|
2170
|
+
return false;
|
|
2171
|
+
const { deprecated } = item;
|
|
2172
|
+
return deprecated === undefined || hasStrings(deprecated, ["use"]);
|
|
2173
|
+
}
|
|
2174
|
+
function fieldProblem(fields) {
|
|
2175
|
+
const checks = [
|
|
2176
|
+
["subject", isRoutingWord(fields.subject), "a word with no space, no slash and no leading dash"],
|
|
2177
|
+
["action", isRoutingWord(fields.action), "a word with no space, no slash and no leading dash"],
|
|
2178
|
+
["name", typeof fields.name === "string", "a string"],
|
|
2179
|
+
["summary", typeof fields.summary === "string", "a string"],
|
|
2180
|
+
["description", typeof fields.description === "string", "a string"],
|
|
2181
|
+
["args", isListOf(fields.args, (item) => hasStrings(item, ["name"])), "a list of named arguments"],
|
|
2182
|
+
["flags", isListOf(fields.flags, isFlag), "a list of named flags, each deprecation naming its use"],
|
|
2183
|
+
["examples", isListOf(fields.examples, (item) => hasStrings(item, ["cmd", "note"])), "a list of examples"],
|
|
2184
|
+
[
|
|
2185
|
+
"outputs",
|
|
2186
|
+
isListOf(fields.outputs, (item) => COMMAND_OUTPUTS.includes(item)),
|
|
2187
|
+
`a list of ${COMMAND_OUTPUTS.join(", ")}`
|
|
2188
|
+
],
|
|
2189
|
+
[
|
|
2190
|
+
"aliases",
|
|
2191
|
+
fields.aliases === undefined || isListOf(fields.aliases, (item) => typeof item === "string"),
|
|
2192
|
+
"absent or a list of strings"
|
|
2193
|
+
],
|
|
2194
|
+
[
|
|
2195
|
+
"lastingAliases",
|
|
2196
|
+
fields.lastingAliases === undefined || isListOf(fields.lastingAliases, (item) => typeof item === "string"),
|
|
2197
|
+
"absent or a list of strings"
|
|
2198
|
+
],
|
|
2199
|
+
[
|
|
2200
|
+
"deprecated",
|
|
2201
|
+
fields.deprecated === undefined || hasStrings(fields.deprecated, ["since", "use"]),
|
|
2202
|
+
"absent or a mapping of since and use"
|
|
2203
|
+
],
|
|
2204
|
+
["hidden", fields.hidden === undefined || typeof fields.hidden === "boolean", "absent or a boolean"],
|
|
2205
|
+
[
|
|
2206
|
+
"needsProject",
|
|
2207
|
+
fields.needsProject === undefined || typeof fields.needsProject === "boolean",
|
|
2208
|
+
"absent or a boolean"
|
|
2209
|
+
],
|
|
2210
|
+
["exec", fields.exec === undefined || fields.exec === true, "absent or true"],
|
|
2211
|
+
["run", typeof fields.run === "function", "a function"]
|
|
2212
|
+
];
|
|
2213
|
+
const failed = checks.find(([, passes]) => !passes);
|
|
2214
|
+
if (failed === undefined)
|
|
2215
|
+
return null;
|
|
2216
|
+
const [key, , expected] = failed;
|
|
2217
|
+
return `${key} is ${describeValue(fields[key])}, expected ${expected}`;
|
|
2218
|
+
}
|
|
2219
|
+
function commandProblem(value) {
|
|
2220
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2221
|
+
return `a command is ${describeValue(value)}, expected a mapping`;
|
|
2222
|
+
}
|
|
2223
|
+
const fields = value;
|
|
2224
|
+
const problem = fieldProblem(fields) ?? spendsProblem(fields.spends);
|
|
2225
|
+
if (problem === null)
|
|
2226
|
+
return null;
|
|
2227
|
+
const named = isRoutingWord(fields.subject) && isRoutingWord(fields.action) ? `command "${commandSpelling(fields)}"` : "a command";
|
|
2228
|
+
return `${named}: ${problem}`;
|
|
2229
|
+
}
|
|
2230
|
+
function isTopLevel(command) {
|
|
2231
|
+
return command.subject === command.action;
|
|
2232
|
+
}
|
|
2233
|
+
function commandSpelling(command) {
|
|
2234
|
+
return isTopLevel(command) ? command.subject : `${command.subject} ${command.action}`;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
1992
2237
|
// src/effort/store/fix-schema.ts
|
|
1993
|
-
import { existsSync as
|
|
1994
|
-
import { Database as
|
|
2238
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2239
|
+
import { Database as Database4 } from "bun:sqlite";
|
|
1995
2240
|
|
|
1996
2241
|
// src/effort/store/rebuild-aside.ts
|
|
1997
|
-
import { existsSync as
|
|
2242
|
+
import { closeSync, existsSync as existsSync7, fsyncSync, openSync, renameSync as renameSync5, rmSync as rmSync6 } from "fs";
|
|
2243
|
+
|
|
2244
|
+
// src/effort/store/copy.ts
|
|
2245
|
+
import { copyFileSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync2, rmSync as rmSync5, statSync as statSync2 } from "fs";
|
|
2246
|
+
import { join as join10, resolve as resolve3 } from "path";
|
|
2247
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
2248
|
+
var NDJSON_KINDS = { sessions: true, commits: true };
|
|
2249
|
+
var EFFORT_COPY_FILE_NAMES = [
|
|
2250
|
+
SQLITE_STORE_FILE_NAME,
|
|
2251
|
+
...Object.keys(NDJSON_KINDS).map((kind) => effortStoreFileName(kind))
|
|
2252
|
+
];
|
|
2253
|
+
|
|
2254
|
+
class EffortCopyRefusal extends Error {
|
|
2255
|
+
name = "EffortCopyRefusal";
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
class EffortCopyFailure extends Error {
|
|
2259
|
+
name = "EffortCopyFailure";
|
|
2260
|
+
}
|
|
2261
|
+
function refuseUsedTarget(target) {
|
|
2262
|
+
if (!existsSync6(target))
|
|
2263
|
+
return;
|
|
2264
|
+
let entries;
|
|
2265
|
+
try {
|
|
2266
|
+
if (!statSync2(target).isDirectory()) {
|
|
2267
|
+
throw new EffortCopyRefusal(`${target} is a file, not a directory; name an empty or absent directory with --to=<dir>.`);
|
|
2268
|
+
}
|
|
2269
|
+
entries = readdirSync2(target);
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
if (error instanceof EffortCopyRefusal)
|
|
2272
|
+
throw error;
|
|
2273
|
+
throw new EffortCopyFailure(`${target} cannot be read: ${String(error)}`);
|
|
2274
|
+
}
|
|
2275
|
+
if (entries.length === 0)
|
|
2276
|
+
return;
|
|
2277
|
+
throw new EffortCopyRefusal(`${target} is not empty (${String(entries.length)} entries); nothing was copied.` + " Name an empty or absent directory with --to=<dir>.");
|
|
2278
|
+
}
|
|
2279
|
+
function vacuumInto(source, target) {
|
|
2280
|
+
const db = new Database3(source, { readonly: true });
|
|
2281
|
+
try {
|
|
2282
|
+
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
2283
|
+
db.run("VACUUM INTO ?", [target]);
|
|
2284
|
+
} finally {
|
|
2285
|
+
db.close();
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
function copyOne(name, source, target) {
|
|
2289
|
+
if (name === SQLITE_STORE_FILE_NAME) {
|
|
2290
|
+
vacuumInto(join10(source, name), join10(target, name));
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
copyFileSync(join10(source, name), join10(target, name), constants.COPYFILE_EXCL);
|
|
2294
|
+
}
|
|
2295
|
+
function undo(target, written, madeDir) {
|
|
2296
|
+
try {
|
|
2297
|
+
if (madeDir === undefined) {
|
|
2298
|
+
for (const name of written)
|
|
2299
|
+
rmSync5(join10(target, name), { force: true });
|
|
2300
|
+
} else {
|
|
2301
|
+
rmSync5(madeDir, { recursive: true, force: true });
|
|
2302
|
+
}
|
|
2303
|
+
return "";
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
return ` Removing what it made failed too: ${String(error)}`;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
function copyEffortStore(request) {
|
|
2309
|
+
const source = resolve3(request.source);
|
|
2310
|
+
const target = resolve3(request.target);
|
|
2311
|
+
guardTestProcess(source);
|
|
2312
|
+
guardTestProcess(target);
|
|
2313
|
+
const files = EFFORT_COPY_FILE_NAMES.filter((name) => existsSync6(join10(source, name)));
|
|
2314
|
+
if (files.length === 0) {
|
|
2315
|
+
throw new EffortCopyRefusal(`no effort store at ${source}: it holds none of ${EFFORT_COPY_FILE_NAMES.join(", ")}. Nothing to copy.`);
|
|
2316
|
+
}
|
|
2317
|
+
refuseUsedTarget(target);
|
|
2318
|
+
const written = [];
|
|
2319
|
+
let madeDir;
|
|
2320
|
+
try {
|
|
2321
|
+
madeDir = mkdirSync6(target, { recursive: true });
|
|
2322
|
+
for (const name of files) {
|
|
2323
|
+
written.push(name);
|
|
2324
|
+
copyOne(name, source, target);
|
|
2325
|
+
}
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
const left = undo(target, written, madeDir);
|
|
2328
|
+
throw new EffortCopyFailure(`copying ${source} to ${target} failed: ${String(error)}.${left}`);
|
|
2329
|
+
}
|
|
2330
|
+
return Object.freeze({ source, directory: target, files: Object.freeze(files) });
|
|
2331
|
+
}
|
|
1998
2332
|
|
|
2333
|
+
// src/effort/store/rebuild-aside.ts
|
|
1999
2334
|
class RebuildRefusal extends Error {
|
|
2000
|
-
constructor(message) {
|
|
2001
|
-
super(message);
|
|
2335
|
+
constructor(message, options) {
|
|
2336
|
+
super(message, options);
|
|
2002
2337
|
this.name = "RebuildRefusal";
|
|
2003
2338
|
}
|
|
2004
2339
|
}
|
|
2005
|
-
function quoted(
|
|
2006
|
-
return `"${
|
|
2340
|
+
function quoted(name) {
|
|
2341
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
2007
2342
|
}
|
|
2008
2343
|
function count(db, sql) {
|
|
2009
2344
|
return db.query(sql).get()?.n ?? 0;
|
|
2010
2345
|
}
|
|
2011
2346
|
function refuseInFlight(path) {
|
|
2012
2347
|
for (const suffix of ["-journal", "-wal"]) {
|
|
2013
|
-
if (
|
|
2348
|
+
if (existsSync7(`${path}${suffix}`)) {
|
|
2014
2349
|
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");
|
|
2015
2350
|
}
|
|
2016
2351
|
}
|
|
@@ -2033,21 +2368,78 @@ function refuseCorrupt(db) {
|
|
|
2033
2368
|
throw new RebuildRefusal(`the rebuilt store failed integrity_check: ${answer ?? "no answer"}`);
|
|
2034
2369
|
}
|
|
2035
2370
|
function removeParallel(parallelPath) {
|
|
2036
|
-
|
|
2037
|
-
|
|
2371
|
+
rmSync6(parallelPath, { force: true });
|
|
2372
|
+
rmSync6(`${parallelPath}-journal`, { force: true });
|
|
2373
|
+
}
|
|
2374
|
+
var STEP_TEXT = {
|
|
2375
|
+
backup: ({ path, backupPath }) => ({
|
|
2376
|
+
doing: `writing ${path} out to the backup ${backupPath}`,
|
|
2377
|
+
backup: `any part of the backup written at ${backupPath}`
|
|
2378
|
+
}),
|
|
2379
|
+
carry: ({ path, parallelPath, backupPath }) => ({
|
|
2380
|
+
doing: `carrying the store id of ${path} onto ${parallelPath}`,
|
|
2381
|
+
backup: `the whole backup at ${backupPath}`
|
|
2382
|
+
}),
|
|
2383
|
+
record: ({ path, parallelPath, backupPath }) => ({
|
|
2384
|
+
doing: `writing the generation carried onto ${parallelPath} to the side record of ${path}`,
|
|
2385
|
+
backup: `the whole backup at ${backupPath}`
|
|
2386
|
+
}),
|
|
2387
|
+
rename: ({ path, parallelPath, backupPath }) => ({
|
|
2388
|
+
doing: `renaming ${parallelPath} over ${path}`,
|
|
2389
|
+
backup: `the whole backup at ${backupPath}`
|
|
2390
|
+
})
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
class SwapFailure extends RebuildRefusal {
|
|
2394
|
+
step;
|
|
2395
|
+
parallelPath;
|
|
2396
|
+
backupPath;
|
|
2397
|
+
constructor(step, files, cause) {
|
|
2398
|
+
const { doing, backup } = STEP_TEXT[step](files);
|
|
2399
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
2400
|
+
super(`${doing} failed (${reason}), so ${files.path} was not replaced and is unchanged.` + ` The rebuilt store is left at ${files.parallelPath}, and ${backup}.`, { cause });
|
|
2401
|
+
this.name = "SwapFailure";
|
|
2402
|
+
this.step = step;
|
|
2403
|
+
this.parallelPath = files.parallelPath;
|
|
2404
|
+
this.backupPath = files.backupPath;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
function writeBackup(path, backupPath) {
|
|
2408
|
+
vacuumInto(path, backupPath);
|
|
2409
|
+
const backup = openSync(backupPath, "r");
|
|
2410
|
+
try {
|
|
2411
|
+
fsyncSync(backup);
|
|
2412
|
+
} finally {
|
|
2413
|
+
closeSync(backup);
|
|
2414
|
+
}
|
|
2038
2415
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2416
|
+
var SWAP_STEPS = {
|
|
2417
|
+
backup: writeBackup,
|
|
2418
|
+
carry: (path, parallelPath) => carryStoreIdentity(path, parallelPath),
|
|
2419
|
+
record: writeStoreGeneration,
|
|
2420
|
+
rename: renameSync5
|
|
2421
|
+
};
|
|
2422
|
+
function runStep(step, files, act) {
|
|
2041
2423
|
try {
|
|
2042
|
-
|
|
2424
|
+
return act();
|
|
2043
2425
|
} catch (error) {
|
|
2044
|
-
|
|
2045
|
-
|
|
2426
|
+
throw new SwapFailure(step, files, error);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
function swapIn(path, parallelPath, backupPath, steps = {}) {
|
|
2430
|
+
const { backup, carry, record, rename } = { ...SWAP_STEPS, ...steps };
|
|
2431
|
+
const files = { path, parallelPath, backupPath };
|
|
2432
|
+
runStep("backup", files, () => backup(path, backupPath));
|
|
2433
|
+
const carried = runStep("carry", files, () => carry(path, parallelPath));
|
|
2434
|
+
if (carried.action === "carried" && carried.generation !== null) {
|
|
2435
|
+
const { generation } = carried;
|
|
2436
|
+
runStep("record", files, () => record(path, generation));
|
|
2046
2437
|
}
|
|
2438
|
+
runStep("rename", files, () => rename(parallelPath, path));
|
|
2047
2439
|
}
|
|
2048
2440
|
function refuseTaken(paths) {
|
|
2049
2441
|
for (const taken of paths) {
|
|
2050
|
-
if (
|
|
2442
|
+
if (existsSync7(taken))
|
|
2051
2443
|
throw new RebuildRefusal(`${taken} is already there; move it aside or pass another stamp`);
|
|
2052
2444
|
}
|
|
2053
2445
|
}
|
|
@@ -2086,7 +2478,7 @@ function columnNames(db, schema, table) {
|
|
|
2086
2478
|
return db.query("SELECT name FROM pragma_table_info(?, ?)").all(table, schema).map((row) => row.name);
|
|
2087
2479
|
}
|
|
2088
2480
|
function readOnlySchema(path) {
|
|
2089
|
-
const db = new
|
|
2481
|
+
const db = new Database4(path, { readonly: true });
|
|
2090
2482
|
try {
|
|
2091
2483
|
return readStoreSchema(db, path);
|
|
2092
2484
|
} finally {
|
|
@@ -2134,7 +2526,7 @@ function refuseUnusable(db, parallelPath, migrations) {
|
|
|
2134
2526
|
throw new RebuildRefusal(`the rebuilt store still needs ${needs.join(", ")}`);
|
|
2135
2527
|
}
|
|
2136
2528
|
function buildParallel(path, parallelPath, migrations, stamp) {
|
|
2137
|
-
const db = new
|
|
2529
|
+
const db = new Database4(parallelPath, { create: true, readwrite: true });
|
|
2138
2530
|
try {
|
|
2139
2531
|
bringForward(db, parallelPath, "write", "open", { migrations, ...stamp, builtAside: true });
|
|
2140
2532
|
db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
|
|
@@ -2152,8 +2544,8 @@ function buildParallel(path, parallelPath, migrations, stamp) {
|
|
|
2152
2544
|
db.close();
|
|
2153
2545
|
}
|
|
2154
2546
|
}
|
|
2155
|
-
function asidePaths(path,
|
|
2156
|
-
return { parallelPath: `${path}.fix-${stamp}`, backupPath: `${path}.v${String(
|
|
2547
|
+
function asidePaths(path, version, stamp) {
|
|
2548
|
+
return { parallelPath: `${path}.fix-${stamp}`, backupPath: `${path}.v${String(version)}-${stamp}.bak` };
|
|
2157
2549
|
}
|
|
2158
2550
|
function isRepairable(reason) {
|
|
2159
2551
|
return REPAIRABLE_REASONS.includes(reason);
|
|
@@ -2171,7 +2563,7 @@ function fixStoreSchema(options) {
|
|
|
2171
2563
|
const appliedBy = appliedByName(identity);
|
|
2172
2564
|
const known = migrations.map(({ id }) => id);
|
|
2173
2565
|
const nothing = { known, appliedBy, pending: [], kept: [], leftTables: [], leftColumns: [], backupPath: null };
|
|
2174
|
-
if (!
|
|
2566
|
+
if (!existsSync8(path)) {
|
|
2175
2567
|
return { path, status: "missing", storeVersion: null, logged: false, reason: null, refusal: null, unknown: [], ...nothing };
|
|
2176
2568
|
}
|
|
2177
2569
|
refuseInFlight(path);
|
|
@@ -2186,8 +2578,8 @@ function fixStoreSchema(options) {
|
|
|
2186
2578
|
const plan = planSchema(store, sqliteCatalogue(migrations), "write", "open");
|
|
2187
2579
|
if (plan.verdict === "use") {
|
|
2188
2580
|
const pending = writeNames(plan);
|
|
2189
|
-
const
|
|
2190
|
-
return { ...found, ...nothing, status
|
|
2581
|
+
const status = pending.length === 0 ? "current" : "behind";
|
|
2582
|
+
return { ...found, ...nothing, status, reason: null, refusal: null, pending };
|
|
2191
2583
|
}
|
|
2192
2584
|
if (!isRepairable(plan.reason)) {
|
|
2193
2585
|
throw new RebuildRefusal(`a rebuild at this rafa's migrations does not repair ${plan.reason}. ${plan.message}`);
|
|
@@ -2213,24 +2605,24 @@ function withoutWayOut(message, nextStep) {
|
|
|
2213
2605
|
}
|
|
2214
2606
|
|
|
2215
2607
|
// src/commands/plan/plan-files.ts
|
|
2216
|
-
import { statSync as
|
|
2217
|
-
import { resolve as
|
|
2608
|
+
import { statSync as statSync3 } from "fs";
|
|
2609
|
+
import { resolve as resolve4 } from "path";
|
|
2218
2610
|
|
|
2219
2611
|
// src/config-load.ts
|
|
2220
|
-
import { existsSync as
|
|
2612
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, realpathSync as realpathSync5 } from "fs";
|
|
2221
2613
|
import { isAbsolute as isAbsolute6 } from "path";
|
|
2222
|
-
function
|
|
2223
|
-
const path =
|
|
2224
|
-
if (!
|
|
2614
|
+
function readConfigFile2(root) {
|
|
2615
|
+
const path = configFilePath2(root);
|
|
2616
|
+
if (!existsSync9(path))
|
|
2225
2617
|
return null;
|
|
2226
2618
|
let text;
|
|
2227
2619
|
try {
|
|
2228
|
-
text =
|
|
2620
|
+
text = readFileSync8(path, "utf8");
|
|
2229
2621
|
} catch (error) {
|
|
2230
2622
|
const problem = `${path}: cannot be read (${messageOf(error)})`;
|
|
2231
|
-
throw new
|
|
2623
|
+
throw new ConfigError2([problem], { cause: error });
|
|
2232
2624
|
}
|
|
2233
|
-
return
|
|
2625
|
+
return parseConfigText2(text, path);
|
|
2234
2626
|
}
|
|
2235
2627
|
function isSameDirectory(first, second) {
|
|
2236
2628
|
try {
|
|
@@ -2241,19 +2633,19 @@ function isSameDirectory(first, second) {
|
|
|
2241
2633
|
}
|
|
2242
2634
|
function readUserConfigFile({ root, home }) {
|
|
2243
2635
|
if (!isAbsolute6(home)) {
|
|
2244
|
-
throw new
|
|
2636
|
+
throw new ConfigError2([
|
|
2245
2637
|
`home directory is ${describeValue(home)}, expected an absolute path`
|
|
2246
2638
|
]);
|
|
2247
2639
|
}
|
|
2248
|
-
return isSameDirectory(root, home) ? null :
|
|
2640
|
+
return isSameDirectory(root, home) ? null : readConfigFile2(home);
|
|
2249
2641
|
}
|
|
2250
2642
|
function printWarning(message) {
|
|
2251
2643
|
activeOutput().warn(message);
|
|
2252
2644
|
}
|
|
2253
|
-
function
|
|
2645
|
+
function loadConfig2(roots, cli = {}, warn = printWarning) {
|
|
2254
2646
|
const user = readUserConfigFile(roots);
|
|
2255
|
-
const file =
|
|
2256
|
-
const resolved =
|
|
2647
|
+
const file = readConfigFile2(roots.root);
|
|
2648
|
+
const resolved = resolveConfig2({ cli, file, user });
|
|
2257
2649
|
for (const warning of resolved.warnings)
|
|
2258
2650
|
warn(warning);
|
|
2259
2651
|
setActiveStoreSettings({ busyTimeoutMs: resolved.config.effortBusyTimeoutMs });
|
|
@@ -2262,7 +2654,7 @@ function loadConfig(roots, cli = {}, warn = printWarning) {
|
|
|
2262
2654
|
|
|
2263
2655
|
// src/commands/plan/plan-files.ts
|
|
2264
2656
|
function plansDirAt(root, planDir) {
|
|
2265
|
-
return { label: planDir, path:
|
|
2657
|
+
return { label: planDir, path: resolve4(root, planDir) };
|
|
2266
2658
|
}
|
|
2267
2659
|
function requireProject(context, command) {
|
|
2268
2660
|
if (context.project === null)
|
|
@@ -2274,9 +2666,9 @@ function resolvePlansDir(project, command, warn) {
|
|
|
2274
2666
|
}
|
|
2275
2667
|
function resolveProjectConfig(project, command, warn) {
|
|
2276
2668
|
try {
|
|
2277
|
-
return
|
|
2669
|
+
return loadConfig2({ root: project.root, home: project.home }, {}, warn).config;
|
|
2278
2670
|
} catch (error) {
|
|
2279
|
-
if (!(error instanceof
|
|
2671
|
+
if (!(error instanceof ConfigError2))
|
|
2280
2672
|
throw error;
|
|
2281
2673
|
throw new CommandExit(1, [
|
|
2282
2674
|
`\u274C ${command}: the config cannot be used:`,
|
|
@@ -2304,18 +2696,18 @@ function checkbox(status) {
|
|
|
2304
2696
|
function planFileName(stub, tracker) {
|
|
2305
2697
|
return tracker ? `PLAN_TRACKER-${stub}.md` : `PLAN-${stub}.md`;
|
|
2306
2698
|
}
|
|
2307
|
-
function stubOfPlanFile(
|
|
2308
|
-
const stub = /^PLAN-(.+)\.md$/.exec(
|
|
2699
|
+
function stubOfPlanFile(name) {
|
|
2700
|
+
const stub = /^PLAN-(.+)\.md$/.exec(name)?.[1];
|
|
2309
2701
|
return stub !== undefined && isStampableStub(stub) ? stub : null;
|
|
2310
2702
|
}
|
|
2311
2703
|
function isFile(path) {
|
|
2312
|
-
return
|
|
2704
|
+
return statSync3(path, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
2313
2705
|
}
|
|
2314
2706
|
function issueLine(file, issue) {
|
|
2315
2707
|
return `${file}:${issue.line}: ${issue.reason}: ${issue.text}`;
|
|
2316
2708
|
}
|
|
2317
|
-
function plural(
|
|
2318
|
-
return
|
|
2709
|
+
function plural(count, noun) {
|
|
2710
|
+
return count === 1 ? `${count} ${noun}` : `${count} ${noun}s`;
|
|
2319
2711
|
}
|
|
2320
2712
|
function expectNoArgument(args, usage) {
|
|
2321
2713
|
if (args.length === 0)
|
|
@@ -2340,12 +2732,12 @@ function expectAtMostOneArgument(args, usage) {
|
|
|
2340
2732
|
throw new CommandExit(1, `\u274C Expected at most one argument, got ${args.length}: ${args.join(" ")}
|
|
2341
2733
|
Usage: ${usage}`);
|
|
2342
2734
|
}
|
|
2343
|
-
function readSwitch(
|
|
2735
|
+
function readSwitch(name, value, hint) {
|
|
2344
2736
|
if (value === undefined || value === false || value === "false")
|
|
2345
2737
|
return false;
|
|
2346
2738
|
if (value === true || value === "true")
|
|
2347
2739
|
return true;
|
|
2348
|
-
throw new CommandExit(1, `\u274C --${
|
|
2740
|
+
throw new CommandExit(1, `\u274C --${name} takes no value, and read "${value}" as one. ${hint}`);
|
|
2349
2741
|
}
|
|
2350
2742
|
|
|
2351
2743
|
// src/commands/effort/fix-schema.ts
|
|
@@ -2355,6 +2747,9 @@ var DRY_RUN_FLAG = "dry-run";
|
|
|
2355
2747
|
function fileStamp(date) {
|
|
2356
2748
|
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
2357
2749
|
}
|
|
2750
|
+
function keepsIdLine(swapped) {
|
|
2751
|
+
return `The ${swapped} keeps the store's id; the backup is a copy, so renamed back it takes a new id on its` + " next write.";
|
|
2752
|
+
}
|
|
2358
2753
|
function refuseLiveLoop(root, isAlive) {
|
|
2359
2754
|
let records;
|
|
2360
2755
|
try {
|
|
@@ -2412,7 +2807,7 @@ function renderFixSchema(result) {
|
|
|
2412
2807
|
case "rebuilt":
|
|
2413
2808
|
return [
|
|
2414
2809
|
...rebuildLines(result),
|
|
2415
|
-
`\u2705 Rebuilt at this rafa's migrations. The original is kept whole at ${String(result.backupPath)};` +
|
|
2810
|
+
`\u2705 Rebuilt at this rafa's migrations. The original is kept whole at ${String(result.backupPath)};` + ` rename it back to undo. ${keepsIdLine("rebuild")}`
|
|
2416
2811
|
];
|
|
2417
2812
|
}
|
|
2418
2813
|
}
|
|
@@ -2452,7 +2847,7 @@ function createFixSchemaCommand(seams = {}) {
|
|
|
2452
2847
|
subject: "effort",
|
|
2453
2848
|
action: "fix-schema",
|
|
2454
2849
|
summary: "rebuild an effort store this rafa refuses at the migrations it knows, keeping the original",
|
|
2455
|
-
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
|
|
2850
|
+
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 writes the original to `effort.sqlite.v<user_version>-<stamp>.bak`," + " whole, and moves the rebuild into its place. The rebuild keeps the store's id; the backup is a copy," + " so renamed back it takes a new id on its next write. 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.",
|
|
2456
2851
|
args: [],
|
|
2457
2852
|
flags: [
|
|
2458
2853
|
{
|
|
@@ -2481,101 +2876,12 @@ function createFixSchemaCommand(seams = {}) {
|
|
|
2481
2876
|
}
|
|
2482
2877
|
var fix_schema_default = createFixSchemaCommand();
|
|
2483
2878
|
|
|
2484
|
-
// src/effort/store/copy.ts
|
|
2485
|
-
import { copyFileSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync2, rmSync as rmSync5, statSync as statSync3 } from "fs";
|
|
2486
|
-
import { join as join10, resolve as resolve4 } from "path";
|
|
2487
|
-
import { Database as Database3 } from "bun:sqlite";
|
|
2488
|
-
var NDJSON_KINDS = { sessions: true, commits: true };
|
|
2489
|
-
var EFFORT_COPY_FILE_NAMES = [
|
|
2490
|
-
SQLITE_STORE_FILE_NAME,
|
|
2491
|
-
...Object.keys(NDJSON_KINDS).map((kind) => effortStoreFileName(kind))
|
|
2492
|
-
];
|
|
2493
|
-
|
|
2494
|
-
class EffortCopyRefusal extends Error {
|
|
2495
|
-
name = "EffortCopyRefusal";
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
class EffortCopyFailure extends Error {
|
|
2499
|
-
name = "EffortCopyFailure";
|
|
2500
|
-
}
|
|
2501
|
-
function refuseUsedTarget(target) {
|
|
2502
|
-
if (!existsSync9(target))
|
|
2503
|
-
return;
|
|
2504
|
-
let entries;
|
|
2505
|
-
try {
|
|
2506
|
-
if (!statSync3(target).isDirectory()) {
|
|
2507
|
-
throw new EffortCopyRefusal(`${target} is a file, not a directory; name an empty or absent directory with --to=<dir>.`);
|
|
2508
|
-
}
|
|
2509
|
-
entries = readdirSync2(target);
|
|
2510
|
-
} catch (error) {
|
|
2511
|
-
if (error instanceof EffortCopyRefusal)
|
|
2512
|
-
throw error;
|
|
2513
|
-
throw new EffortCopyFailure(`${target} cannot be read: ${String(error)}`);
|
|
2514
|
-
}
|
|
2515
|
-
if (entries.length === 0)
|
|
2516
|
-
return;
|
|
2517
|
-
throw new EffortCopyRefusal(`${target} is not empty (${String(entries.length)} entries); nothing was copied.` + " Name an empty or absent directory with --to=<dir>.");
|
|
2518
|
-
}
|
|
2519
|
-
function vacuumInto(source, target) {
|
|
2520
|
-
const db = new Database3(source, { readonly: true });
|
|
2521
|
-
try {
|
|
2522
|
-
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
2523
|
-
db.run("VACUUM INTO ?", [target]);
|
|
2524
|
-
} finally {
|
|
2525
|
-
db.close();
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
function copyOne(name2, source, target) {
|
|
2529
|
-
if (name2 === SQLITE_STORE_FILE_NAME) {
|
|
2530
|
-
vacuumInto(join10(source, name2), join10(target, name2));
|
|
2531
|
-
return;
|
|
2532
|
-
}
|
|
2533
|
-
copyFileSync(join10(source, name2), join10(target, name2), constants.COPYFILE_EXCL);
|
|
2534
|
-
}
|
|
2535
|
-
function undo(target, written, madeDir) {
|
|
2536
|
-
try {
|
|
2537
|
-
if (madeDir === undefined) {
|
|
2538
|
-
for (const name2 of written)
|
|
2539
|
-
rmSync5(join10(target, name2), { force: true });
|
|
2540
|
-
} else {
|
|
2541
|
-
rmSync5(madeDir, { recursive: true, force: true });
|
|
2542
|
-
}
|
|
2543
|
-
return "";
|
|
2544
|
-
} catch (error) {
|
|
2545
|
-
return ` Removing what it made failed too: ${String(error)}`;
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
function copyEffortStore(request) {
|
|
2549
|
-
const source = resolve4(request.source);
|
|
2550
|
-
const target = resolve4(request.target);
|
|
2551
|
-
guardTestProcess(source);
|
|
2552
|
-
guardTestProcess(target);
|
|
2553
|
-
const files = EFFORT_COPY_FILE_NAMES.filter((name2) => existsSync9(join10(source, name2)));
|
|
2554
|
-
if (files.length === 0) {
|
|
2555
|
-
throw new EffortCopyRefusal(`no effort store at ${source}: it holds none of ${EFFORT_COPY_FILE_NAMES.join(", ")}. Nothing to copy.`);
|
|
2556
|
-
}
|
|
2557
|
-
refuseUsedTarget(target);
|
|
2558
|
-
const written = [];
|
|
2559
|
-
let madeDir;
|
|
2560
|
-
try {
|
|
2561
|
-
madeDir = mkdirSync6(target, { recursive: true });
|
|
2562
|
-
for (const name2 of files) {
|
|
2563
|
-
written.push(name2);
|
|
2564
|
-
copyOne(name2, source, target);
|
|
2565
|
-
}
|
|
2566
|
-
} catch (error) {
|
|
2567
|
-
const left = undo(target, written, madeDir);
|
|
2568
|
-
throw new EffortCopyFailure(`copying ${source} to ${target} failed: ${String(error)}.${left}`);
|
|
2569
|
-
}
|
|
2570
|
-
return Object.freeze({ source, directory: target, files: Object.freeze(files) });
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
2879
|
// src/effort/store/merge-store.ts
|
|
2574
|
-
import { randomUUID as
|
|
2575
|
-
import { existsSync as existsSync11, mkdtempSync, rmSync as
|
|
2880
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2881
|
+
import { existsSync as existsSync11, mkdtempSync, rmSync as rmSync7 } from "fs";
|
|
2576
2882
|
import { tmpdir as tmpdir3 } from "os";
|
|
2577
2883
|
import { dirname as dirname11, join as join11 } from "path";
|
|
2578
|
-
import { Database as
|
|
2884
|
+
import { Database as Database6, SQLiteError as SQLiteError2 } from "bun:sqlite";
|
|
2579
2885
|
|
|
2580
2886
|
// src/effort/commits.ts
|
|
2581
2887
|
import { execFileSync } from "child_process";
|
|
@@ -2593,8 +2899,8 @@ function normaliseBranch(source) {
|
|
|
2593
2899
|
return null;
|
|
2594
2900
|
return source.startsWith("refs/heads/") ? source.slice("refs/heads/".length) : source;
|
|
2595
2901
|
}
|
|
2596
|
-
function parseStatCount(
|
|
2597
|
-
return /^\d+$/.test(
|
|
2902
|
+
function parseStatCount(field) {
|
|
2903
|
+
return /^\d+$/.test(field) ? Number(field) : null;
|
|
2598
2904
|
}
|
|
2599
2905
|
function parseCommitHeader(line) {
|
|
2600
2906
|
const parts = line.split(FIELD_SEPARATOR);
|
|
@@ -2822,7 +3128,7 @@ function mergedTables() {
|
|
|
2822
3128
|
return Object.entries(MERGE_RULES).flatMap(([table, rule]) => rule.scope === "merged" ? [{ table, identity: rule.identity }] : []);
|
|
2823
3129
|
}
|
|
2824
3130
|
function carriedColumns(db, table) {
|
|
2825
|
-
return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name
|
|
3131
|
+
return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name }) => name).filter((name) => name !== "seq");
|
|
2826
3132
|
}
|
|
2827
3133
|
function refuseColumnMismatch(local, other, tables) {
|
|
2828
3134
|
const faults = tables.flatMap(({ table }) => {
|
|
@@ -2913,29 +3219,29 @@ function sameValue(left, right) {
|
|
|
2913
3219
|
return Buffer.from(left).equals(right);
|
|
2914
3220
|
return (left ?? null) === (right ?? null);
|
|
2915
3221
|
}
|
|
2916
|
-
function fillStatement(table,
|
|
2917
|
-
const statement = SET_ONCE_FILLS[`${table}.${
|
|
3222
|
+
function fillStatement(table, field) {
|
|
3223
|
+
const statement = SET_ONCE_FILLS[`${table}.${field}`];
|
|
2918
3224
|
if (statement === undefined) {
|
|
2919
|
-
throw new Error(`${table}.${
|
|
3225
|
+
throw new Error(`${table}.${field} is set-once in MERGE_RULES and has no statement in SET_ONCE_FILLS`);
|
|
2920
3226
|
}
|
|
2921
3227
|
return statement;
|
|
2922
3228
|
}
|
|
2923
|
-
function settleSetOnce(local, table,
|
|
2924
|
-
const incoming = there[
|
|
3229
|
+
function settleSetOnce(local, table, field, here, there) {
|
|
3230
|
+
const incoming = there[field] ?? null;
|
|
2925
3231
|
if (incoming === null)
|
|
2926
3232
|
return { skipped: true, filled: [], conflictFields: [] };
|
|
2927
|
-
if ((here[
|
|
2928
|
-
return { skipped: false, filled: [], conflictFields: [
|
|
2929
|
-
local.query(fillStatement(table,
|
|
2930
|
-
return { skipped: true, filled: [
|
|
3233
|
+
if ((here[field] ?? null) !== null)
|
|
3234
|
+
return { skipped: false, filled: [], conflictFields: [field] };
|
|
3235
|
+
local.query(fillStatement(table, field)).run(incoming, Number(here.seq));
|
|
3236
|
+
return { skipped: true, filled: [field], conflictFields: [] };
|
|
2931
3237
|
}
|
|
2932
|
-
function settleRecomputed(table,
|
|
2933
|
-
const comparable = RECOMPUTED_COMPARISONS[`${table}.${
|
|
3238
|
+
function settleRecomputed(table, field, here, there) {
|
|
3239
|
+
const comparable = RECOMPUTED_COMPARISONS[`${table}.${field}`];
|
|
2934
3240
|
if (comparable === undefined) {
|
|
2935
|
-
throw new Error(`${table}.${
|
|
3241
|
+
throw new Error(`${table}.${field} is recomputed in MERGE_RULES and has no entry in RECOMPUTED_COMPARISONS`);
|
|
2936
3242
|
}
|
|
2937
|
-
const hereValue = here[
|
|
2938
|
-
const thereValue = there[
|
|
3243
|
+
const hereValue = here[field];
|
|
3244
|
+
const thereValue = there[field];
|
|
2939
3245
|
const same = typeof hereValue === "string" && typeof thereValue === "string" && comparable(hereValue) === comparable(thereValue);
|
|
2940
3246
|
return same ? { skipped: true, filled: [], conflictFields: [] } : { skipped: false, filled: [], conflictFields: [null] };
|
|
2941
3247
|
}
|
|
@@ -2945,12 +3251,12 @@ function settlePair(local, table, edited, here, there) {
|
|
|
2945
3251
|
return { skipped: true, filled: [], conflictFields: [] };
|
|
2946
3252
|
if (differing.some((column) => !Object.hasOwn(edited, column)))
|
|
2947
3253
|
return { skipped: false, filled: [], conflictFields: [null] };
|
|
2948
|
-
const outcomes = differing.map((
|
|
2949
|
-
if (edited[
|
|
2950
|
-
return settleSetOnce(local, table,
|
|
2951
|
-
if (edited[
|
|
2952
|
-
return settleRecomputed(table,
|
|
2953
|
-
throw new Error(`${table}.${
|
|
3254
|
+
const outcomes = differing.map((field) => {
|
|
3255
|
+
if (edited[field] === SET_ONCE)
|
|
3256
|
+
return settleSetOnce(local, table, field, here, there);
|
|
3257
|
+
if (edited[field] === RECOMPUTED)
|
|
3258
|
+
return settleRecomputed(table, field, here, there);
|
|
3259
|
+
throw new Error(`${table}.${field} has the rule ${edited[field]}, which the merge does not apply`);
|
|
2954
3260
|
});
|
|
2955
3261
|
return {
|
|
2956
3262
|
skipped: outcomes.every((outcome) => outcome.skipped),
|
|
@@ -2967,16 +3273,16 @@ function settleTable(local, other, union, trail) {
|
|
|
2967
3273
|
const here = rowAt(local, union.table, columns, match.localSeq, "here");
|
|
2968
3274
|
const there = rowAt(other, union.table, columns, match.incomingSeq, "in the other store");
|
|
2969
3275
|
const outcome = settlePair(local, union.table, rule.edited, here, there);
|
|
2970
|
-
const conflicts = outcome.conflictFields.map((
|
|
2971
|
-
local.query(INSERT_CONFLICT).run(trail.mergeId, union.table, match.localSeq,
|
|
2972
|
-
return { localSeq: match.localSeq, incomingSeq: match.incomingSeq, field
|
|
3276
|
+
const conflicts = outcome.conflictFields.map((field) => {
|
|
3277
|
+
local.query(INSERT_CONFLICT).run(trail.mergeId, union.table, match.localSeq, field, JSON.stringify(there), trail.recordedAt);
|
|
3278
|
+
return { localSeq: match.localSeq, incomingSeq: match.incomingSeq, field };
|
|
2973
3279
|
});
|
|
2974
3280
|
return { match, outcome, conflicts };
|
|
2975
3281
|
});
|
|
2976
3282
|
return {
|
|
2977
3283
|
table: union.table,
|
|
2978
3284
|
skipped: settled.flatMap(({ match, outcome }) => outcome.skipped ? [match.localSeq] : []),
|
|
2979
|
-
filled: settled.flatMap(({ match, outcome }) => outcome.filled.map((
|
|
3285
|
+
filled: settled.flatMap(({ match, outcome }) => outcome.filled.map((field) => ({ localSeq: match.localSeq, field }))),
|
|
2980
3286
|
conflicts: settled.flatMap(({ conflicts }) => conflicts)
|
|
2981
3287
|
};
|
|
2982
3288
|
}
|
|
@@ -2986,7 +3292,7 @@ function settleMatches(local, other, unions, trail) {
|
|
|
2986
3292
|
|
|
2987
3293
|
// src/effort/store/migrate.ts
|
|
2988
3294
|
import { existsSync as existsSync10 } from "fs";
|
|
2989
|
-
import { Database as
|
|
3295
|
+
import { Database as Database5 } from "bun:sqlite";
|
|
2990
3296
|
var LIVE2 = "live";
|
|
2991
3297
|
function asidePaths2(path, first, stamp) {
|
|
2992
3298
|
return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
|
|
@@ -3014,7 +3320,7 @@ function refuseLiveLoop2(path, breaking, options) {
|
|
|
3014
3320
|
}
|
|
3015
3321
|
function buildMigrated(path, parallelPath, migrations, stamp) {
|
|
3016
3322
|
vacuumInto(path, parallelPath);
|
|
3017
|
-
const db = new
|
|
3323
|
+
const db = new Database5(parallelPath, { readwrite: true });
|
|
3018
3324
|
try {
|
|
3019
3325
|
const brought = bringForward(db, parallelPath, "write", "migrate", { migrations, ...stamp, builtAside: true });
|
|
3020
3326
|
db.run(`ATTACH DATABASE ? AS ${LIVE2}`, [path]);
|
|
@@ -3098,7 +3404,7 @@ function refuseMissing(path, which) {
|
|
|
3098
3404
|
function refuseDamaged(path) {
|
|
3099
3405
|
let answers;
|
|
3100
3406
|
try {
|
|
3101
|
-
const db = new
|
|
3407
|
+
const db = new Database6(path, { readonly: true });
|
|
3102
3408
|
try {
|
|
3103
3409
|
answers = db.query("PRAGMA integrity_check").all().map((row) => row.integrity_check);
|
|
3104
3410
|
} finally {
|
|
@@ -3120,7 +3426,7 @@ function writablePlan(path, migrations) {
|
|
|
3120
3426
|
return plan;
|
|
3121
3427
|
}
|
|
3122
3428
|
function recordedMeta(path) {
|
|
3123
|
-
const db = new
|
|
3429
|
+
const db = new Database6(path, { readonly: true });
|
|
3124
3430
|
try {
|
|
3125
3431
|
const hasTable = db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
|
|
3126
3432
|
return hasTable ? readStoreMeta(db) : null;
|
|
@@ -3146,18 +3452,18 @@ function refuseLiveLoop3(path, options, sessionId) {
|
|
|
3146
3452
|
function openOther(otherPath, plan, stamp) {
|
|
3147
3453
|
const needs = writeNames(plan);
|
|
3148
3454
|
if (needs.length === 0) {
|
|
3149
|
-
const db = new
|
|
3455
|
+
const db = new Database6(otherPath, { readonly: true });
|
|
3150
3456
|
return { db, broughtForward: [], release: () => db.close() };
|
|
3151
3457
|
}
|
|
3152
3458
|
const dir = mkdtempSync(join11(tmpdir3(), "rafa-merge-other-"));
|
|
3153
3459
|
const release = (db) => {
|
|
3154
3460
|
db?.close();
|
|
3155
|
-
|
|
3461
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
3156
3462
|
};
|
|
3157
3463
|
try {
|
|
3158
3464
|
const copyPath = join11(dir, "other.sqlite");
|
|
3159
3465
|
vacuumInto(otherPath, copyPath);
|
|
3160
|
-
const db = new
|
|
3466
|
+
const db = new Database6(copyPath, { readwrite: true });
|
|
3161
3467
|
try {
|
|
3162
3468
|
bringForward(db, copyPath, "write", "open", { ...stamp, builtAside: true });
|
|
3163
3469
|
} catch (error) {
|
|
@@ -3186,7 +3492,7 @@ function mergeInto(db, other, mergeId, otherStore, mergedAt) {
|
|
|
3186
3492
|
collided: union.collided.length
|
|
3187
3493
|
};
|
|
3188
3494
|
});
|
|
3189
|
-
const sum = (
|
|
3495
|
+
const sum = (field) => tables.reduce((total, entry) => total + entry[field], 0);
|
|
3190
3496
|
const totals = { rowsAdded: sum("added"), rowsSkipped: sum("skipped"), rowsInConflict: sum("inConflict") };
|
|
3191
3497
|
db.query(INSERT_MERGE).run(mergeId, otherStore, mergedAt, totals.rowsAdded, totals.rowsSkipped, totals.rowsInConflict);
|
|
3192
3498
|
return { tables, ...totals, gapsRewritten: gaps.length };
|
|
@@ -3199,18 +3505,18 @@ function addedRows(merged) {
|
|
|
3199
3505
|
merge_conflicts: merged.rowsInConflict
|
|
3200
3506
|
};
|
|
3201
3507
|
}
|
|
3202
|
-
function buildMerged(path, parallelPath, other,
|
|
3508
|
+
function buildMerged(path, parallelPath, other, merge) {
|
|
3203
3509
|
vacuumInto(path, parallelPath);
|
|
3204
|
-
const db = new
|
|
3510
|
+
const db = new Database6(parallelPath, { readwrite: true });
|
|
3205
3511
|
try {
|
|
3206
|
-
bringForward(db, parallelPath, "write", "open", { ...
|
|
3207
|
-
const merged = mergeInto(db, other,
|
|
3512
|
+
bringForward(db, parallelPath, "write", "open", { ...merge.stamp, builtAside: true });
|
|
3513
|
+
const merged = mergeInto(db, other, merge.id, merge.otherStore, merge.stamp.now().toISOString());
|
|
3208
3514
|
db.run(`ATTACH DATABASE ? AS ${LIVE3}`, [path]);
|
|
3209
3515
|
const live = new Set(tableNames(db, LIVE3));
|
|
3210
3516
|
checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE3, addedRows(merged));
|
|
3211
3517
|
db.run(`DETACH DATABASE ${LIVE3}`);
|
|
3212
3518
|
refuseCorrupt(db);
|
|
3213
|
-
refuseUnusable(db, parallelPath,
|
|
3519
|
+
refuseUnusable(db, parallelPath, merge.stamp.migrations);
|
|
3214
3520
|
return merged;
|
|
3215
3521
|
} finally {
|
|
3216
3522
|
db.close();
|
|
@@ -3233,7 +3539,7 @@ function refuseUnmergeable(options, migrations) {
|
|
|
3233
3539
|
}
|
|
3234
3540
|
return { otherPlan, otherStore };
|
|
3235
3541
|
}
|
|
3236
|
-
function
|
|
3542
|
+
function mergeStore2(options) {
|
|
3237
3543
|
const { path, otherPath, dryRun, stamp } = options;
|
|
3238
3544
|
guardTestProcess(path);
|
|
3239
3545
|
guardTestProcess(otherPath);
|
|
@@ -3244,7 +3550,7 @@ function mergeStore(options) {
|
|
|
3244
3550
|
appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
|
|
3245
3551
|
now: options.now ?? (() => new Date)
|
|
3246
3552
|
};
|
|
3247
|
-
const mergeId = (options.newMergeId ??
|
|
3553
|
+
const mergeId = (options.newMergeId ?? randomUUID5)();
|
|
3248
3554
|
const other = openOther(otherPath, otherPlan, mergeStamp);
|
|
3249
3555
|
try {
|
|
3250
3556
|
const { built, backupPath } = rebuildAside({
|
|
@@ -3268,10 +3574,10 @@ var NOTHING_COPIED = "Nothing was copied.";
|
|
|
3268
3574
|
class FileSyncRefusal extends Error {
|
|
3269
3575
|
name = "FileSyncRefusal";
|
|
3270
3576
|
}
|
|
3271
|
-
function requirePath(
|
|
3577
|
+
function requirePath(field, value) {
|
|
3272
3578
|
if (typeof value === "string" && value !== "")
|
|
3273
3579
|
return value;
|
|
3274
|
-
throw new TypeError(`${REFUSAL}: ${
|
|
3580
|
+
throw new TypeError(`${REFUSAL}: ${field} is ${describeValue(value)}, expected a path`);
|
|
3275
3581
|
}
|
|
3276
3582
|
function refuseUncarriable(backend, source) {
|
|
3277
3583
|
if (backend === "ndjson") {
|
|
@@ -3301,8 +3607,8 @@ function createFileSync(options) {
|
|
|
3301
3607
|
const otherPath = resolve5(repoRoot, requirePath("from", from));
|
|
3302
3608
|
const path = join12(storeDir(), SQLITE_STORE_FILE_NAME);
|
|
3303
3609
|
const stamp = fileStamp(now());
|
|
3304
|
-
const
|
|
3305
|
-
return Object.freeze({ status: "pulled", merge
|
|
3610
|
+
const merge = mergeStore2({ ...mergeSeams(options), path, otherPath, backend, dryRun, stamp, now, sessionId });
|
|
3611
|
+
return Object.freeze({ status: "pulled", merge });
|
|
3306
3612
|
};
|
|
3307
3613
|
return Object.freeze({ kind: "file", push, pull });
|
|
3308
3614
|
}
|
|
@@ -3353,7 +3659,7 @@ function contextOf(repoRoot, config) {
|
|
|
3353
3659
|
...hub === undefined ? {} : { hub }
|
|
3354
3660
|
};
|
|
3355
3661
|
}
|
|
3356
|
-
function selectSync(repoRoot, config, registry =
|
|
3662
|
+
function selectSync(repoRoot, config, registry = CORE_ADAPTER_REGISTRY2) {
|
|
3357
3663
|
const kind = config.effortSync;
|
|
3358
3664
|
const adapter = typeof kind === "string" ? registry.find("sync", kind) : undefined;
|
|
3359
3665
|
if (adapter !== undefined)
|
|
@@ -3361,8 +3667,8 @@ function selectSync(repoRoot, config, registry = CORE_ADAPTER_REGISTRY) {
|
|
|
3361
3667
|
const registered = registry.kinds("sync");
|
|
3362
3668
|
if (isModuleStrategy(kind))
|
|
3363
3669
|
throw new SyncModuleMissing(kind, registered);
|
|
3364
|
-
const known =
|
|
3365
|
-
const expected = known ? `registered: ${listed(registered)}` : `expected one of: ${
|
|
3670
|
+
const known = SYNC_STRATEGIES2.includes(kind);
|
|
3671
|
+
const expected = known ? `registered: ${listed(registered)}` : `expected one of: ${SYNC_STRATEGIES2.join(", ")}`;
|
|
3366
3672
|
throw new TypeError(`${REFUSAL2}: effort.sync is ${describeValue(kind)}, and no sync adapter is registered under it; ${expected}`);
|
|
3367
3673
|
}
|
|
3368
3674
|
|
|
@@ -3371,13 +3677,13 @@ import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
|
3371
3677
|
import { join as join15 } from "path";
|
|
3372
3678
|
|
|
3373
3679
|
// src/commands/instinct/instinct-records.ts
|
|
3374
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
3680
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
|
|
3375
3681
|
import { join as join14 } from "path";
|
|
3376
3682
|
|
|
3377
3683
|
// src/check/layout.ts
|
|
3378
3684
|
import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
3379
3685
|
import { join as join13, relative as relative2 } from "path";
|
|
3380
|
-
var
|
|
3686
|
+
var SKILL_FILE2 = "SKILL.md";
|
|
3381
3687
|
var MARKDOWN_EXTENSION = ".md";
|
|
3382
3688
|
var LAYOUT_SEVERITY = Object.freeze({
|
|
3383
3689
|
"flat-grouped-layout": "failure",
|
|
@@ -3405,13 +3711,13 @@ function entryKind(path) {
|
|
|
3405
3711
|
}
|
|
3406
3712
|
}
|
|
3407
3713
|
function visibleNames(dir) {
|
|
3408
|
-
return readdirSync3(dir).filter((
|
|
3714
|
+
return readdirSync3(dir).filter((name) => !name.startsWith(".")).sort((left, right) => left.localeCompare(right));
|
|
3409
3715
|
}
|
|
3410
|
-
function isMarkdown(
|
|
3411
|
-
return
|
|
3716
|
+
function isMarkdown(name) {
|
|
3717
|
+
return name.endsWith(MARKDOWN_EXTENSION);
|
|
3412
3718
|
}
|
|
3413
|
-
function markdownStem(
|
|
3414
|
-
return
|
|
3719
|
+
function markdownStem(name) {
|
|
3720
|
+
return name.slice(0, -MARKDOWN_EXTENSION.length);
|
|
3415
3721
|
}
|
|
3416
3722
|
function shown(root, path) {
|
|
3417
3723
|
return relative2(root, path) || ".";
|
|
@@ -3436,37 +3742,37 @@ function unreadableEntry(kind, root, path) {
|
|
|
3436
3742
|
issues: [layoutIssue("unreadable-entry", `${shown(root, path)} cannot be read (a broken symlink, or a path stat refuses)`)]
|
|
3437
3743
|
};
|
|
3438
3744
|
}
|
|
3439
|
-
function skillFileEntry(path,
|
|
3440
|
-
return { kind: "skill", path, isFile: true, shape: "skill-file", name
|
|
3745
|
+
function skillFileEntry(path, name) {
|
|
3746
|
+
return { kind: "skill", path, isFile: true, shape: "skill-file", name, issues: [] };
|
|
3441
3747
|
}
|
|
3442
|
-
function flatGroupedEntry(root, path,
|
|
3748
|
+
function flatGroupedEntry(root, path, name) {
|
|
3443
3749
|
return {
|
|
3444
3750
|
kind: "skill",
|
|
3445
3751
|
path,
|
|
3446
3752
|
isFile: true,
|
|
3447
3753
|
shape: "flat-grouped",
|
|
3448
|
-
name
|
|
3449
|
-
issues: [layoutIssue("flat-grouped-layout", `${shown(root, path)} is the flat grouped layout <dir>/<group>/<name>.md,` + ` which does not register; move it to <dir>/${
|
|
3754
|
+
name,
|
|
3755
|
+
issues: [layoutIssue("flat-grouped-layout", `${shown(root, path)} is the flat grouped layout <dir>/<group>/<name>.md,` + ` which does not register; move it to <dir>/${name}/${SKILL_FILE2}`)]
|
|
3450
3756
|
};
|
|
3451
3757
|
}
|
|
3452
|
-
function nestedGroupEntry(root, path,
|
|
3758
|
+
function nestedGroupEntry(root, path, name) {
|
|
3453
3759
|
return {
|
|
3454
3760
|
kind: "skill",
|
|
3455
3761
|
path,
|
|
3456
3762
|
isFile: true,
|
|
3457
3763
|
shape: "nested-group",
|
|
3458
|
-
name
|
|
3459
|
-
issues: [layoutIssue("nested-group-layout", `${shown(root, path)} is the nested layout <dir>/<group>/<name>/${
|
|
3764
|
+
name,
|
|
3765
|
+
issues: [layoutIssue("nested-group-layout", `${shown(root, path)} is the nested layout <dir>/<group>/<name>/${SKILL_FILE2},` + ` which does not register; move it to <dir>/${name}/${SKILL_FILE2}`)]
|
|
3460
3766
|
};
|
|
3461
3767
|
}
|
|
3462
|
-
function looseFileEntry(root, path,
|
|
3768
|
+
function looseFileEntry(root, path, name) {
|
|
3463
3769
|
return {
|
|
3464
3770
|
kind: "skill",
|
|
3465
3771
|
path,
|
|
3466
3772
|
isFile: true,
|
|
3467
3773
|
shape: "loose-file",
|
|
3468
|
-
name
|
|
3469
|
-
issues: [layoutIssue("loose-file", `${shown(root, path)} sits directly under the skills directory, which registers` + ` only <dir>/<name>/${
|
|
3774
|
+
name,
|
|
3775
|
+
issues: [layoutIssue("loose-file", `${shown(root, path)} sits directly under the skills directory, which registers` + ` only <dir>/<name>/${SKILL_FILE2}; move it to <dir>/${name}/${SKILL_FILE2}`)]
|
|
3470
3776
|
};
|
|
3471
3777
|
}
|
|
3472
3778
|
function noSkillFileEntry(root, dir) {
|
|
@@ -3476,26 +3782,26 @@ function noSkillFileEntry(root, dir) {
|
|
|
3476
3782
|
isFile: false,
|
|
3477
3783
|
shape: "no-skill-file",
|
|
3478
3784
|
name: null,
|
|
3479
|
-
issues: [layoutIssue("missing-skill-file", `${shown(root, dir)}/ holds no ${
|
|
3785
|
+
issues: [layoutIssue("missing-skill-file", `${shown(root, dir)}/ holds no ${SKILL_FILE2}, so it registers no skill`)]
|
|
3480
3786
|
};
|
|
3481
3787
|
}
|
|
3482
|
-
function nestedInstinctEntry(root, path,
|
|
3788
|
+
function nestedInstinctEntry(root, path, name) {
|
|
3483
3789
|
return {
|
|
3484
3790
|
kind: "instinct",
|
|
3485
3791
|
path,
|
|
3486
3792
|
isFile: true,
|
|
3487
3793
|
shape: "nested-instinct",
|
|
3488
|
-
name
|
|
3489
|
-
issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${
|
|
3794
|
+
name,
|
|
3795
|
+
issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${name}${MARKDOWN_EXTENSION}`)]
|
|
3490
3796
|
};
|
|
3491
3797
|
}
|
|
3492
|
-
function scanGroupMember(root, dir,
|
|
3798
|
+
function scanGroupMember(root, dir, name) {
|
|
3493
3799
|
const children = visibleNames(dir);
|
|
3494
3800
|
if (children.length === 0)
|
|
3495
3801
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
3496
|
-
const skillPath = join13(dir,
|
|
3497
|
-
if (children.includes(
|
|
3498
|
-
return [nestedGroupEntry(root, skillPath,
|
|
3802
|
+
const skillPath = join13(dir, SKILL_FILE2);
|
|
3803
|
+
if (children.includes(SKILL_FILE2) && entryKind(skillPath) === "file") {
|
|
3804
|
+
return [nestedGroupEntry(root, skillPath, name)];
|
|
3499
3805
|
}
|
|
3500
3806
|
return [noSkillFileEntry(root, dir)];
|
|
3501
3807
|
}
|
|
@@ -3519,31 +3825,31 @@ function scanSkillGroup(root, dir, children) {
|
|
|
3519
3825
|
return [noSkillFileEntry(root, dir)];
|
|
3520
3826
|
return entries;
|
|
3521
3827
|
}
|
|
3522
|
-
function scanSkillDirectory(root, dir,
|
|
3828
|
+
function scanSkillDirectory(root, dir, name) {
|
|
3523
3829
|
const children = visibleNames(dir);
|
|
3524
3830
|
if (children.length === 0)
|
|
3525
3831
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
3526
|
-
const skillPath = join13(dir,
|
|
3527
|
-
if (children.includes(
|
|
3528
|
-
return [skillFileEntry(skillPath,
|
|
3832
|
+
const skillPath = join13(dir, SKILL_FILE2);
|
|
3833
|
+
if (children.includes(SKILL_FILE2) && entryKind(skillPath) === "file") {
|
|
3834
|
+
return [skillFileEntry(skillPath, name)];
|
|
3529
3835
|
}
|
|
3530
3836
|
return scanSkillGroup(root, dir, children);
|
|
3531
3837
|
}
|
|
3532
3838
|
function scanSkillRoot(root) {
|
|
3533
3839
|
const entries = [];
|
|
3534
|
-
for (const
|
|
3535
|
-
const path = join13(root,
|
|
3840
|
+
for (const name of visibleNames(root)) {
|
|
3841
|
+
const path = join13(root, name);
|
|
3536
3842
|
const found = entryKind(path);
|
|
3537
3843
|
if (found === null) {
|
|
3538
3844
|
entries.push(unreadableEntry("skill", root, path));
|
|
3539
3845
|
continue;
|
|
3540
3846
|
}
|
|
3541
3847
|
if (found === "directory") {
|
|
3542
|
-
entries.push(...scanSkillDirectory(root, path,
|
|
3848
|
+
entries.push(...scanSkillDirectory(root, path, name));
|
|
3543
3849
|
continue;
|
|
3544
3850
|
}
|
|
3545
|
-
if (isMarkdown(
|
|
3546
|
-
entries.push(looseFileEntry(root, path, markdownStem(
|
|
3851
|
+
if (isMarkdown(name))
|
|
3852
|
+
entries.push(looseFileEntry(root, path, markdownStem(name)));
|
|
3547
3853
|
}
|
|
3548
3854
|
return entries;
|
|
3549
3855
|
}
|
|
@@ -3570,8 +3876,8 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
3570
3876
|
}
|
|
3571
3877
|
function scanInstinctRoot(root) {
|
|
3572
3878
|
const entries = [];
|
|
3573
|
-
for (const
|
|
3574
|
-
const path = join13(root,
|
|
3879
|
+
for (const name of visibleNames(root)) {
|
|
3880
|
+
const path = join13(root, name);
|
|
3575
3881
|
const found = entryKind(path);
|
|
3576
3882
|
if (found === null) {
|
|
3577
3883
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -3581,14 +3887,14 @@ function scanInstinctRoot(root) {
|
|
|
3581
3887
|
entries.push(...scanInstinctSubdirectory(root, path));
|
|
3582
3888
|
continue;
|
|
3583
3889
|
}
|
|
3584
|
-
if (!isMarkdown(
|
|
3890
|
+
if (!isMarkdown(name))
|
|
3585
3891
|
continue;
|
|
3586
3892
|
entries.push({
|
|
3587
3893
|
kind: "instinct",
|
|
3588
3894
|
path,
|
|
3589
3895
|
isFile: true,
|
|
3590
3896
|
shape: "instinct-file",
|
|
3591
|
-
name: markdownStem(
|
|
3897
|
+
name: markdownStem(name),
|
|
3592
3898
|
issues: []
|
|
3593
3899
|
});
|
|
3594
3900
|
}
|
|
@@ -3608,9 +3914,9 @@ function scanLayout(root, kind) {
|
|
|
3608
3914
|
function declaredNameIssue(entry, declared) {
|
|
3609
3915
|
if (entry.name === null || declared === null || declared === entry.name)
|
|
3610
3916
|
return null;
|
|
3611
|
-
const
|
|
3917
|
+
const field = entry.kind === "skill" ? "name" : "id";
|
|
3612
3918
|
const source = entry.kind === "skill" ? "directory name" : "file name";
|
|
3613
|
-
return layoutIssue("name-mismatch", `frontmatter ${
|
|
3919
|
+
return layoutIssue("name-mismatch", `frontmatter ${field} "${declared}" is not the ${source} "${entry.name}"`);
|
|
3614
3920
|
}
|
|
3615
3921
|
|
|
3616
3922
|
// src/schema/frontmatter.ts
|
|
@@ -3699,8 +4005,8 @@ function writeFrontmatter(document, data) {
|
|
|
3699
4005
|
}
|
|
3700
4006
|
|
|
3701
4007
|
// src/schema/instinct.ts
|
|
3702
|
-
var INSTINCT_KINDS =
|
|
3703
|
-
var INSTINCT_SIGNALS =
|
|
4008
|
+
var INSTINCT_KINDS = FINDING_KINDS2;
|
|
4009
|
+
var INSTINCT_SIGNALS = FINDING_SIGNALS2;
|
|
3704
4010
|
var INSTINCT_DOMAINS = FINDING_DOMAINS;
|
|
3705
4011
|
var INSTINCT_SCOPES = ["project", "user"];
|
|
3706
4012
|
var INSTINCT_SOURCES = ["task-report", "loop-observed", "imported", "demoted"];
|
|
@@ -3730,8 +4036,8 @@ var INSTINCT_KEY_ORDER = [
|
|
|
3730
4036
|
"created_at",
|
|
3731
4037
|
"updated_at"
|
|
3732
4038
|
];
|
|
3733
|
-
function issue(code,
|
|
3734
|
-
return { code, field
|
|
4039
|
+
function issue(code, field, message) {
|
|
4040
|
+
return { code, field, message };
|
|
3735
4041
|
}
|
|
3736
4042
|
function typeName(value) {
|
|
3737
4043
|
if (value === null)
|
|
@@ -3746,25 +4052,25 @@ function typeName(value) {
|
|
|
3746
4052
|
return "a number";
|
|
3747
4053
|
return `a ${typeof value}`;
|
|
3748
4054
|
}
|
|
3749
|
-
function requiredString(data,
|
|
3750
|
-
const value = data[
|
|
4055
|
+
function requiredString(data, field) {
|
|
4056
|
+
const value = data[field];
|
|
3751
4057
|
if (value === undefined || value === null) {
|
|
3752
|
-
return [issue("missing-field",
|
|
4058
|
+
return [issue("missing-field", field, `${field} is required and is absent`)];
|
|
3753
4059
|
}
|
|
3754
4060
|
if (typeof value !== "string") {
|
|
3755
|
-
return [issue("wrong-type",
|
|
4061
|
+
return [issue("wrong-type", field, `${field} must be a string, not ${typeName(value)}`)];
|
|
3756
4062
|
}
|
|
3757
4063
|
if (value.trim() === "") {
|
|
3758
|
-
return [issue("missing-field",
|
|
4064
|
+
return [issue("missing-field", field, `${field} is required and is empty`)];
|
|
3759
4065
|
}
|
|
3760
4066
|
return [];
|
|
3761
4067
|
}
|
|
3762
|
-
function requiredChoice(data,
|
|
3763
|
-
const issues = requiredString(data,
|
|
4068
|
+
function requiredChoice(data, field, allowed, code) {
|
|
4069
|
+
const issues = requiredString(data, field);
|
|
3764
4070
|
if (issues.length > 0)
|
|
3765
4071
|
return issues;
|
|
3766
|
-
const value = data[
|
|
3767
|
-
return allowed.includes(value) ? [] : [issue(code,
|
|
4072
|
+
const value = data[field];
|
|
4073
|
+
return allowed.includes(value) ? [] : [issue(code, field, `${field} "${value}" is not one of ${allowed.join(", ")}`)];
|
|
3768
4074
|
}
|
|
3769
4075
|
function checkId(data) {
|
|
3770
4076
|
const issues = requiredString(data, "id");
|
|
@@ -3800,11 +4106,11 @@ function checkSources(data) {
|
|
|
3800
4106
|
return [issue("wrong-type", "sources", `sources must be a list, not ${typeName(value)}`)];
|
|
3801
4107
|
}
|
|
3802
4108
|
return value.flatMap((entry, index) => {
|
|
3803
|
-
const
|
|
4109
|
+
const field = `sources[${index}]`;
|
|
3804
4110
|
if (typeof entry !== "string") {
|
|
3805
|
-
return [issue("wrong-type",
|
|
4111
|
+
return [issue("wrong-type", field, `sources entries must be strings, and this one is ${typeName(entry)}`)];
|
|
3806
4112
|
}
|
|
3807
|
-
return entry.trim() === "" ? [issue("wrong-type",
|
|
4113
|
+
return entry.trim() === "" ? [issue("wrong-type", field, "sources entries must name a source, and this one is blank")] : [];
|
|
3808
4114
|
});
|
|
3809
4115
|
}
|
|
3810
4116
|
function checkPromotedTo(data) {
|
|
@@ -3833,26 +4139,26 @@ function checkProjectId(data) {
|
|
|
3833
4139
|
}
|
|
3834
4140
|
return PROJECT_ID_PATTERN.test(value) ? [] : [issue("invalid-project-id", "project_id", `project_id "${value}" is not ${PROJECT_ID_LENGTH} lowercase hex characters`)];
|
|
3835
4141
|
}
|
|
3836
|
-
function checkTimestamp(data,
|
|
3837
|
-
const issues = requiredString(data,
|
|
4142
|
+
function checkTimestamp(data, field) {
|
|
4143
|
+
const issues = requiredString(data, field);
|
|
3838
4144
|
if (issues.length > 0)
|
|
3839
4145
|
return issues;
|
|
3840
|
-
const value = data[
|
|
3841
|
-
return TIMESTAMP_PATTERN.test(value) && !Number.isNaN(Date.parse(value)) ? [] : [issue("invalid-timestamp",
|
|
4146
|
+
const value = data[field];
|
|
4147
|
+
return TIMESTAMP_PATTERN.test(value) && !Number.isNaN(Date.parse(value)) ? [] : [issue("invalid-timestamp", field, `${field} "${value}" is not an ISO 8601 instant such as 2026-09-11T10:00:00Z`)];
|
|
3842
4148
|
}
|
|
3843
4149
|
function isScalar(value) {
|
|
3844
4150
|
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
3845
4151
|
}
|
|
3846
4152
|
function checkEvidenceEntry(entry, index) {
|
|
3847
|
-
const
|
|
4153
|
+
const field = `evidence[${index}]`;
|
|
3848
4154
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
3849
|
-
return [issue("wrong-type",
|
|
4155
|
+
return [issue("wrong-type", field, `evidence entries must be mappings, and this one is ${typeName(entry)}`)];
|
|
3850
4156
|
}
|
|
3851
4157
|
const entries = Object.entries(entry);
|
|
3852
4158
|
if (entries.length === 0) {
|
|
3853
|
-
return [issue("wrong-type",
|
|
4159
|
+
return [issue("wrong-type", field, "evidence entries must say something, and this one is empty")];
|
|
3854
4160
|
}
|
|
3855
|
-
return entries.flatMap(([key, value]) => isScalar(value) ? [] : [issue("wrong-type", `${
|
|
4161
|
+
return entries.flatMap(([key, value]) => isScalar(value) ? [] : [issue("wrong-type", `${field}.${key}`, `evidence values must be text, numbers or booleans, and this one is ${typeName(value)}`)]);
|
|
3856
4162
|
}
|
|
3857
4163
|
function checkEvidence(data) {
|
|
3858
4164
|
const value = data["evidence"];
|
|
@@ -3965,7 +4271,7 @@ function buildInstinct(data, body) {
|
|
|
3965
4271
|
updatedAt: data["updated_at"],
|
|
3966
4272
|
action,
|
|
3967
4273
|
cause: sectionText(body, CAUSE_HEADING) ?? "",
|
|
3968
|
-
actionHash:
|
|
4274
|
+
actionHash: actionHash2(action)
|
|
3969
4275
|
};
|
|
3970
4276
|
}
|
|
3971
4277
|
function instinctFrontmatter(instinct) {
|
|
@@ -4012,11 +4318,11 @@ function writeInstinct(instinct, newline = `
|
|
|
4012
4318
|
}
|
|
4013
4319
|
|
|
4014
4320
|
// src/commands/instinct/instinct-records.ts
|
|
4015
|
-
function isRecordFile(
|
|
4016
|
-
return !
|
|
4321
|
+
function isRecordFile(name) {
|
|
4322
|
+
return !name.startsWith(".") && name.endsWith(MARKDOWN_EXTENSION) && name !== MARKDOWN_EXTENSION;
|
|
4017
4323
|
}
|
|
4018
|
-
function recordId(
|
|
4019
|
-
return
|
|
4324
|
+
function recordId(name) {
|
|
4325
|
+
return name.slice(0, -MARKDOWN_EXTENSION.length);
|
|
4020
4326
|
}
|
|
4021
4327
|
function unreadableIssue(message) {
|
|
4022
4328
|
return { code: "missing-frontmatter", field: "file", message: `the file could not be read: ${message}` };
|
|
@@ -4024,7 +4330,7 @@ function unreadableIssue(message) {
|
|
|
4024
4330
|
function readRecord(path, scope, id) {
|
|
4025
4331
|
let text;
|
|
4026
4332
|
try {
|
|
4027
|
-
text =
|
|
4333
|
+
text = readFileSync9(path, "utf8");
|
|
4028
4334
|
} catch (error) {
|
|
4029
4335
|
const message = error instanceof Error ? error.message : String(error);
|
|
4030
4336
|
return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
|
|
@@ -4040,7 +4346,7 @@ function isFile2(path) {
|
|
|
4040
4346
|
}
|
|
4041
4347
|
}
|
|
4042
4348
|
function recordFiles(dir) {
|
|
4043
|
-
return readdirSync4(dir).filter((
|
|
4349
|
+
return readdirSync4(dir).filter((name) => isRecordFile(name) && isFile2(join14(dir, name))).sort((left, right) => left.localeCompare(right));
|
|
4044
4350
|
}
|
|
4045
4351
|
function readScope(scope, dir) {
|
|
4046
4352
|
if (!tierExists(dir))
|
|
@@ -4049,7 +4355,7 @@ function readScope(scope, dir) {
|
|
|
4049
4355
|
scope,
|
|
4050
4356
|
dir,
|
|
4051
4357
|
exists: true,
|
|
4052
|
-
records: recordFiles(dir).map((
|
|
4358
|
+
records: recordFiles(dir).map((name) => readRecord(join14(dir, name), scope, recordId(name)))
|
|
4053
4359
|
};
|
|
4054
4360
|
}
|
|
4055
4361
|
function readScopes(seams) {
|
|
@@ -4061,9 +4367,9 @@ function allRecords(listings) {
|
|
|
4061
4367
|
function findRecords(listings, id) {
|
|
4062
4368
|
return allRecords(listings).filter((entry) => entry.id === id);
|
|
4063
4369
|
}
|
|
4064
|
-
function instinctProject(context,
|
|
4370
|
+
function instinctProject(context, name) {
|
|
4065
4371
|
if (context.project === null)
|
|
4066
|
-
throw new Error(`${
|
|
4372
|
+
throw new Error(`${name} runs inside a project, and was handed none`);
|
|
4067
4373
|
return context.project;
|
|
4068
4374
|
}
|
|
4069
4375
|
|
|
@@ -4100,17 +4406,17 @@ function toHeldRecord(instinct) {
|
|
|
4100
4406
|
function toHeldRecords(held) {
|
|
4101
4407
|
const actions = new Map;
|
|
4102
4408
|
for (const instinct of held) {
|
|
4103
|
-
const key =
|
|
4409
|
+
const key = triggerKey2(instinct.trigger);
|
|
4104
4410
|
actions.set(key, (actions.get(key) ?? new Set).add(instinct.actionHash));
|
|
4105
4411
|
}
|
|
4106
4412
|
return held.map((instinct) => {
|
|
4107
4413
|
const record = toHeldRecord(instinct);
|
|
4108
|
-
return actions.get(
|
|
4414
|
+
return actions.get(triggerKey2(instinct.trigger)).size > 1 ? { ...record, status: "flagged" } : record;
|
|
4109
4415
|
});
|
|
4110
4416
|
}
|
|
4111
4417
|
function earliestHeldMember(record, held) {
|
|
4112
|
-
const key =
|
|
4113
|
-
const members = held.filter((instinct) => instinct.actionHash === record.action_hash &&
|
|
4418
|
+
const key = triggerKey2(record.trigger);
|
|
4419
|
+
const members = held.filter((instinct) => instinct.actionHash === record.action_hash && triggerKey2(instinct.trigger) === key);
|
|
4114
4420
|
return [...members].sort(byAge)[0] ?? null;
|
|
4115
4421
|
}
|
|
4116
4422
|
function toHeldInstinct(record, held, fallback) {
|
|
@@ -4137,7 +4443,7 @@ function toHeldInstinct(record, held, fallback) {
|
|
|
4137
4443
|
updatedAt: record.updated_at,
|
|
4138
4444
|
action: record.action,
|
|
4139
4445
|
cause: description.cause,
|
|
4140
|
-
actionHash:
|
|
4446
|
+
actionHash: actionHash2(record.action)
|
|
4141
4447
|
};
|
|
4142
4448
|
}
|
|
4143
4449
|
|
|
@@ -4219,7 +4525,7 @@ function recordProblem(record, index) {
|
|
|
4219
4525
|
if (problem !== null)
|
|
4220
4526
|
return problem;
|
|
4221
4527
|
const { action, action_hash: hash, description } = record;
|
|
4222
|
-
if (hash !==
|
|
4528
|
+
if (hash !== actionHash2(action)) {
|
|
4223
4529
|
return `action_hash is ${describeValue(hash)} in ${at}, expected the sha256 of its trimmed, lower-cased action`;
|
|
4224
4530
|
}
|
|
4225
4531
|
return description === undefined ? null : fieldsProblem(description, DESCRIPTION_CHECKS, `${at}.description`);
|
|
@@ -4231,7 +4537,7 @@ function payloadProblem(payload) {
|
|
|
4231
4537
|
const { instincts } = payload;
|
|
4232
4538
|
return instincts.map(recordProblem).find((found) => found !== null) ?? null;
|
|
4233
4539
|
}
|
|
4234
|
-
function
|
|
4540
|
+
function flagProblem2(value) {
|
|
4235
4541
|
return fieldsProblem(value, FLAG_CHECKS, "the flag");
|
|
4236
4542
|
}
|
|
4237
4543
|
function copyInstinct(record) {
|
|
@@ -4308,9 +4614,9 @@ function unflagged(records, flagged) {
|
|
|
4308
4614
|
return records.filter((record) => !flagged.has(record.id));
|
|
4309
4615
|
}
|
|
4310
4616
|
function userAdditions(userBlessed, projectHeld, flagged) {
|
|
4311
|
-
const heldTriggers = new Set(projectHeld.map((record) =>
|
|
4617
|
+
const heldTriggers = new Set(projectHeld.map((record) => triggerKey2(record.trigger)));
|
|
4312
4618
|
const heldIds = new Set(projectHeld.map((record) => record.id));
|
|
4313
|
-
return unflagged(userBlessed, flagged).filter((record) => !heldTriggers.has(
|
|
4619
|
+
return unflagged(userBlessed, flagged).filter((record) => !heldTriggers.has(triggerKey2(record.trigger)) && !heldIds.has(record.id));
|
|
4314
4620
|
}
|
|
4315
4621
|
function byAge2(left, right) {
|
|
4316
4622
|
if (left.created_at !== right.created_at) {
|
|
@@ -4321,8 +4627,8 @@ function byAge2(left, right) {
|
|
|
4321
4627
|
return left.id < right.id ? -1 : 1;
|
|
4322
4628
|
}
|
|
4323
4629
|
function payloadDescription(record, described) {
|
|
4324
|
-
const key =
|
|
4325
|
-
const members = described.filter((candidate) => candidate.description !== undefined && candidate.action_hash === record.action_hash &&
|
|
4630
|
+
const key = triggerKey2(record.trigger);
|
|
4631
|
+
const members = described.filter((candidate) => candidate.description !== undefined && candidate.action_hash === record.action_hash && triggerKey2(candidate.trigger) === key);
|
|
4326
4632
|
return [...members].sort(byAge2)[0]?.description;
|
|
4327
4633
|
}
|
|
4328
4634
|
function producedRecords(result) {
|
|
@@ -4348,35 +4654,35 @@ function heldChanges(dir, held, result, described) {
|
|
|
4348
4654
|
throw new Error(`${PUSH_REFUSAL}: ${file} has no held member and no payload description to be written with`);
|
|
4349
4655
|
}
|
|
4350
4656
|
const text = writeInstinct(instinct);
|
|
4351
|
-
const [
|
|
4352
|
-
if (
|
|
4353
|
-
throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${
|
|
4657
|
+
const [issue] = parseInstinct(text).issues;
|
|
4658
|
+
if (issue !== undefined) {
|
|
4659
|
+
throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue.field}: ${issue.message}`);
|
|
4354
4660
|
}
|
|
4355
4661
|
const path = join15(dir, file);
|
|
4356
4662
|
if (held.unreadable.has(path)) {
|
|
4357
4663
|
throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
|
|
4358
4664
|
}
|
|
4359
4665
|
const before = heldAt.get(path);
|
|
4360
|
-
if (before !== undefined &&
|
|
4666
|
+
if (before !== undefined && triggerKey2(before.trigger) !== triggerKey2(record.trigger)) {
|
|
4361
4667
|
throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which holds another trigger`);
|
|
4362
4668
|
}
|
|
4363
4669
|
kept.add(path);
|
|
4364
4670
|
if (before === undefined || writeInstinct(before) !== text)
|
|
4365
4671
|
writes.push({ path, text });
|
|
4366
4672
|
}
|
|
4367
|
-
const touched = new Set(result.decisions.map((decision) =>
|
|
4368
|
-
const removals = held.files.filter((file) => touched.has(
|
|
4673
|
+
const touched = new Set(result.decisions.map((decision) => triggerKey2(decision.incoming.trigger)));
|
|
4674
|
+
const removals = held.files.filter((file) => touched.has(triggerKey2(file.instinct.trigger)) && !kept.has(file.path)).map((file) => file.path);
|
|
4369
4675
|
return { writes, removals };
|
|
4370
4676
|
}
|
|
4371
4677
|
function pushLogLines(sourceId, described, result) {
|
|
4372
4678
|
return result.decisions.map((decision, index) => {
|
|
4373
|
-
const key =
|
|
4679
|
+
const key = triggerKey2(decision.incoming.trigger);
|
|
4374
4680
|
const line = {
|
|
4375
4681
|
source_id: sourceId,
|
|
4376
4682
|
rule: decision.rule,
|
|
4377
4683
|
incoming: loggedIncoming(described[index]),
|
|
4378
4684
|
produced: decision.produced.map((record) => record.id),
|
|
4379
|
-
discarded: result.discarded.filter((record) =>
|
|
4685
|
+
discarded: result.discarded.filter((record) => triggerKey2(record.trigger) === key).map((record) => record.id)
|
|
4380
4686
|
};
|
|
4381
4687
|
return JSON.stringify(line);
|
|
4382
4688
|
});
|
|
@@ -4401,7 +4707,7 @@ function createLocalLearning(options) {
|
|
|
4401
4707
|
for (const entry of records) {
|
|
4402
4708
|
if (entry.instinct === null) {
|
|
4403
4709
|
unreadable.add(entry.path);
|
|
4404
|
-
const problems = entry.issues.map((
|
|
4710
|
+
const problems = entry.issues.map((issue) => `${issue.field}: ${issue.message}`).join("; ");
|
|
4405
4711
|
warn(`${PREFIX}: ${entry.path} is not a held instinct, so it was skipped: ${problems}`);
|
|
4406
4712
|
} else {
|
|
4407
4713
|
files.push({ path: entry.path, instinct: entry.instinct });
|
|
@@ -4418,7 +4724,7 @@ function createLocalLearning(options) {
|
|
|
4418
4724
|
async function flaggedIds() {
|
|
4419
4725
|
const lines = numberedLines(await readIfPresent(flagsPath));
|
|
4420
4726
|
return new Set(lines.map(([number, line]) => {
|
|
4421
|
-
const reading = readLine(line,
|
|
4727
|
+
const reading = readLine(line, flagProblem2);
|
|
4422
4728
|
if (reading.ok)
|
|
4423
4729
|
return reading.value.id;
|
|
4424
4730
|
throw new Error(`${PREFIX}: line ${number} of ${flagsPath} is not a flag,` + ` and a bundle skipping it could hold an instinct it flags: ${reading.problem}`);
|
|
@@ -4432,7 +4738,7 @@ function createLocalLearning(options) {
|
|
|
4432
4738
|
}
|
|
4433
4739
|
const described = payload.instincts;
|
|
4434
4740
|
const held = heldSet();
|
|
4435
|
-
const result =
|
|
4741
|
+
const result = merge2(toHeldRecords(held.files.map((file) => file.instinct)), { source_id: payload.source_id, instincts: described.map(copyInstinct) }, now());
|
|
4436
4742
|
const changes = heldChanges(instinctsDir, held, result, described);
|
|
4437
4743
|
const lines = pushLogLines(payload.source_id, described, result);
|
|
4438
4744
|
if (lines.length > 0) {
|
|
@@ -4450,13 +4756,13 @@ function createLocalLearning(options) {
|
|
|
4450
4756
|
pullBlessed: async () => {
|
|
4451
4757
|
const flagged = await flaggedIds();
|
|
4452
4758
|
const projectHeld = heldRecords(heldSet());
|
|
4453
|
-
const project =
|
|
4454
|
-
const user =
|
|
4759
|
+
const project = bless2(unflagged(projectHeld, flagged), { minConfidence }).instincts;
|
|
4760
|
+
const user = bless2(heldRecords(readHeld("user", userDir)), { minConfidence }).instincts;
|
|
4455
4761
|
return { version: now(), instincts: [...project, ...userAdditions(user, projectHeld, flagged)] };
|
|
4456
4762
|
},
|
|
4457
4763
|
flag: async (id, reason) => {
|
|
4458
4764
|
const flag = { id, reason, flagged_at: now() };
|
|
4459
|
-
const problem =
|
|
4765
|
+
const problem = flagProblem2(flag);
|
|
4460
4766
|
if (problem !== null) {
|
|
4461
4767
|
throw new TypeError(`${PREFIX}: refused to write an invalid flag: ${problem}`);
|
|
4462
4768
|
}
|
|
@@ -4576,8 +4882,8 @@ function describeValue3(value) {
|
|
|
4576
4882
|
function messageOf2(error) {
|
|
4577
4883
|
return error instanceof Error ? error.message : String(error);
|
|
4578
4884
|
}
|
|
4579
|
-
function counted(
|
|
4580
|
-
return
|
|
4885
|
+
function counted(count, noun) {
|
|
4886
|
+
return count === 1 ? `1 ${noun}` : `${String(count)} ${noun}s`;
|
|
4581
4887
|
}
|
|
4582
4888
|
function valueAt(mapping, key) {
|
|
4583
4889
|
return Object.hasOwn(mapping, key) ? mapping[key] ?? null : null;
|
|
@@ -4595,39 +4901,39 @@ function verdictOf(value) {
|
|
|
4595
4901
|
const word = spelled.replace(VERDICT_BREAK, "-");
|
|
4596
4902
|
return SPEC_REVIEW_VERDICTS.includes(word) ? word : null;
|
|
4597
4903
|
}
|
|
4598
|
-
function issueAt(
|
|
4599
|
-
return { field
|
|
4904
|
+
function issueAt(field, text) {
|
|
4905
|
+
return { field, text };
|
|
4600
4906
|
}
|
|
4601
|
-
function readBlocking(item,
|
|
4907
|
+
function readBlocking(item, field, issues) {
|
|
4602
4908
|
const value = valueAt(item, "blocking");
|
|
4603
4909
|
if (typeof value === "boolean")
|
|
4604
4910
|
return value;
|
|
4605
|
-
const text = `${
|
|
4606
|
-
issues.push(issueAt(`${
|
|
4911
|
+
const text = `${field}.blocking is ${describeValue3(value)}, not true or false; ` + "the gap is read as blocking";
|
|
4912
|
+
issues.push(issueAt(`${field}.blocking`, text));
|
|
4607
4913
|
return true;
|
|
4608
4914
|
}
|
|
4609
|
-
function readGap(item,
|
|
4915
|
+
function readGap(item, field, issues) {
|
|
4610
4916
|
if (!isMapping2(item)) {
|
|
4611
|
-
const text = `${
|
|
4612
|
-
issues.push(issueAt(
|
|
4917
|
+
const text = `${field} is ${describeValue3(item)}, not a mapping of heading and what; dropped`;
|
|
4918
|
+
issues.push(issueAt(field, text));
|
|
4613
4919
|
return null;
|
|
4614
4920
|
}
|
|
4615
4921
|
const what = stringAt(item, "what");
|
|
4616
4922
|
if (what === null) {
|
|
4617
|
-
const text = `${
|
|
4618
|
-
issues.push(issueAt(`${
|
|
4923
|
+
const text = `${field}.what is ${describeValue3(valueAt(item, "what"))}, ` + "not a sentence; the gap is dropped";
|
|
4924
|
+
issues.push(issueAt(`${field}.what`, text));
|
|
4619
4925
|
return null;
|
|
4620
4926
|
}
|
|
4621
4927
|
const heading = stringAt(item, "heading");
|
|
4622
4928
|
if (heading === null) {
|
|
4623
|
-
const text = `${
|
|
4624
|
-
issues.push(issueAt(`${
|
|
4929
|
+
const text = `${field}.heading is ${describeValue3(valueAt(item, "heading"))}, ` + `not a heading; the gap is filed under ${REVIEW_HEADING}`;
|
|
4930
|
+
issues.push(issueAt(`${field}.heading`, text));
|
|
4625
4931
|
}
|
|
4626
|
-
const blocking = readBlocking(item,
|
|
4932
|
+
const blocking = readBlocking(item, field, issues);
|
|
4627
4933
|
const assumption = stringAt(item, "assumption");
|
|
4628
4934
|
if (!blocking && assumption === null) {
|
|
4629
|
-
const text = `${
|
|
4630
|
-
issues.push(issueAt(`${
|
|
4935
|
+
const text = `${field}.assumption is ${describeValue3(valueAt(item, "assumption"))}, ` + "not what a non-blocking gap would be planned under; the gap is read as blocking";
|
|
4936
|
+
issues.push(issueAt(`${field}.assumption`, text));
|
|
4631
4937
|
return { heading: heading ?? REVIEW_HEADING, what, blocking: true, assumption: null };
|
|
4632
4938
|
}
|
|
4633
4939
|
return { heading: heading ?? REVIEW_HEADING, what, blocking, assumption };
|
|
@@ -4676,7 +4982,7 @@ function judged(verdict, gaps, block, issues) {
|
|
|
4676
4982
|
};
|
|
4677
4983
|
}
|
|
4678
4984
|
function parseSpecReview(output) {
|
|
4679
|
-
const reviews =
|
|
4985
|
+
const reviews = readRafaBlocks2(output).filter((found) => found.kind === SPEC_REVIEW_KIND);
|
|
4680
4986
|
const block = reviews.at(-1);
|
|
4681
4987
|
if (block === undefined) {
|
|
4682
4988
|
return unreadable("absent", null, "the session output holds no rafa:spec-review block");
|
|
@@ -4725,35 +5031,6 @@ function sessionSpawnEnv(base, binDir = bundledBinDirectory()) {
|
|
|
4725
5031
|
}
|
|
4726
5032
|
|
|
4727
5033
|
// src/utils/claude.ts
|
|
4728
|
-
async function getClaudeUsagePercent() {
|
|
4729
|
-
const envPct = process.env["CLAUDE_USAGE_PERCENT"];
|
|
4730
|
-
if (envPct !== undefined) {
|
|
4731
|
-
const pct = Number(envPct);
|
|
4732
|
-
return Number.isFinite(pct) ? Math.max(0, Math.min(100, pct)) : null;
|
|
4733
|
-
}
|
|
4734
|
-
return null;
|
|
4735
|
-
}
|
|
4736
|
-
async function checkUsage(context) {
|
|
4737
|
-
const pct = await getClaudeUsagePercent();
|
|
4738
|
-
if (pct === null)
|
|
4739
|
-
return false;
|
|
4740
|
-
if (context === "task" && pct >= 90) {
|
|
4741
|
-
activeOutput().warn(`
|
|
4742
|
-
Claude usage at ${pct.toFixed(0)}% (>=90%). Pausing after current task to avoid hitting the limit.`);
|
|
4743
|
-
return true;
|
|
4744
|
-
}
|
|
4745
|
-
if (pct >= 80) {
|
|
4746
|
-
activeOutput().warn(`
|
|
4747
|
-
Claude usage at ${pct.toFixed(0)}% (>=80%). Monitor closely \u2014 tasks may be interrupted.`);
|
|
4748
|
-
} else if (context === "issue" && pct >= 70) {
|
|
4749
|
-
activeOutput().warn(`
|
|
4750
|
-
Claude usage at ${pct.toFixed(0)}% (>=70%). Consider whether to start the next issue.`);
|
|
4751
|
-
} else {
|
|
4752
|
-
activeOutput().info(`
|
|
4753
|
-
Claude usage: ${pct.toFixed(0)}%`);
|
|
4754
|
-
}
|
|
4755
|
-
return false;
|
|
4756
|
-
}
|
|
4757
5034
|
var CLAUDE_BIN = "claude";
|
|
4758
5035
|
var CLAUDE_BASE_ARGS = [
|
|
4759
5036
|
"-p",
|
|
@@ -4774,16 +5051,16 @@ class UndeclaredSpendError extends Error {
|
|
|
4774
5051
|
name = "UndeclaredSpendError";
|
|
4775
5052
|
}
|
|
4776
5053
|
function carriesFlag(flags, flag) {
|
|
4777
|
-
const
|
|
4778
|
-
const value = flags[
|
|
5054
|
+
const name = flag.replace(/^--/, "");
|
|
5055
|
+
const value = flags[name];
|
|
4779
5056
|
if (value !== undefined && value !== false)
|
|
4780
5057
|
return true;
|
|
4781
|
-
return
|
|
5058
|
+
return name.startsWith("no-") && flags[name.slice(3)] === false;
|
|
4782
5059
|
}
|
|
4783
|
-
function spendRefusal(
|
|
4784
|
-
if (
|
|
5060
|
+
function spendRefusal(running) {
|
|
5061
|
+
if (running === null)
|
|
4785
5062
|
return null;
|
|
4786
|
-
const { command, flags } =
|
|
5063
|
+
const { command, flags } = running;
|
|
4787
5064
|
const spend = command.spends;
|
|
4788
5065
|
const named = `rafa ${commandSpelling(command)}`;
|
|
4789
5066
|
const remedy = "declare spends on the command to cover this run";
|
|
@@ -5028,8 +5305,8 @@ async function withDeadline(work, timeoutMs) {
|
|
|
5028
5305
|
if (timeoutMs === undefined)
|
|
5029
5306
|
return work;
|
|
5030
5307
|
let timer;
|
|
5031
|
-
const deadline = new Promise((
|
|
5032
|
-
timer = setTimeout(() =>
|
|
5308
|
+
const deadline = new Promise((resolve) => {
|
|
5309
|
+
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
5033
5310
|
});
|
|
5034
5311
|
try {
|
|
5035
5312
|
return await Promise.race([work, deadline]);
|
|
@@ -5056,8 +5333,8 @@ function isRepoName(value) {
|
|
|
5056
5333
|
function isLabelPart(value) {
|
|
5057
5334
|
return typeof value === "string" && !value.includes(",");
|
|
5058
5335
|
}
|
|
5059
|
-
function optionalFlag(
|
|
5060
|
-
return value === undefined ? [] : [
|
|
5336
|
+
function optionalFlag(name, value) {
|
|
5337
|
+
return value === undefined ? [] : [name, value];
|
|
5061
5338
|
}
|
|
5062
5339
|
function detailOf(result) {
|
|
5063
5340
|
return result.stderr.trim() || result.stdout.trim() || "it exited non-zero and wrote nothing";
|
|
@@ -5120,7 +5397,7 @@ function labelNames(value) {
|
|
|
5120
5397
|
if (!Array.isArray(value))
|
|
5121
5398
|
return null;
|
|
5122
5399
|
const names = value.map((label) => isMapping(label) && typeof label["name"] === "string" ? label["name"] : null);
|
|
5123
|
-
return names.every((
|
|
5400
|
+
return names.every((name) => name !== null) ? names : null;
|
|
5124
5401
|
}
|
|
5125
5402
|
function viewProblem(payload, number) {
|
|
5126
5403
|
if (!isMapping(payload))
|
|
@@ -5178,13 +5455,13 @@ function listedIssues(stdout) {
|
|
|
5178
5455
|
}
|
|
5179
5456
|
function listedOpenIssues(stdout) {
|
|
5180
5457
|
return listedRows(stdout).map((row, index) => {
|
|
5181
|
-
const
|
|
5458
|
+
const listed = listedRow(row, index);
|
|
5182
5459
|
const { title, body } = row;
|
|
5183
5460
|
if (typeof title !== "string")
|
|
5184
5461
|
throw rowRefusal(index, `with title ${describeValue(title)}, expected a string`);
|
|
5185
5462
|
if (typeof body !== "string")
|
|
5186
5463
|
throw rowRefusal(index, `with body ${describeValue(body)}, expected a string`);
|
|
5187
|
-
return { ...
|
|
5464
|
+
return { ...listed, title, body };
|
|
5188
5465
|
});
|
|
5189
5466
|
}
|
|
5190
5467
|
function refOfRow(row) {
|
|
@@ -5242,9 +5519,9 @@ function createGithubTracker(options) {
|
|
|
5242
5519
|
return result.stdout;
|
|
5243
5520
|
}
|
|
5244
5521
|
async function ensureLabels(names) {
|
|
5245
|
-
const missing = [...new Set(names)].filter((
|
|
5246
|
-
for (const
|
|
5247
|
-
await run(["label", "create",
|
|
5522
|
+
const missing = [...new Set(names)].filter((name) => !knownLabels.has(name));
|
|
5523
|
+
for (const name of missing) {
|
|
5524
|
+
await run(["label", "create", name, "--force"], `gh label create ${describeValue(name)}`);
|
|
5248
5525
|
}
|
|
5249
5526
|
knownLabels = new Set([...knownLabels, ...missing]);
|
|
5250
5527
|
}
|
|
@@ -5423,8 +5700,8 @@ function parseIssueNumber(text) {
|
|
|
5423
5700
|
const number = Number(text);
|
|
5424
5701
|
return Number.isSafeInteger(number) ? number : null;
|
|
5425
5702
|
}
|
|
5426
|
-
function issueNumberOfFile(
|
|
5427
|
-
return
|
|
5703
|
+
function issueNumberOfFile(name) {
|
|
5704
|
+
return name.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name.slice(0, -ISSUE_EXTENSION.length)) : null;
|
|
5428
5705
|
}
|
|
5429
5706
|
function issuePath(issuesDir, number) {
|
|
5430
5707
|
return join16(issuesDir, `${number}${ISSUE_EXTENSION}`);
|
|
@@ -5629,7 +5906,7 @@ function createLocalTracker(options) {
|
|
|
5629
5906
|
|
|
5630
5907
|
// src/adapters/registry.ts
|
|
5631
5908
|
var REFUSAL3 = "adapter registry";
|
|
5632
|
-
var
|
|
5909
|
+
var PORT_VERSIONS2 = Object.freeze({
|
|
5633
5910
|
tracker: 1,
|
|
5634
5911
|
store: 1,
|
|
5635
5912
|
learning: 1,
|
|
@@ -5637,7 +5914,7 @@ var PORT_VERSIONS = Object.freeze({
|
|
|
5637
5914
|
planner: 1,
|
|
5638
5915
|
sync: 1
|
|
5639
5916
|
});
|
|
5640
|
-
var SERVED_VERSIONS = new Map(Object.entries(
|
|
5917
|
+
var SERVED_VERSIONS = new Map(Object.entries(PORT_VERSIONS2));
|
|
5641
5918
|
var PORT_TYPE_LIST = [...SERVED_VERSIONS.keys()].join(", ");
|
|
5642
5919
|
function checkPortType(port) {
|
|
5643
5920
|
if (!SERVED_VERSIONS.has(port)) {
|
|
@@ -5677,11 +5954,11 @@ function indexByPort(adapters) {
|
|
|
5677
5954
|
sync: only("sync")
|
|
5678
5955
|
};
|
|
5679
5956
|
}
|
|
5680
|
-
function
|
|
5957
|
+
function createAdapterRegistry2(adapters = []) {
|
|
5681
5958
|
const held = adapters.reduce((accepted, adapter) => [...accepted, accept(adapter, accepted)], []);
|
|
5682
5959
|
const byPort = indexByPort(held);
|
|
5683
5960
|
const registry = {
|
|
5684
|
-
register: (adapter) =>
|
|
5961
|
+
register: (adapter) => createAdapterRegistry2([...held, adapter]),
|
|
5685
5962
|
find: (port, kind) => SERVED_VERSIONS.has(port) ? byPort[port].get(kind) : undefined,
|
|
5686
5963
|
resolve: (port, kind) => {
|
|
5687
5964
|
checkPortType(port);
|
|
@@ -5689,8 +5966,8 @@ function createAdapterRegistry(adapters = []) {
|
|
|
5689
5966
|
if (adapter !== undefined)
|
|
5690
5967
|
return adapter;
|
|
5691
5968
|
const registered = [...byPort[port].keys()];
|
|
5692
|
-
const
|
|
5693
|
-
throw new Error(`${REFUSAL3}: no ${port} adapter is registered as ${describeValue(kind)};` + ` registered: ${
|
|
5969
|
+
const listed = registered.length === 0 ? "none" : registered.join(", ");
|
|
5970
|
+
throw new Error(`${REFUSAL3}: no ${port} adapter is registered as ${describeValue(kind)};` + ` registered: ${listed}`);
|
|
5694
5971
|
},
|
|
5695
5972
|
kinds: (port) => {
|
|
5696
5973
|
checkPortType(port);
|
|
@@ -5700,38 +5977,38 @@ function createAdapterRegistry(adapters = []) {
|
|
|
5700
5977
|
return Object.freeze(registry);
|
|
5701
5978
|
}
|
|
5702
5979
|
var STORE_OPENERS = {
|
|
5703
|
-
sqlite:
|
|
5704
|
-
ndjson:
|
|
5980
|
+
sqlite: openSqliteStore2,
|
|
5981
|
+
ndjson: openNdjsonStore2
|
|
5705
5982
|
};
|
|
5706
5983
|
var CORE_ADAPTERS = [
|
|
5707
|
-
...
|
|
5984
|
+
...STORE_BACKENDS2.map((backend) => ({
|
|
5708
5985
|
port: "store",
|
|
5709
5986
|
kind: backend,
|
|
5710
|
-
portVersion:
|
|
5987
|
+
portVersion: PORT_VERSIONS2.store,
|
|
5711
5988
|
create: ({ repoRoot }) => STORE_OPENERS[backend](repoRoot)
|
|
5712
5989
|
})),
|
|
5713
5990
|
{
|
|
5714
5991
|
port: "output",
|
|
5715
5992
|
kind: "text",
|
|
5716
|
-
portVersion:
|
|
5993
|
+
portVersion: PORT_VERSIONS2.output,
|
|
5717
5994
|
create: ({ stream = process.stdout, verbosity = 0 }) => createTextOutput({ verbosity, stream })
|
|
5718
5995
|
},
|
|
5719
5996
|
{
|
|
5720
5997
|
port: "output",
|
|
5721
5998
|
kind: "json",
|
|
5722
|
-
portVersion:
|
|
5999
|
+
portVersion: PORT_VERSIONS2.output,
|
|
5723
6000
|
create: ({ stream = process.stdout }) => createJsonOutput({ stream })
|
|
5724
6001
|
},
|
|
5725
6002
|
{
|
|
5726
6003
|
port: "output",
|
|
5727
6004
|
kind: "events",
|
|
5728
|
-
portVersion:
|
|
6005
|
+
portVersion: PORT_VERSIONS2.output,
|
|
5729
6006
|
create: ({ stream = process.stdout }) => createEventsOutput({ stream })
|
|
5730
6007
|
},
|
|
5731
6008
|
{
|
|
5732
6009
|
port: "tracker",
|
|
5733
6010
|
kind: "local",
|
|
5734
|
-
portVersion:
|
|
6011
|
+
portVersion: PORT_VERSIONS2.tracker,
|
|
5735
6012
|
create: ({ repoRoot, fallbackReason = null }) => createLocalTracker({
|
|
5736
6013
|
issuesDir: localIssuesDir(repoRoot),
|
|
5737
6014
|
fallbackReason
|
|
@@ -5740,17 +6017,17 @@ var CORE_ADAPTERS = [
|
|
|
5740
6017
|
{
|
|
5741
6018
|
port: "tracker",
|
|
5742
6019
|
kind: "github",
|
|
5743
|
-
portVersion:
|
|
6020
|
+
portVersion: PORT_VERSIONS2.tracker,
|
|
5744
6021
|
create: ({ repoRoot, gh }) => createGithubTracker({ gh: gh ?? createGhRunner({ cwd: repoRoot }) })
|
|
5745
6022
|
},
|
|
5746
6023
|
{
|
|
5747
6024
|
port: "learning",
|
|
5748
6025
|
kind: "local",
|
|
5749
|
-
portVersion:
|
|
6026
|
+
portVersion: PORT_VERSIONS2.learning,
|
|
5750
6027
|
create: ({
|
|
5751
6028
|
repoRoot,
|
|
5752
6029
|
home = homedir(),
|
|
5753
|
-
learningBlessMinConfidence =
|
|
6030
|
+
learningBlessMinConfidence = CONFIG_DEFAULTS2.learningBlessMinConfidence
|
|
5754
6031
|
}) => createLocalLearning({
|
|
5755
6032
|
instinctsDir: localInstinctsDir(repoRoot),
|
|
5756
6033
|
home,
|
|
@@ -5760,7 +6037,7 @@ var CORE_ADAPTERS = [
|
|
|
5760
6037
|
{
|
|
5761
6038
|
port: "planner",
|
|
5762
6039
|
kind: "claude",
|
|
5763
|
-
portVersion:
|
|
6040
|
+
portVersion: PORT_VERSIONS2.planner,
|
|
5764
6041
|
create: ({ repoRoot, settingSources, planPrompt, planDir, claude }) => {
|
|
5765
6042
|
if (!Array.isArray(settingSources)) {
|
|
5766
6043
|
throw new TypeError(`${REFUSAL3}: planner/claude has settingSources ${describeValue(settingSources)} in its context,` + " expected a list of setting sources");
|
|
@@ -5777,26 +6054,26 @@ var CORE_ADAPTERS = [
|
|
|
5777
6054
|
{
|
|
5778
6055
|
port: "sync",
|
|
5779
6056
|
kind: "local",
|
|
5780
|
-
portVersion:
|
|
6057
|
+
portVersion: PORT_VERSIONS2.sync,
|
|
5781
6058
|
create: () => createLocalSync()
|
|
5782
6059
|
},
|
|
5783
6060
|
{
|
|
5784
6061
|
port: "sync",
|
|
5785
6062
|
kind: "file",
|
|
5786
|
-
portVersion:
|
|
6063
|
+
portVersion: PORT_VERSIONS2.sync,
|
|
5787
6064
|
create: ({ repoRoot, store }) => {
|
|
5788
|
-
if (!
|
|
5789
|
-
throw new TypeError(`${REFUSAL3}: sync/file has store ${describeValue(store)} in its context,` + ` expected one of: ${
|
|
6065
|
+
if (!STORE_BACKENDS2.includes(store)) {
|
|
6066
|
+
throw new TypeError(`${REFUSAL3}: sync/file has store ${describeValue(store)} in its context,` + ` expected one of: ${STORE_BACKENDS2.join(", ")}`);
|
|
5790
6067
|
}
|
|
5791
6068
|
return createFileSync({ repoRoot, backend: store });
|
|
5792
6069
|
}
|
|
5793
6070
|
}
|
|
5794
6071
|
];
|
|
5795
|
-
var
|
|
6072
|
+
var CORE_ADAPTER_REGISTRY2 = createAdapterRegistry2(CORE_ADAPTERS);
|
|
5796
6073
|
|
|
5797
6074
|
// src/board/trust.ts
|
|
5798
6075
|
var REPO_PATH = "{owner}/{repo}";
|
|
5799
|
-
var
|
|
6076
|
+
var TRUSTED_PERMISSIONS2 = ["admin", "maintain", "write"];
|
|
5800
6077
|
var TRUST_REFUSAL_EXIT = 2;
|
|
5801
6078
|
var REMEDY = Object.freeze({
|
|
5802
6079
|
issue: "a member must open the spec",
|
|
@@ -5808,7 +6085,7 @@ function failureDetail(reading) {
|
|
|
5808
6085
|
return detail === "" ? UNREPORTED : detail;
|
|
5809
6086
|
}
|
|
5810
6087
|
function isTrustedPermission(value) {
|
|
5811
|
-
return
|
|
6088
|
+
return TRUSTED_PERMISSIONS2.some((permission) => permission === value);
|
|
5812
6089
|
}
|
|
5813
6090
|
function stringOrNull(value) {
|
|
5814
6091
|
return typeof value === "string" ? value : null;
|
|
@@ -5858,8 +6135,8 @@ async function readAuthorTrust(options) {
|
|
|
5858
6135
|
};
|
|
5859
6136
|
}
|
|
5860
6137
|
const wanted = login.toLowerCase();
|
|
5861
|
-
const
|
|
5862
|
-
if (
|
|
6138
|
+
const listed = trustedAuthors.some((author) => author.toLowerCase() === wanted);
|
|
6139
|
+
if (listed) {
|
|
5863
6140
|
return { login, trusted: true, source: "allow-list", refusal: null, permission: null };
|
|
5864
6141
|
}
|
|
5865
6142
|
const permission = await permissions(login);
|
|
@@ -5911,18 +6188,18 @@ function requireTrustedAuthor(item, reading) {
|
|
|
5911
6188
|
throw new CommandExit(TRUST_REFUSAL_EXIT, trustRefusalMessage(item, reading));
|
|
5912
6189
|
}
|
|
5913
6190
|
// src/effort/sync/wire.ts
|
|
5914
|
-
import { existsSync as existsSync14, mkdtempSync as mkdtempSync2, rmSync as
|
|
6191
|
+
import { existsSync as existsSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
|
|
5915
6192
|
import { tmpdir as tmpdir4 } from "os";
|
|
5916
6193
|
import { join as join17 } from "path";
|
|
5917
|
-
import { Database as
|
|
5918
|
-
var
|
|
5919
|
-
var
|
|
6194
|
+
import { Database as Database7, SQLiteError as SQLiteError3 } from "bun:sqlite";
|
|
6195
|
+
var WIRE_FORMAT2 = "rafa-effort-wire";
|
|
6196
|
+
var WIRE_VERSION2 = 1;
|
|
5920
6197
|
|
|
5921
|
-
class
|
|
6198
|
+
class WireFormatError2 extends Error {
|
|
5922
6199
|
name = "WireFormatError";
|
|
5923
6200
|
}
|
|
5924
6201
|
|
|
5925
|
-
class
|
|
6202
|
+
class WireExportRefusal2 extends Error {
|
|
5926
6203
|
name = "WireExportRefusal";
|
|
5927
6204
|
}
|
|
5928
6205
|
var MERGED_TABLES = Object.entries(MERGE_RULES).filter(([, rule]) => rule.scope === "merged").map(([table]) => table);
|
|
@@ -5933,17 +6210,17 @@ function isRecord4(value) {
|
|
|
5933
6210
|
function isCount(value) {
|
|
5934
6211
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
5935
6212
|
}
|
|
5936
|
-
function isMergedTable(
|
|
5937
|
-
return MERGED_TABLES.includes(
|
|
6213
|
+
function isMergedTable(name) {
|
|
6214
|
+
return MERGED_TABLES.includes(name);
|
|
5938
6215
|
}
|
|
5939
6216
|
function checkCursor(value) {
|
|
5940
6217
|
if (!isRecord4(value))
|
|
5941
|
-
throw new
|
|
6218
|
+
throw new WireFormatError2("wire: cursor is not an object");
|
|
5942
6219
|
const entries = Object.entries(value).map(([table, seq]) => {
|
|
5943
6220
|
if (!isMergedTable(table))
|
|
5944
|
-
throw new
|
|
6221
|
+
throw new WireFormatError2(`wire: cursor names ${JSON.stringify(table)}, which is no merged table`);
|
|
5945
6222
|
if (!isCount(seq))
|
|
5946
|
-
throw new
|
|
6223
|
+
throw new WireFormatError2(`wire: cursor for ${table} is ${JSON.stringify(seq)}, expected a whole number of 0 or more`);
|
|
5947
6224
|
return [table, seq];
|
|
5948
6225
|
});
|
|
5949
6226
|
return Object.freeze(Object.fromEntries(entries));
|
|
@@ -5951,67 +6228,67 @@ function checkCursor(value) {
|
|
|
5951
6228
|
function checkRow(table, value, index) {
|
|
5952
6229
|
const where = `wire: ${table} row ${String(index)}`;
|
|
5953
6230
|
if (!isRecord4(value))
|
|
5954
|
-
throw new
|
|
6231
|
+
throw new WireFormatError2(`${where} is not an object`);
|
|
5955
6232
|
for (const [column, cell] of Object.entries(value)) {
|
|
5956
6233
|
const carried = cell === null || typeof cell === "string" || typeof cell === "number" && Number.isFinite(cell);
|
|
5957
6234
|
if (!carried)
|
|
5958
|
-
throw new
|
|
6235
|
+
throw new WireFormatError2(`${where} holds ${JSON.stringify(cell) ?? typeof cell} in ${column}, expected a string, a number or null`);
|
|
5959
6236
|
}
|
|
5960
6237
|
const seq = value.seq;
|
|
5961
6238
|
if (!isCount(seq) || seq === 0)
|
|
5962
|
-
throw new
|
|
6239
|
+
throw new WireFormatError2(`${where} has seq ${JSON.stringify(seq) ?? "undefined"}, expected a whole number above 0`);
|
|
5963
6240
|
return Object.freeze({ ...value });
|
|
5964
6241
|
}
|
|
5965
6242
|
function checkMigrations(value) {
|
|
5966
6243
|
if (!Array.isArray(value))
|
|
5967
|
-
throw new
|
|
6244
|
+
throw new WireFormatError2("wire: migrations is not a list");
|
|
5968
6245
|
const ids = value.map((id) => {
|
|
5969
6246
|
if (typeof id !== "string" || id === "")
|
|
5970
|
-
throw new
|
|
6247
|
+
throw new WireFormatError2(`wire: migration id ${JSON.stringify(id)} is not a name`);
|
|
5971
6248
|
return id;
|
|
5972
6249
|
});
|
|
5973
6250
|
const twice = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|
5974
6251
|
if (twice.length > 0)
|
|
5975
|
-
throw new
|
|
6252
|
+
throw new WireFormatError2(`wire: migrations name ${twice.join(", ")} more than once`);
|
|
5976
6253
|
return Object.freeze(ids);
|
|
5977
6254
|
}
|
|
5978
6255
|
function checkTables(value) {
|
|
5979
6256
|
if (!isRecord4(value))
|
|
5980
|
-
throw new
|
|
6257
|
+
throw new WireFormatError2("wire: tables is not an object");
|
|
5981
6258
|
const entries = Object.entries(value).map(([table, rows]) => {
|
|
5982
6259
|
if (!isMergedTable(table))
|
|
5983
|
-
throw new
|
|
6260
|
+
throw new WireFormatError2(`wire: tables names ${JSON.stringify(table)}, which is no merged table`);
|
|
5984
6261
|
if (!Array.isArray(rows))
|
|
5985
|
-
throw new
|
|
6262
|
+
throw new WireFormatError2(`wire: tables.${table} is not a list`);
|
|
5986
6263
|
return [table, Object.freeze(rows.map((row, index) => checkRow(table, row, index)))];
|
|
5987
6264
|
});
|
|
5988
6265
|
return Object.freeze(Object.fromEntries(entries));
|
|
5989
6266
|
}
|
|
5990
6267
|
function checkPayload(value) {
|
|
5991
6268
|
if (!isRecord4(value))
|
|
5992
|
-
throw new
|
|
5993
|
-
if (value.format !==
|
|
5994
|
-
throw new
|
|
5995
|
-
if (value.version !==
|
|
5996
|
-
throw new
|
|
6269
|
+
throw new WireFormatError2("wire: the payload is not an object");
|
|
6270
|
+
if (value.format !== WIRE_FORMAT2)
|
|
6271
|
+
throw new WireFormatError2(`wire: format is ${JSON.stringify(value.format) ?? "undefined"}, expected ${JSON.stringify(WIRE_FORMAT2)}`);
|
|
6272
|
+
if (value.version !== WIRE_VERSION2)
|
|
6273
|
+
throw new WireFormatError2(`wire: version is ${JSON.stringify(value.version) ?? "undefined"}; this rafa reads version ${String(WIRE_VERSION2)}`);
|
|
5997
6274
|
return Object.freeze({
|
|
5998
|
-
format:
|
|
5999
|
-
version:
|
|
6275
|
+
format: WIRE_FORMAT2,
|
|
6276
|
+
version: WIRE_VERSION2,
|
|
6000
6277
|
migrations: checkMigrations(value.migrations),
|
|
6001
6278
|
tables: checkTables(value.tables),
|
|
6002
6279
|
cursor: checkCursor(value.cursor)
|
|
6003
6280
|
});
|
|
6004
6281
|
}
|
|
6005
|
-
function
|
|
6282
|
+
function encodeWirePayload2(payload) {
|
|
6006
6283
|
return JSON.stringify(payload);
|
|
6007
6284
|
}
|
|
6008
|
-
function
|
|
6285
|
+
function decodeWirePayload2(text) {
|
|
6009
6286
|
let parsed;
|
|
6010
6287
|
try {
|
|
6011
6288
|
parsed = JSON.parse(text);
|
|
6012
6289
|
} catch (error) {
|
|
6013
6290
|
const reason = error instanceof Error ? error.message : String(error);
|
|
6014
|
-
throw new
|
|
6291
|
+
throw new WireFormatError2(`wire: the payload is not JSON (${reason})`);
|
|
6015
6292
|
}
|
|
6016
6293
|
return checkPayload(parsed);
|
|
6017
6294
|
}
|
|
@@ -6022,17 +6299,17 @@ function readLoggedIds(db) {
|
|
|
6022
6299
|
return db.query(`SELECT id FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map(({ id }) => id);
|
|
6023
6300
|
}
|
|
6024
6301
|
function columnsOf(db, table) {
|
|
6025
|
-
return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name
|
|
6302
|
+
return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name }) => name);
|
|
6026
6303
|
}
|
|
6027
6304
|
function wireValue(value, where) {
|
|
6028
6305
|
if (typeof value === "bigint") {
|
|
6029
6306
|
const number = Number(value);
|
|
6030
6307
|
if (Number.isSafeInteger(number))
|
|
6031
6308
|
return number;
|
|
6032
|
-
throw new
|
|
6309
|
+
throw new WireExportRefusal2(`wire: ${where} holds ${String(value)}, past the whole numbers JSON carries exactly`);
|
|
6033
6310
|
}
|
|
6034
6311
|
if (value instanceof Uint8Array)
|
|
6035
|
-
throw new
|
|
6312
|
+
throw new WireExportRefusal2(`wire: ${where} holds a BLOB, which the wire does not carry`);
|
|
6036
6313
|
return value;
|
|
6037
6314
|
}
|
|
6038
6315
|
function readRows(db, table, columns, after, path) {
|
|
@@ -6045,7 +6322,7 @@ function readRows(db, table, columns, after, path) {
|
|
|
6045
6322
|
function readPayload(db, path, cursor) {
|
|
6046
6323
|
const migrations = readLoggedIds(db);
|
|
6047
6324
|
if (migrations === null) {
|
|
6048
|
-
throw new
|
|
6325
|
+
throw new WireExportRefusal2(`wire: ${path} has no migration log; any rafa command that writes the store brings it forward`);
|
|
6049
6326
|
}
|
|
6050
6327
|
const tables = {};
|
|
6051
6328
|
const next = { ...cursor };
|
|
@@ -6055,7 +6332,7 @@ function readPayload(db, path, cursor) {
|
|
|
6055
6332
|
continue;
|
|
6056
6333
|
const missing = ORIGIN_COLUMNS.filter((column) => !columns.includes(column));
|
|
6057
6334
|
if (missing.length > 0) {
|
|
6058
|
-
throw new
|
|
6335
|
+
throw new WireExportRefusal2(`wire: ${path} holds no ${missing.map((column) => `${table}.${column}`).join(", ")}, so its rows carry no origin pair`);
|
|
6059
6336
|
}
|
|
6060
6337
|
const rows = readRows(db, table, columns, cursor[table] ?? 0, path);
|
|
6061
6338
|
tables[table] = Object.freeze(rows);
|
|
@@ -6063,20 +6340,20 @@ function readPayload(db, path, cursor) {
|
|
|
6063
6340
|
next[table] = last === undefined ? cursor[table] ?? 0 : Number(last.seq);
|
|
6064
6341
|
}
|
|
6065
6342
|
return Object.freeze({
|
|
6066
|
-
format:
|
|
6067
|
-
version:
|
|
6343
|
+
format: WIRE_FORMAT2,
|
|
6344
|
+
version: WIRE_VERSION2,
|
|
6068
6345
|
migrations: Object.freeze([...migrations]),
|
|
6069
6346
|
tables: Object.freeze(tables),
|
|
6070
6347
|
cursor: Object.freeze(next)
|
|
6071
6348
|
});
|
|
6072
6349
|
}
|
|
6073
|
-
function
|
|
6350
|
+
function exportWirePayload2(options) {
|
|
6074
6351
|
const { path } = options;
|
|
6075
6352
|
const cursor = checkCursor(options.cursor ?? {});
|
|
6076
6353
|
guardTestProcess(path);
|
|
6077
6354
|
if (!existsSync14(path))
|
|
6078
|
-
throw new
|
|
6079
|
-
const db = new
|
|
6355
|
+
throw new WireExportRefusal2(`wire: ${path} is not there`);
|
|
6356
|
+
const db = new Database7(path, { readonly: true, safeIntegers: true });
|
|
6080
6357
|
try {
|
|
6081
6358
|
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
6082
6359
|
return db.transaction(() => readPayload(db, path, cursor))();
|
|
@@ -6088,34 +6365,34 @@ function namedMigrations(ids, catalogue) {
|
|
|
6088
6365
|
const known = new Set(catalogue.map(({ id }) => id));
|
|
6089
6366
|
const unknown = ids.filter((id) => !known.has(id));
|
|
6090
6367
|
if (unknown.length > 0) {
|
|
6091
|
-
throw new
|
|
6368
|
+
throw new WireFormatError2(`wire: the payload's store holds migration ${unknown.join(", ")}, which this rafa does not know; update rafa to take its rows`);
|
|
6092
6369
|
}
|
|
6093
6370
|
return catalogue.filter(({ id }) => ids.includes(id));
|
|
6094
6371
|
}
|
|
6095
6372
|
function insertRows(db, table, rows) {
|
|
6096
6373
|
const columns = columnsOf(db, table);
|
|
6097
6374
|
if (columns.length === 0)
|
|
6098
|
-
throw new
|
|
6375
|
+
throw new WireFormatError2(`wire: the payload carries ${table}, which its migrations do not make`);
|
|
6099
6376
|
const statement = db.query(`INSERT INTO ${quoted(table)} (${columns.map(quoted).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`);
|
|
6100
6377
|
const expected = [...columns].sort().join(",");
|
|
6101
6378
|
rows.forEach((row, index) => {
|
|
6102
6379
|
const where = `wire: ${table} row ${String(index)}`;
|
|
6103
6380
|
const held = Object.keys(row);
|
|
6104
6381
|
if ([...held].sort().join(",") !== expected) {
|
|
6105
|
-
throw new
|
|
6382
|
+
throw new WireFormatError2(`${where} holds columns ${held.join(", ")}; its migrations make ${columns.join(", ")}`);
|
|
6106
6383
|
}
|
|
6107
6384
|
try {
|
|
6108
6385
|
statement.run(...columns.map((column) => row[column] ?? null));
|
|
6109
6386
|
} catch (error) {
|
|
6110
6387
|
if (!(error instanceof SQLiteError3))
|
|
6111
6388
|
throw error;
|
|
6112
|
-
throw new
|
|
6389
|
+
throw new WireFormatError2(`${where} (seq ${String(row.seq)}) is refused by the table: ${error.message}`);
|
|
6113
6390
|
}
|
|
6114
6391
|
});
|
|
6115
6392
|
}
|
|
6116
6393
|
function buildStore(path, payload, options) {
|
|
6117
6394
|
const migrations = namedMigrations(payload.migrations, options.migrations ?? SQLITE_MIGRATIONS);
|
|
6118
|
-
const db = new
|
|
6395
|
+
const db = new Database7(path, { create: true, readwrite: true });
|
|
6119
6396
|
try {
|
|
6120
6397
|
bringForward(db, path, "write", "open", {
|
|
6121
6398
|
migrations,
|
|
@@ -6131,10 +6408,10 @@ function buildStore(path, payload, options) {
|
|
|
6131
6408
|
db.close();
|
|
6132
6409
|
}
|
|
6133
6410
|
}
|
|
6134
|
-
function
|
|
6411
|
+
function materialiseWirePayload2(payload, options = {}) {
|
|
6135
6412
|
const checked = checkPayload(payload);
|
|
6136
6413
|
const directory = mkdtempSync2(join17(options.tempDir ?? tmpdir4(), "rafa-wire-"));
|
|
6137
|
-
const release = () =>
|
|
6414
|
+
const release = () => rmSync8(directory, { recursive: true, force: true });
|
|
6138
6415
|
const path = join17(directory, SQLITE_STORE_FILE_NAME);
|
|
6139
6416
|
try {
|
|
6140
6417
|
guardTestProcess(path);
|
|
@@ -6154,13 +6431,13 @@ function describeValue4(value) {
|
|
|
6154
6431
|
return "an object";
|
|
6155
6432
|
return String(value);
|
|
6156
6433
|
}
|
|
6157
|
-
function
|
|
6158
|
-
const adapter =
|
|
6434
|
+
function selectEffortStore2(repoRoot, config) {
|
|
6435
|
+
const adapter = CORE_ADAPTER_REGISTRY2.find("store", config.store);
|
|
6159
6436
|
if (adapter === undefined) {
|
|
6160
|
-
const expected =
|
|
6437
|
+
const expected = CORE_ADAPTER_REGISTRY2.kinds("store").join(", ");
|
|
6161
6438
|
throw new TypeError(`effort store: store is ${describeValue4(config.store)},` + ` expected one of: ${expected}`);
|
|
6162
6439
|
}
|
|
6163
6440
|
return adapter.create({ repoRoot });
|
|
6164
6441
|
}
|
|
6165
6442
|
|
|
6166
|
-
export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess,
|
|
6443
|
+
export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS2, openNdjsonStore2, name, version, RAFA_VERSION, versionLine, createGitRunner, gitSaid, NOT_A_REPOSITORY, mainCheckoutOf, SCOPE_DIR2, INIT_COMMAND2, DISK_FILE_SYSTEM2, ScopeError2, scopeAt2, initHint2, selfAndAncestors2, resolveScope2, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, positionAt, rehome, hop, goHome, HOP_FILE, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, isSessionId, sessionPhase, SessionConflictError, SessionStateError, 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, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore2, SPENDS_GLYPH, CommandExit, isRoutingWord, commandProblem, isTopLevel, commandSpelling, EffortCopyRefusal, EffortCopyFailure, copyEffortStore, RebuildRefusal, readConfigFile2, loadConfig2, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, stubOfPlanFile, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, fileStamp, keepsIdLine, fix_schema_default, minutesBetween, readCommitLog, UnionSchemaMismatch, migrateStore, MOVE_TO_SQLITE, MergeRefusal, mergeStore2, createFileSync, SyncModuleMissing, selectSync, SKILL_FILE2, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, padKind, oneLine, sessionSpawnEnv, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, spawnClaude, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS2, createAdapterRegistry2, CORE_ADAPTER_REGISTRY2, TRUSTED_PERMISSIONS2, createGhPermissions, readAuthorTrust, ghBoardTrust, readBoardTrust, requireTrustedBoardAuthor, trustRefusalClause, requireTrustedAuthor, WIRE_FORMAT2, WIRE_VERSION2, WireFormatError2, WireExportRefusal2, encodeWirePayload2, decodeWirePayload2, exportWirePayload2, materialiseWirePayload2, selectEffortStore2 };
|