@open-tomato/rafa 0.24.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -1
- package/dist/bundled/skills/dev-planner/SKILL.md +11 -0
- package/dist/cli.js +7101 -2568
- package/dist/{describe-66gth329.js → describe-vbwqxj1f.js} +3 -3
- package/dist/effort/store/index.js +4 -4
- package/dist/{index-4fwgqveh.js → index-4j1e3h26.js} +1 -1
- package/dist/{index-7qgng1rf.js → index-4rwvr6t4.js} +1481 -228
- package/dist/{index-h369efkh.js → index-6jkj0dcx.js} +4457 -2562
- package/dist/{index-amyd4dse.js → index-em1vd7kd.js} +2346 -1113
- package/dist/{index-21qkxk63.js → index-p1265xgj.js} +4 -33
- package/dist/{index-twbp38kz.js → index-rv7y8y1j.js} +36 -2
- package/dist/{index-pmkgvtfe.js → index-sf2brh4w.js} +7 -1
- package/dist/{index-w1dnwja1.js → index-xwr54rzt.js} +2 -2
- package/dist/index.js +16 -16
- package/dist/plan/index.js +3 -3
- package/package.json +1 -1
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
CONFIG_DEFAULTS,
|
|
4
|
+
CONFIG_FILE,
|
|
4
5
|
FINDING_DOMAINS,
|
|
5
6
|
FINDING_KINDS,
|
|
6
7
|
FINDING_SIGNALS,
|
|
7
8
|
activeOutput,
|
|
8
9
|
activeOutputMode,
|
|
10
|
+
configFilePath,
|
|
9
11
|
createTextOutput,
|
|
12
|
+
isStampableStub,
|
|
10
13
|
readRafaBlocks
|
|
11
|
-
} from "./index-
|
|
14
|
+
} from "./index-rv7y8y1j.js";
|
|
12
15
|
import {
|
|
13
16
|
bless,
|
|
14
17
|
merge
|
|
15
18
|
} from "./index-d6d0942q.js";
|
|
16
19
|
import {
|
|
17
20
|
commandSpelling
|
|
18
|
-
} from "./index-
|
|
21
|
+
} from "./index-4j1e3h26.js";
|
|
19
22
|
import {
|
|
23
|
+
BUSY_TIMEOUT_MAX_MS,
|
|
24
|
+
BUSY_TIMEOUT_MIN_MS,
|
|
20
25
|
RAFA_INSTINCTS_PATH,
|
|
21
26
|
STORE_BACKENDS,
|
|
22
27
|
bundledBinDirectory,
|
|
23
28
|
describeValue,
|
|
29
|
+
isBusyTimeoutMs,
|
|
24
30
|
isMapping,
|
|
25
31
|
messageOf,
|
|
26
32
|
resolveInstinctScopes,
|
|
27
33
|
tierExists
|
|
28
|
-
} from "./index-
|
|
34
|
+
} from "./index-sf2brh4w.js";
|
|
29
35
|
import {
|
|
30
36
|
actionHash,
|
|
31
37
|
triggerKey
|
|
@@ -34,6 +40,9 @@ import {
|
|
|
34
40
|
// src/adapters/registry.ts
|
|
35
41
|
import { homedir } from "os";
|
|
36
42
|
|
|
43
|
+
// src/effort/store/ndjson.ts
|
|
44
|
+
import { join as join3 } from "path";
|
|
45
|
+
|
|
37
46
|
// src/effort/store.ts
|
|
38
47
|
import {
|
|
39
48
|
appendFileSync,
|
|
@@ -53,8 +62,8 @@ function asObject(value) {
|
|
|
53
62
|
function normaliseKey(key) {
|
|
54
63
|
return typeof key === "string" && key.length > 0 ? key : null;
|
|
55
64
|
}
|
|
56
|
-
function
|
|
57
|
-
return
|
|
65
|
+
function effortStoreFileName(kind) {
|
|
66
|
+
return STORE_FILE_NAMES[kind];
|
|
58
67
|
}
|
|
59
68
|
function readStoreText(path) {
|
|
60
69
|
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
@@ -141,6 +150,54 @@ function appendRows(path, rows, keyOf) {
|
|
|
141
150
|
};
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
// src/effort/store/location.ts
|
|
154
|
+
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
155
|
+
import { tmpdir } from "os";
|
|
156
|
+
import { isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
157
|
+
var EFFORT_DIR_VARIABLE = "RAFA_EFFORT_DIR";
|
|
158
|
+
var TEST_PROCESS_VARIABLE = "RAFA_TEST";
|
|
159
|
+
var TEST_ENTRY_SUFFIX = ".test.ts";
|
|
160
|
+
function canonical(path) {
|
|
161
|
+
const resolved = resolve(path);
|
|
162
|
+
return existsSync2(resolved) ? realpathSync(resolved) : resolved;
|
|
163
|
+
}
|
|
164
|
+
function isWithin(path, dir) {
|
|
165
|
+
const between = relative(resolve(dir), resolve(path));
|
|
166
|
+
const climbsOut = between === ".." || between.startsWith(`..${sep}`);
|
|
167
|
+
return !climbsOut && !isAbsolute(between);
|
|
168
|
+
}
|
|
169
|
+
function readEffortDirOverride(repoRoot, env = process.env) {
|
|
170
|
+
const value = env[EFFORT_DIR_VARIABLE];
|
|
171
|
+
if (value === undefined || value === "")
|
|
172
|
+
return null;
|
|
173
|
+
if (!isAbsolute(value)) {
|
|
174
|
+
throw new Error(`effort store: ${EFFORT_DIR_VARIABLE} is not an absolute path (${value}); run this command again` + ` with ${EFFORT_DIR_VARIABLE} set to the absolute path 'rafa effort copy' printed.`);
|
|
175
|
+
}
|
|
176
|
+
if (canonical(value) === canonical(join2(repoRoot, EFFORT_STORE_DIR))) {
|
|
177
|
+
throw new Error(`effort store: ${EFFORT_DIR_VARIABLE} names this project's own store (${value}); it points a command` + " at a copy only. Copy the store with 'rafa effort copy' and run this command with" + ` ${EFFORT_DIR_VARIABLE}=<the copy>.`);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
function effortStoreDir(repoRoot, env = process.env) {
|
|
182
|
+
return readEffortDirOverride(repoRoot, env) ?? join2(repoRoot, EFFORT_STORE_DIR);
|
|
183
|
+
}
|
|
184
|
+
function isTestProcess(main = Bun.main, env = process.env) {
|
|
185
|
+
return main.endsWith(TEST_ENTRY_SUFFIX) || env[TEST_PROCESS_VARIABLE] === "1";
|
|
186
|
+
}
|
|
187
|
+
function isUnderTempDir(path, tempDir = tmpdir()) {
|
|
188
|
+
return isWithin(path, tempDir) || isWithin(path, canonical(tempDir));
|
|
189
|
+
}
|
|
190
|
+
function currentProbe() {
|
|
191
|
+
return { main: Bun.main, env: process.env, tempDir: tmpdir() };
|
|
192
|
+
}
|
|
193
|
+
function guardTestProcess(path, probe = currentProbe()) {
|
|
194
|
+
if (!isTestProcess(probe.main, probe.env))
|
|
195
|
+
return;
|
|
196
|
+
if (isUnderTempDir(path, probe.tempDir))
|
|
197
|
+
return;
|
|
198
|
+
throw new Error(`effort store: a test opened ${resolve(path)}, outside the temp directory ${probe.tempDir};` + " a test opens stores under tmpdir() only");
|
|
199
|
+
}
|
|
200
|
+
|
|
144
201
|
// src/effort/store/types.ts
|
|
145
202
|
function keyOrNull(value) {
|
|
146
203
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
@@ -151,15 +208,23 @@ var EFFORT_KEY_PROJECTIONS = {
|
|
|
151
208
|
};
|
|
152
209
|
|
|
153
210
|
// src/effort/store/ndjson.ts
|
|
211
|
+
function kindPath(repoRoot, kind) {
|
|
212
|
+
return join3(effortStoreDir(repoRoot), effortStoreFileName(kind));
|
|
213
|
+
}
|
|
214
|
+
function openedKindPath(repoRoot, kind) {
|
|
215
|
+
const path = kindPath(repoRoot, kind);
|
|
216
|
+
guardTestProcess(path);
|
|
217
|
+
return path;
|
|
218
|
+
}
|
|
154
219
|
function readKind(repoRoot, kind) {
|
|
155
|
-
return readStoreRows(
|
|
220
|
+
return readStoreRows(openedKindPath(repoRoot, kind));
|
|
156
221
|
}
|
|
157
222
|
function keysOfKind(repoRoot, kind) {
|
|
158
|
-
const path =
|
|
223
|
+
const path = openedKindPath(repoRoot, kind);
|
|
159
224
|
return collectedKeys(path, EFFORT_KEY_PROJECTIONS[kind]);
|
|
160
225
|
}
|
|
161
226
|
function appendKind(repoRoot, kind, rows) {
|
|
162
|
-
const path =
|
|
227
|
+
const path = openedKindPath(repoRoot, kind);
|
|
163
228
|
return appendRows(path, rows, EFFORT_KEY_PROJECTIONS[kind]);
|
|
164
229
|
}
|
|
165
230
|
function openNdjsonStore(repoRoot) {
|
|
@@ -167,19 +232,663 @@ function openNdjsonStore(repoRoot) {
|
|
|
167
232
|
append: (kind, rows) => appendKind(repoRoot, kind, rows),
|
|
168
233
|
keys: (kind) => keysOfKind(repoRoot, kind),
|
|
169
234
|
read: (kind) => readKind(repoRoot, kind).rows,
|
|
170
|
-
path: (kind) =>
|
|
235
|
+
path: (kind) => kindPath(repoRoot, kind),
|
|
171
236
|
readRows: (kind) => readKind(repoRoot, kind)
|
|
172
237
|
};
|
|
173
238
|
}
|
|
174
239
|
|
|
175
240
|
// src/effort/store/sqlite.ts
|
|
176
|
-
import { existsSync as
|
|
177
|
-
import { dirname as
|
|
241
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
242
|
+
import { dirname as dirname9, join as join9 } from "path";
|
|
178
243
|
import { Database } from "bun:sqlite";
|
|
179
244
|
|
|
245
|
+
// package.json
|
|
246
|
+
var name = "@open-tomato/rafa";
|
|
247
|
+
var version = "0.28.0";
|
|
248
|
+
|
|
249
|
+
// src/cli/version.ts
|
|
250
|
+
var RAFA_VERSION = version;
|
|
251
|
+
function versionLine() {
|
|
252
|
+
return `rafa ${RAFA_VERSION}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/effort/store/development-build.ts
|
|
256
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
257
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute3 } from "path";
|
|
258
|
+
|
|
259
|
+
// src/loop/sessions.ts
|
|
260
|
+
import {
|
|
261
|
+
linkSync,
|
|
262
|
+
mkdirSync as mkdirSync4,
|
|
263
|
+
readdirSync,
|
|
264
|
+
readFileSync as readFileSync4,
|
|
265
|
+
renameSync as renameSync3,
|
|
266
|
+
rmSync as rmSync3,
|
|
267
|
+
writeFileSync as writeFileSync3
|
|
268
|
+
} from "fs";
|
|
269
|
+
import { basename, dirname as dirname5, join as join7 } from "path";
|
|
270
|
+
|
|
271
|
+
// src/next/hop-record.ts
|
|
272
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
273
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
274
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
275
|
+
|
|
276
|
+
// src/project/position.ts
|
|
277
|
+
import { randomUUID } from "crypto";
|
|
278
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
|
|
279
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
280
|
+
|
|
281
|
+
// src/project/scope.ts
|
|
282
|
+
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
|
|
283
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
284
|
+
var SCOPE_DIR = dirname2(CONFIG_FILE);
|
|
285
|
+
var INIT_COMMAND = "rafa init";
|
|
286
|
+
var DISK_FILE_SYSTEM = Object.freeze({
|
|
287
|
+
exists: (path) => existsSync3(path),
|
|
288
|
+
realpath: (path) => realpathSync2(path)
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
class ScopeError extends Error {
|
|
292
|
+
constructor(message, options) {
|
|
293
|
+
super(`rafa scope: ${message}`, options);
|
|
294
|
+
this.name = "ScopeError";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function scopeAt(base) {
|
|
298
|
+
return { dir: join4(base, SCOPE_DIR), configFile: configFilePath(base) };
|
|
299
|
+
}
|
|
300
|
+
function initHint(start, passedUserConfig = null) {
|
|
301
|
+
const passed = passedUserConfig === null ? [] : [`${passedUserConfig} is the user scope's config and marks no project`];
|
|
302
|
+
return [
|
|
303
|
+
`not inside a rafa project: no ${CONFIG_FILE} in ${start} or any directory above it`,
|
|
304
|
+
...passed,
|
|
305
|
+
`run \`${INIT_COMMAND}\` to set one up`
|
|
306
|
+
].join(`
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
309
|
+
function requireAbsolute(label, path) {
|
|
310
|
+
if (!isAbsolute2(path)) {
|
|
311
|
+
throw new ScopeError(`${label} is ${describeValue(path)}, expected an absolute path`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function realStartOf(start, fs) {
|
|
315
|
+
try {
|
|
316
|
+
return fs.realpath(start);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const message = `start directory ${start} does not resolve (${messageOf(error)})`;
|
|
319
|
+
throw new ScopeError(message, { cause: error });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function realHomeOf(home, fs) {
|
|
323
|
+
try {
|
|
324
|
+
return fs.realpath(home);
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function selfAndAncestors(path) {
|
|
330
|
+
const parent = dirname2(path);
|
|
331
|
+
return parent === path ? [path] : [path, ...selfAndAncestors(parent)];
|
|
332
|
+
}
|
|
333
|
+
function walkUp(realStart, realHome, fs) {
|
|
334
|
+
let passedUserConfig = false;
|
|
335
|
+
for (const dir of selfAndAncestors(realStart)) {
|
|
336
|
+
if (!fs.exists(configFilePath(dir)))
|
|
337
|
+
continue;
|
|
338
|
+
if (dir !== realHome)
|
|
339
|
+
return { root: dir, passedUserConfig };
|
|
340
|
+
passedUserConfig = true;
|
|
341
|
+
}
|
|
342
|
+
return { root: null, passedUserConfig };
|
|
343
|
+
}
|
|
344
|
+
function resolveScope(start, seams) {
|
|
345
|
+
const { home, fs = DISK_FILE_SYSTEM } = seams;
|
|
346
|
+
requireAbsolute("start directory", start);
|
|
347
|
+
requireAbsolute("home directory", home);
|
|
348
|
+
const user = scopeAt(home);
|
|
349
|
+
const walk = walkUp(realStartOf(start, fs), realHomeOf(home, fs), fs);
|
|
350
|
+
if (walk.root === null) {
|
|
351
|
+
const passed = walk.passedUserConfig ? user.configFile : null;
|
|
352
|
+
return { found: false, start, home, user, hint: initHint(start, passed) };
|
|
353
|
+
}
|
|
354
|
+
return { found: true, root: walk.root, home, project: scopeAt(walk.root), user };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/project/position.ts
|
|
358
|
+
var POSITION_FILE = "position.json";
|
|
359
|
+
function positionFilePath(root) {
|
|
360
|
+
return join5(scopeAt(root).dir, POSITION_FILE);
|
|
361
|
+
}
|
|
362
|
+
function isRecord(value) {
|
|
363
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
364
|
+
}
|
|
365
|
+
function isIssueNumber(value) {
|
|
366
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
367
|
+
}
|
|
368
|
+
function asPlace(value) {
|
|
369
|
+
if (!isRecord(value) || !isIssueNumber(value.board))
|
|
370
|
+
return null;
|
|
371
|
+
if (value.epic !== null && !isIssueNumber(value.epic))
|
|
372
|
+
return null;
|
|
373
|
+
return { board: value.board, epic: value.epic };
|
|
374
|
+
}
|
|
375
|
+
function asPosition(value) {
|
|
376
|
+
if (!isRecord(value))
|
|
377
|
+
return null;
|
|
378
|
+
const current = asPlace(value.current);
|
|
379
|
+
const home = asPlace(value.home);
|
|
380
|
+
const previous = value.previous === null ? null : asPlace(value.previous);
|
|
381
|
+
if (current === null || home === null)
|
|
382
|
+
return null;
|
|
383
|
+
if (previous === null && value.previous !== null)
|
|
384
|
+
return null;
|
|
385
|
+
return { current, previous, home };
|
|
386
|
+
}
|
|
387
|
+
function readPositionFile(root) {
|
|
388
|
+
const file = positionFilePath(root);
|
|
389
|
+
let text;
|
|
390
|
+
try {
|
|
391
|
+
text = readFileSync2(file, "utf8");
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const code = error.code;
|
|
394
|
+
return code === "ENOENT" ? { set: false, reason: "absent", detail: `No position file at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
|
|
395
|
+
}
|
|
396
|
+
let parsed;
|
|
397
|
+
try {
|
|
398
|
+
parsed = JSON.parse(text);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return { set: false, reason: "invalid-json", detail: `${file} holds no JSON: ${messageOf(error)}` };
|
|
401
|
+
}
|
|
402
|
+
const position = asPosition(parsed);
|
|
403
|
+
return position === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a current, previous and home place` } : { set: true, position };
|
|
404
|
+
}
|
|
405
|
+
function writePositionFile(root, position) {
|
|
406
|
+
const file = positionFilePath(root);
|
|
407
|
+
mkdirSync2(dirname3(file), { recursive: true });
|
|
408
|
+
const temporary = `${file}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
409
|
+
const { current, previous, home } = position;
|
|
410
|
+
try {
|
|
411
|
+
writeFileSync(temporary, `${JSON.stringify({ current, previous, home }, null, 2)}
|
|
412
|
+
`);
|
|
413
|
+
renameSync(temporary, file);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
rmSync(temporary, { force: true });
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function positionAt(place) {
|
|
420
|
+
return { current: place, previous: null, home: place };
|
|
421
|
+
}
|
|
422
|
+
function rehome(position, place) {
|
|
423
|
+
return { current: place, previous: position.current, home: place };
|
|
424
|
+
}
|
|
425
|
+
function hop(position, place) {
|
|
426
|
+
return { current: place, previous: position.current, home: position.home };
|
|
427
|
+
}
|
|
428
|
+
function goHome(position) {
|
|
429
|
+
return { current: position.home, previous: position.current, home: position.home };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/next/hop-record.ts
|
|
433
|
+
var HOP_FILE = "hop.json";
|
|
434
|
+
var HOP_KINDS = ["blocker", "dry"];
|
|
435
|
+
var HOP_STATES = ["away", "waiting", "merged", "halted"];
|
|
436
|
+
function hopFilePath(root) {
|
|
437
|
+
return join6(scopeAt(root).dir, HOP_FILE);
|
|
438
|
+
}
|
|
439
|
+
function isRecord2(value) {
|
|
440
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
441
|
+
}
|
|
442
|
+
function isIssueNumber2(value) {
|
|
443
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
444
|
+
}
|
|
445
|
+
function isIssueOrNull(value) {
|
|
446
|
+
return value === null || isIssueNumber2(value);
|
|
447
|
+
}
|
|
448
|
+
function isTimestamp(value) {
|
|
449
|
+
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
450
|
+
}
|
|
451
|
+
function issuesMatchKind(kind, blocked, target) {
|
|
452
|
+
return kind === "blocker" ? blocked !== null && target !== null : blocked === null && target === null;
|
|
453
|
+
}
|
|
454
|
+
function asHopRecord(value) {
|
|
455
|
+
if (!isRecord2(value))
|
|
456
|
+
return null;
|
|
457
|
+
const { kind, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = value;
|
|
458
|
+
const home = asPlace(value.home);
|
|
459
|
+
const from = asPlace(value.from);
|
|
460
|
+
if (typeof kind !== "string" || !HOP_KINDS.includes(kind))
|
|
461
|
+
return null;
|
|
462
|
+
if (typeof state !== "string" || !HOP_STATES.includes(state))
|
|
463
|
+
return null;
|
|
464
|
+
if (home === null || from === null)
|
|
465
|
+
return null;
|
|
466
|
+
if (!isIssueOrNull(blocked) || !isIssueOrNull(target) || !isIssueOrNull(pullRequest))
|
|
467
|
+
return null;
|
|
468
|
+
if (!isIssueNumber2(targetEpic) || !isIssueNumber2(targetBoard) || !isTimestamp(startedAt))
|
|
469
|
+
return null;
|
|
470
|
+
const hopKind = kind;
|
|
471
|
+
if (!issuesMatchKind(hopKind, blocked, target))
|
|
472
|
+
return null;
|
|
473
|
+
return {
|
|
474
|
+
kind: hopKind,
|
|
475
|
+
home,
|
|
476
|
+
from,
|
|
477
|
+
blocked,
|
|
478
|
+
target,
|
|
479
|
+
targetEpic,
|
|
480
|
+
targetBoard,
|
|
481
|
+
state,
|
|
482
|
+
pullRequest,
|
|
483
|
+
startedAt
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function readHopRecord(root) {
|
|
487
|
+
const file = hopFilePath(root);
|
|
488
|
+
let text;
|
|
489
|
+
try {
|
|
490
|
+
text = readFileSync3(file, "utf8");
|
|
491
|
+
} catch (error) {
|
|
492
|
+
const code = error.code;
|
|
493
|
+
return code === "ENOENT" ? { set: false, reason: "absent", detail: `No hop record at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
|
|
494
|
+
}
|
|
495
|
+
let parsed;
|
|
496
|
+
try {
|
|
497
|
+
parsed = JSON.parse(text);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
return { set: false, reason: "wrong-shape", detail: `${file} holds no JSON: ${messageOf(error)}` };
|
|
500
|
+
}
|
|
501
|
+
const record = asHopRecord(parsed);
|
|
502
|
+
return record === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a hop record` } : { set: true, record };
|
|
503
|
+
}
|
|
504
|
+
function ownKeys(record) {
|
|
505
|
+
const { kind, home, from, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = record;
|
|
506
|
+
return {
|
|
507
|
+
kind,
|
|
508
|
+
home: { board: home.board, epic: home.epic },
|
|
509
|
+
from: { board: from.board, epic: from.epic },
|
|
510
|
+
blocked,
|
|
511
|
+
target,
|
|
512
|
+
targetEpic,
|
|
513
|
+
targetBoard,
|
|
514
|
+
state,
|
|
515
|
+
pullRequest,
|
|
516
|
+
startedAt
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function writeHopRecord(root, record) {
|
|
520
|
+
const file = hopFilePath(root);
|
|
521
|
+
mkdirSync3(dirname4(file), { recursive: true });
|
|
522
|
+
const temporary = `${file}.${String(process.pid)}.${randomUUID2()}.tmp`;
|
|
523
|
+
try {
|
|
524
|
+
writeFileSync2(temporary, `${JSON.stringify(ownKeys(record), null, 2)}
|
|
525
|
+
`);
|
|
526
|
+
renameSync2(temporary, file);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
rmSync2(temporary, { force: true });
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function staleAgainst(record, position) {
|
|
533
|
+
return position.home.board !== record.home.board || position.home.epic !== record.home.epic;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/loop/sessions.ts
|
|
537
|
+
var SESSION_STATES = Object.freeze(["running", "paused", "stopped", "done"]);
|
|
538
|
+
var RUNS_DIR = "runs";
|
|
539
|
+
var RECORD_EXTENSION = ".json";
|
|
540
|
+
var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
541
|
+
|
|
542
|
+
class SessionRecordError extends Error {
|
|
543
|
+
file;
|
|
544
|
+
constructor(file, problem) {
|
|
545
|
+
super(`session record ${file}: ${problem}`);
|
|
546
|
+
this.name = "SessionRecordError";
|
|
547
|
+
this.file = file;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
class SessionConflictError extends Error {
|
|
552
|
+
conflicts;
|
|
553
|
+
constructor(conflicts) {
|
|
554
|
+
const named = conflicts.map((conflict) => `${conflict.record.sessionId} (${conflict.reason})`);
|
|
555
|
+
super(`the plan's sessions refuse the run: ${named.join(", ")}`);
|
|
556
|
+
this.name = "SessionConflictError";
|
|
557
|
+
this.conflicts = conflicts;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
class SessionStateError extends Error {
|
|
562
|
+
file;
|
|
563
|
+
state;
|
|
564
|
+
constructor(file, state, onlyFrom) {
|
|
565
|
+
const acted = onlyFrom.length === 0 ? "no state" : onlyFrom.join(" or ");
|
|
566
|
+
super(`session record ${file}: stores ${state}, and the change acts on ${acted} alone`);
|
|
567
|
+
this.name = "SessionStateError";
|
|
568
|
+
this.file = file;
|
|
569
|
+
this.state = state;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function isSessionId(value) {
|
|
573
|
+
return SESSION_ID_PATTERN.test(value);
|
|
574
|
+
}
|
|
575
|
+
function runsDir(root) {
|
|
576
|
+
return join7(scopeAt(root).dir, RUNS_DIR);
|
|
577
|
+
}
|
|
578
|
+
function sessionFilePath(root, sessionId) {
|
|
579
|
+
if (!isSessionId(sessionId)) {
|
|
580
|
+
throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
|
|
581
|
+
}
|
|
582
|
+
return join7(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
|
|
583
|
+
}
|
|
584
|
+
function describeValue2(value) {
|
|
585
|
+
return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
|
|
586
|
+
}
|
|
587
|
+
function field(fields, key) {
|
|
588
|
+
return Object.hasOwn(fields, key) ? fields[key] : undefined;
|
|
589
|
+
}
|
|
590
|
+
function isObject(value) {
|
|
591
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
592
|
+
}
|
|
593
|
+
function isPositiveWhole(value) {
|
|
594
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
595
|
+
}
|
|
596
|
+
function isText(value) {
|
|
597
|
+
return typeof value === "string" && value.trim() !== "";
|
|
598
|
+
}
|
|
599
|
+
function taskProblem(task) {
|
|
600
|
+
if (task === null)
|
|
601
|
+
return null;
|
|
602
|
+
if (!isObject(task))
|
|
603
|
+
return `task is ${describeValue2(task)}, expected null or an object`;
|
|
604
|
+
if (!isPositiveWhole(field(task, "line"))) {
|
|
605
|
+
return `task.line is ${describeValue2(field(task, "line"))}, expected a whole number from 1`;
|
|
606
|
+
}
|
|
607
|
+
return isText(field(task, "text")) ? null : `task.text is ${describeValue2(field(task, "text"))}, expected a non-empty string`;
|
|
608
|
+
}
|
|
609
|
+
function hopProblem(fields) {
|
|
610
|
+
if (!Object.hasOwn(fields, "hop"))
|
|
611
|
+
return null;
|
|
612
|
+
const hop2 = field(fields, "hop");
|
|
613
|
+
return asHopRecord(hop2) === null ? `hop is ${describeValue2(hop2)}, expected a hop record` : null;
|
|
614
|
+
}
|
|
615
|
+
function sessionIdProblem(sessionId, file) {
|
|
616
|
+
if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
|
|
617
|
+
return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
|
|
618
|
+
}
|
|
619
|
+
return basename(file) === `${sessionId}${RECORD_EXTENSION}` ? null : `sessionId ${JSON.stringify(sessionId)} is not the file's name`;
|
|
620
|
+
}
|
|
621
|
+
function planStubProblem(planStub) {
|
|
622
|
+
return planStub === null || typeof planStub === "string" && isStampableStub(planStub) ? null : `planStub is ${describeValue2(planStub)}, expected null or a plan stub`;
|
|
623
|
+
}
|
|
624
|
+
function textProblems(fields) {
|
|
625
|
+
return ["plan", "branch"].filter((key) => !isText(field(fields, key))).map((key) => `${key} is ${describeValue2(field(fields, key))}, expected a non-empty string`);
|
|
626
|
+
}
|
|
627
|
+
function recordProblems(fields, file) {
|
|
628
|
+
const pid = field(fields, "pid");
|
|
629
|
+
const startedAt = field(fields, "startedAt");
|
|
630
|
+
const state = field(fields, "state");
|
|
631
|
+
const problems = [
|
|
632
|
+
sessionIdProblem(field(fields, "sessionId"), file),
|
|
633
|
+
planStubProblem(field(fields, "planStub")),
|
|
634
|
+
...textProblems(fields),
|
|
635
|
+
isPositiveWhole(pid) ? null : `pid is ${describeValue2(pid)}, expected a whole number from 1`,
|
|
636
|
+
typeof startedAt === "string" && !Number.isNaN(Date.parse(startedAt)) ? null : `startedAt is ${describeValue2(startedAt)}, expected a timestamp`,
|
|
637
|
+
SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
|
|
638
|
+
taskProblem(field(fields, "task")),
|
|
639
|
+
hopProblem(fields)
|
|
640
|
+
];
|
|
641
|
+
return problems.filter((problem) => problem !== null);
|
|
642
|
+
}
|
|
643
|
+
function freezeHop(value) {
|
|
644
|
+
const hop2 = asHopRecord(value);
|
|
645
|
+
return Object.freeze({ ...hop2, home: Object.freeze(hop2.home), from: Object.freeze(hop2.from) });
|
|
646
|
+
}
|
|
647
|
+
function freezeRecord(fields) {
|
|
648
|
+
const task = field(fields, "task");
|
|
649
|
+
const hop2 = field(fields, "hop");
|
|
650
|
+
return Object.freeze({
|
|
651
|
+
sessionId: field(fields, "sessionId"),
|
|
652
|
+
planStub: field(fields, "planStub"),
|
|
653
|
+
plan: field(fields, "plan"),
|
|
654
|
+
branch: field(fields, "branch"),
|
|
655
|
+
pid: field(fields, "pid"),
|
|
656
|
+
startedAt: field(fields, "startedAt"),
|
|
657
|
+
state: field(fields, "state"),
|
|
658
|
+
task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
|
|
659
|
+
...hop2 === undefined ? {} : { hop: freezeHop(hop2) }
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function parseSessionRecord(text, file) {
|
|
663
|
+
let parsed;
|
|
664
|
+
try {
|
|
665
|
+
parsed = JSON.parse(text);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
throw new SessionRecordError(file, `holds no JSON: ${messageOf(error)}`);
|
|
668
|
+
}
|
|
669
|
+
if (!isObject(parsed))
|
|
670
|
+
throw new SessionRecordError(file, "holds no JSON object");
|
|
671
|
+
const problems = recordProblems(parsed, file);
|
|
672
|
+
if (problems.length > 0)
|
|
673
|
+
throw new SessionRecordError(file, problems.join("; "));
|
|
674
|
+
return freezeRecord(parsed);
|
|
675
|
+
}
|
|
676
|
+
function errorCode(error) {
|
|
677
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
678
|
+
}
|
|
679
|
+
function isPidAlive(pid) {
|
|
680
|
+
if (!isPositiveWhole(pid))
|
|
681
|
+
return false;
|
|
682
|
+
try {
|
|
683
|
+
process.kill(pid, 0);
|
|
684
|
+
return true;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return errorCode(error) === "EPERM";
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function readState(record, isAlive = isPidAlive) {
|
|
690
|
+
if (record.state === "stopped" || record.state === "done")
|
|
691
|
+
return record.state;
|
|
692
|
+
return isAlive(record.pid) ? record.state : "stopped";
|
|
693
|
+
}
|
|
694
|
+
function readRecordFile(file) {
|
|
695
|
+
let text;
|
|
696
|
+
try {
|
|
697
|
+
text = readFileSync4(file, "utf8");
|
|
698
|
+
} catch (error) {
|
|
699
|
+
throw new SessionRecordError(file, `cannot be read: ${messageOf(error)}`);
|
|
700
|
+
}
|
|
701
|
+
return parseSessionRecord(text, file);
|
|
702
|
+
}
|
|
703
|
+
function recordNames(dir) {
|
|
704
|
+
try {
|
|
705
|
+
return readdirSync(dir);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (errorCode(error) === "ENOENT")
|
|
708
|
+
return [];
|
|
709
|
+
throw new SessionRecordError(dir, `cannot be listed: ${messageOf(error)}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function byStart(a, b) {
|
|
713
|
+
return Date.parse(a.startedAt) - Date.parse(b.startedAt) || a.sessionId.localeCompare(b.sessionId);
|
|
714
|
+
}
|
|
715
|
+
function readSessions(root, seams = {}) {
|
|
716
|
+
const dir = runsDir(root);
|
|
717
|
+
const isAlive = seams.isAlive ?? isPidAlive;
|
|
718
|
+
const records = recordNames(dir).filter((name2) => name2.endsWith(RECORD_EXTENSION)).map((name2) => readRecordFile(join7(dir, name2))).map((record) => Object.freeze({ ...record, state: readState(record, isAlive) }));
|
|
719
|
+
return Object.freeze(records.sort(byStart));
|
|
720
|
+
}
|
|
721
|
+
function readSession(root, sessionId, seams = {}) {
|
|
722
|
+
const record = readRecordFile(sessionFilePath(root, sessionId));
|
|
723
|
+
return Object.freeze({ ...record, state: readState(record, seams.isAlive ?? isPidAlive) });
|
|
724
|
+
}
|
|
725
|
+
function samePlan(a, b) {
|
|
726
|
+
if (a.planStub !== null || b.planStub !== null)
|
|
727
|
+
return a.planStub === b.planStub;
|
|
728
|
+
return a.plan === b.plan;
|
|
729
|
+
}
|
|
730
|
+
function sessionConflicts(records, candidate) {
|
|
731
|
+
return records.filter((record) => samePlan(record, candidate)).flatMap((record) => {
|
|
732
|
+
if (record.branch !== candidate.branch)
|
|
733
|
+
return [{ reason: "branch", record }];
|
|
734
|
+
return record.state === "running" || record.state === "paused" ? [{ reason: "live", record }] : [];
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function checkRecord(record, file) {
|
|
738
|
+
const problems = recordProblems(record, file);
|
|
739
|
+
if (problems.length > 0)
|
|
740
|
+
throw new SessionRecordError(file, `not written: ${problems.join("; ")}`);
|
|
741
|
+
}
|
|
742
|
+
function writeRecordFile(file, record, isNew) {
|
|
743
|
+
checkRecord(record, file);
|
|
744
|
+
mkdirSync4(dirname5(file), { recursive: true });
|
|
745
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
746
|
+
writeFileSync3(temporary, `${JSON.stringify(record, null, 2)}
|
|
747
|
+
`);
|
|
748
|
+
try {
|
|
749
|
+
if (isNew)
|
|
750
|
+
linkSync(temporary, file);
|
|
751
|
+
else
|
|
752
|
+
renameSync3(temporary, file);
|
|
753
|
+
} finally {
|
|
754
|
+
rmSync3(temporary, { force: true });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function beginSession(root, draft, seams = {}) {
|
|
758
|
+
const file = sessionFilePath(root, draft.sessionId);
|
|
759
|
+
const record = freezeRecord({ ...draft, state: "running", task: null });
|
|
760
|
+
const conflicts = sessionConflicts(readSessions(root, seams), record);
|
|
761
|
+
if (conflicts.length > 0)
|
|
762
|
+
throw new SessionConflictError(conflicts);
|
|
763
|
+
writeRecordFile(file, record, true);
|
|
764
|
+
return record;
|
|
765
|
+
}
|
|
766
|
+
function updateSession(root, sessionId, change) {
|
|
767
|
+
const file = sessionFilePath(root, sessionId);
|
|
768
|
+
const stored = readRecordFile(file);
|
|
769
|
+
if (change.onlyFrom !== undefined && !change.onlyFrom.includes(stored.state)) {
|
|
770
|
+
throw new SessionStateError(file, stored.state, change.onlyFrom);
|
|
771
|
+
}
|
|
772
|
+
const record = freezeRecord({
|
|
773
|
+
...stored,
|
|
774
|
+
state: change.state ?? stored.state,
|
|
775
|
+
task: change.task === undefined ? stored.task : change.task
|
|
776
|
+
});
|
|
777
|
+
writeRecordFile(file, record, false);
|
|
778
|
+
return record;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/runtime/identity.ts
|
|
782
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3 } from "fs";
|
|
783
|
+
import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
|
|
784
|
+
var MANIFEST = "package.json";
|
|
785
|
+
var CHECKOUT_ENTRY = join8("src", "rafa.ts");
|
|
786
|
+
function realOrResolved(path) {
|
|
787
|
+
const resolved = resolve2(path);
|
|
788
|
+
return existsSync4(resolved) ? realpathSync3(resolved) : resolved;
|
|
789
|
+
}
|
|
790
|
+
function manifestName(file) {
|
|
791
|
+
let parsed;
|
|
792
|
+
try {
|
|
793
|
+
parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
794
|
+
} catch {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
798
|
+
return null;
|
|
799
|
+
const name2 = parsed["name"];
|
|
800
|
+
return typeof name2 === "string" ? name2 : null;
|
|
801
|
+
}
|
|
802
|
+
function nearestRafaPackage(start) {
|
|
803
|
+
let dir = start;
|
|
804
|
+
for (;; ) {
|
|
805
|
+
const manifest = join8(dir, MANIFEST);
|
|
806
|
+
if (existsSync4(manifest) && manifestName(manifest) === name)
|
|
807
|
+
return dir;
|
|
808
|
+
const parent = dirname6(dir);
|
|
809
|
+
if (parent === dir)
|
|
810
|
+
return null;
|
|
811
|
+
dir = parent;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function readRuntimeIdentity(entry = Bun.main) {
|
|
815
|
+
const real = realOrResolved(entry);
|
|
816
|
+
const packageDir = nearestRafaPackage(dirname6(real));
|
|
817
|
+
if (packageDir !== null && existsSync4(join8(packageDir, CHECKOUT_ENTRY))) {
|
|
818
|
+
return { kind: "development", entry: real, checkout: packageDir };
|
|
819
|
+
}
|
|
820
|
+
return { kind: "installed", entry: real };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/effort/store/development-build.ts
|
|
824
|
+
var DEVELOPMENT_NEXT_STEP = "rafa effort copy";
|
|
825
|
+
var ADOPTION_NAME = "schema_migrations";
|
|
826
|
+
var EFFORT_DIR_NAME = "effort";
|
|
827
|
+
var DEVELOPMENT_MARK = "+dev:";
|
|
828
|
+
function appliedByName(identity) {
|
|
829
|
+
return identity.kind === "development" ? `${RAFA_VERSION}${DEVELOPMENT_MARK}${identity.checkout}` : RAFA_VERSION;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
class DevelopmentBuildRefusedError extends Error {
|
|
833
|
+
path;
|
|
834
|
+
nextStep;
|
|
835
|
+
constructor(path, message) {
|
|
836
|
+
super(message);
|
|
837
|
+
this.name = "DevelopmentBuildRefusedError";
|
|
838
|
+
this.path = path;
|
|
839
|
+
this.nextStep = DEVELOPMENT_NEXT_STEP;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function storeProjectRoot(path) {
|
|
843
|
+
const storeDir = dirname7(path);
|
|
844
|
+
const scopeDir = dirname7(storeDir);
|
|
845
|
+
if (basename2(storeDir) !== EFFORT_DIR_NAME || basename2(scopeDir) !== SCOPE_DIR)
|
|
846
|
+
return null;
|
|
847
|
+
return dirname7(scopeDir);
|
|
848
|
+
}
|
|
849
|
+
function isOwnedStore(path, env, tempDir) {
|
|
850
|
+
if (isUnderTempDir(path, tempDir))
|
|
851
|
+
return true;
|
|
852
|
+
const override = env[EFFORT_DIR_VARIABLE];
|
|
853
|
+
if (override === undefined || override === "" || !isAbsolute3(override))
|
|
854
|
+
return false;
|
|
855
|
+
return storeProjectRoot(path) === null && isUnderTempDir(path, override);
|
|
856
|
+
}
|
|
857
|
+
function loopSentence(record) {
|
|
858
|
+
return ` Loop ${record.sessionId} (pid ${String(record.pid)}, plan ${record.planStub ?? record.plan}) is running on this store.`;
|
|
859
|
+
}
|
|
860
|
+
function liveLoopSentences(path, isAlive) {
|
|
861
|
+
const root = storeProjectRoot(path);
|
|
862
|
+
if (root === null)
|
|
863
|
+
return "";
|
|
864
|
+
try {
|
|
865
|
+
return readSessions(root, { isAlive }).filter((record) => record.state === "running" || record.state === "paused").map(loopSentence).join("");
|
|
866
|
+
} catch (error) {
|
|
867
|
+
return ` The loop records under ${runsDir(root)} could not be read (${messageOf(error)}).`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function developmentRefusalText(path, needs, checkout, loops) {
|
|
871
|
+
return `effort store: ${path} needs migration ${needs.join(", ")} and this rafa is a development build (${checkout});` + ` a development build migrates only a store under the temp directory or ${EFFORT_DIR_VARIABLE}.` + ` Copy it with '${DEVELOPMENT_NEXT_STEP}' and run this command with ${EFFORT_DIR_VARIABLE}=<the copy>.` + loops;
|
|
872
|
+
}
|
|
873
|
+
function refuseUnownedDevelopmentWrite(path, needs, probe = {}) {
|
|
874
|
+
const identity = probe.identity ?? readRuntimeIdentity();
|
|
875
|
+
if (identity.kind !== "development")
|
|
876
|
+
return;
|
|
877
|
+
if (isOwnedStore(path, probe.env ?? process.env, probe.tempDir ?? tmpdir2()))
|
|
878
|
+
return;
|
|
879
|
+
const loops = liveLoopSentences(path, probe.isAlive ?? isPidAlive);
|
|
880
|
+
throw new DevelopmentBuildRefusedError(path, developmentRefusalText(path, needs, identity.checkout, loops));
|
|
881
|
+
}
|
|
882
|
+
|
|
180
883
|
// src/effort/store/migrations.ts
|
|
884
|
+
import { createHash } from "crypto";
|
|
885
|
+
var LEGACY_GATE_OPEN = 13;
|
|
886
|
+
var LEGACY_GATE_CLOSED = 1000;
|
|
181
887
|
var SQLITE_MIGRATIONS = [
|
|
182
|
-
|
|
888
|
+
{
|
|
889
|
+
id: "kind-tables",
|
|
890
|
+
breaks: [],
|
|
891
|
+
sql: `
|
|
183
892
|
CREATE TABLE sessions (
|
|
184
893
|
seq INTEGER PRIMARY KEY,
|
|
185
894
|
session_id TEXT NOT NULL UNIQUE CHECK (session_id <> ''),
|
|
@@ -191,8 +900,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
191
900
|
sha TEXT NOT NULL UNIQUE CHECK (sha <> ''),
|
|
192
901
|
row_json TEXT NOT NULL
|
|
193
902
|
);
|
|
194
|
-
`,
|
|
195
903
|
`
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
id: "findings",
|
|
907
|
+
breaks: [],
|
|
908
|
+
sql: `
|
|
196
909
|
CREATE TABLE findings (
|
|
197
910
|
seq INTEGER PRIMARY KEY,
|
|
198
911
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -220,8 +933,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
220
933
|
CREATE UNIQUE INDEX findings_by_trigger_what
|
|
221
934
|
ON findings (session_id, trigger, what)
|
|
222
935
|
WHERE artifact IS NULL;
|
|
223
|
-
`,
|
|
224
936
|
`
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
id: "blockers-and-bugs",
|
|
940
|
+
breaks: [],
|
|
941
|
+
sql: `
|
|
225
942
|
CREATE TABLE blockers (
|
|
226
943
|
seq INTEGER PRIMARY KEY,
|
|
227
944
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -252,8 +969,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
252
969
|
|
|
253
970
|
CREATE UNIQUE INDEX out_of_scope_bugs_by_entry
|
|
254
971
|
ON out_of_scope_bugs (session_id, what, ifnull(artifact, ''), ifnull(security, -1));
|
|
255
|
-
`,
|
|
256
972
|
`
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
id: "report-absences",
|
|
976
|
+
breaks: [],
|
|
977
|
+
sql: `
|
|
257
978
|
CREATE TABLE report_absences (
|
|
258
979
|
seq INTEGER PRIMARY KEY,
|
|
259
980
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -266,8 +987,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
266
987
|
outcome TEXT NOT NULL,
|
|
267
988
|
collected_at TEXT NOT NULL
|
|
268
989
|
);
|
|
269
|
-
`,
|
|
270
990
|
`
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
id: "task-reports",
|
|
994
|
+
breaks: [],
|
|
995
|
+
sql: `
|
|
271
996
|
CREATE TABLE task_reports (
|
|
272
997
|
seq INTEGER PRIMARY KEY,
|
|
273
998
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -278,8 +1003,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
278
1003
|
outcome TEXT NOT NULL,
|
|
279
1004
|
collected_at TEXT NOT NULL
|
|
280
1005
|
);
|
|
281
|
-
`,
|
|
282
1006
|
`
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
id: "preflight",
|
|
1010
|
+
breaks: [],
|
|
1011
|
+
sql: `
|
|
283
1012
|
CREATE TABLE preflight (
|
|
284
1013
|
seq INTEGER PRIMARY KEY,
|
|
285
1014
|
run_id TEXT NOT NULL CHECK (run_id <> ''),
|
|
@@ -295,8 +1024,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
295
1024
|
UNIQUE (run_id, position),
|
|
296
1025
|
CHECK ((outcome = 'pass') = (failure IS NULL))
|
|
297
1026
|
);
|
|
298
|
-
`,
|
|
299
1027
|
`
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
id: "dispatches",
|
|
1031
|
+
breaks: [],
|
|
1032
|
+
sql: `
|
|
300
1033
|
CREATE TABLE dispatches (
|
|
301
1034
|
seq INTEGER PRIMARY KEY,
|
|
302
1035
|
session_id TEXT NOT NULL UNIQUE CHECK (session_id <> ''),
|
|
@@ -311,8 +1044,12 @@ var SQLITE_MIGRATIONS = [
|
|
|
311
1044
|
flags TEXT NOT NULL,
|
|
312
1045
|
collected_at TEXT NOT NULL
|
|
313
1046
|
);
|
|
314
|
-
`,
|
|
315
1047
|
`
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
id: "changes",
|
|
1051
|
+
breaks: [],
|
|
1052
|
+
sql: `
|
|
316
1053
|
CREATE TABLE changes (
|
|
317
1054
|
seq INTEGER PRIMARY KEY,
|
|
318
1055
|
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
@@ -328,20 +1065,32 @@ var SQLITE_MIGRATIONS = [
|
|
|
328
1065
|
|
|
329
1066
|
CREATE UNIQUE INDEX changes_by_entry
|
|
330
1067
|
ON changes (session_id, level, ifnull(area, ''), summary);
|
|
331
|
-
`,
|
|
332
1068
|
`
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
id: "out-of-scope-bug-scope",
|
|
1072
|
+
breaks: [],
|
|
1073
|
+
sql: `
|
|
333
1074
|
ALTER TABLE out_of_scope_bugs
|
|
334
1075
|
ADD COLUMN scope TEXT CHECK (scope IN ('machine', 'rafa'));
|
|
335
|
-
`,
|
|
336
1076
|
`
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
id: "dispatch-skills",
|
|
1080
|
+
breaks: [],
|
|
1081
|
+
sql: `
|
|
337
1082
|
ALTER TABLE dispatches
|
|
338
1083
|
ADD COLUMN resolver TEXT CHECK (resolver IN ('planner', 'tag', 'none'));
|
|
339
1084
|
ALTER TABLE dispatches
|
|
340
1085
|
ADD COLUMN skills_offered TEXT CHECK (skills_offered IS NULL OR json_type(skills_offered) = 'array');
|
|
341
1086
|
ALTER TABLE dispatches
|
|
342
1087
|
ADD COLUMN lessons_offered TEXT CHECK (lessons_offered IS NULL OR json_type(lessons_offered) = 'array');
|
|
343
|
-
`,
|
|
344
1088
|
`
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
id: "skill-invocations",
|
|
1092
|
+
breaks: [],
|
|
1093
|
+
sql: `
|
|
345
1094
|
CREATE TABLE skill_invocations (
|
|
346
1095
|
seq INTEGER PRIMARY KEY,
|
|
347
1096
|
session_id TEXT NOT NULL CHECK (session_id <> ''),
|
|
@@ -353,12 +1102,20 @@ var SQLITE_MIGRATIONS = [
|
|
|
353
1102
|
|
|
354
1103
|
CREATE UNIQUE INDEX skill_invocations_by_use
|
|
355
1104
|
ON skill_invocations (session_id, ifnull(name, ''), ifnull(sidechain, -1));
|
|
356
|
-
`,
|
|
357
1105
|
`
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
id: "task-report-skills",
|
|
1109
|
+
breaks: [],
|
|
1110
|
+
sql: `
|
|
358
1111
|
ALTER TABLE task_reports
|
|
359
1112
|
ADD COLUMN skills_used TEXT CHECK (skills_used IS NULL OR json_type(skills_used) = 'array');
|
|
360
|
-
`,
|
|
361
1113
|
`
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
id: "plan-ci",
|
|
1117
|
+
breaks: [],
|
|
1118
|
+
sql: `
|
|
362
1119
|
CREATE TABLE plan_ci (
|
|
363
1120
|
seq INTEGER PRIMARY KEY,
|
|
364
1121
|
plan_stub TEXT NOT NULL CHECK (plan_stub <> ''),
|
|
@@ -371,13 +1128,555 @@ var SQLITE_MIGRATIONS = [
|
|
|
371
1128
|
CHECK ((verdict = 'red') = (json_array_length(failing) > 0))
|
|
372
1129
|
);
|
|
373
1130
|
`
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
id: "row-origins",
|
|
1134
|
+
breaks: [],
|
|
1135
|
+
sql: `
|
|
1136
|
+
ALTER TABLE sessions ADD COLUMN origin_store TEXT;
|
|
1137
|
+
ALTER TABLE sessions ADD COLUMN origin_seq INTEGER;
|
|
1138
|
+
CREATE UNIQUE INDEX sessions_by_origin
|
|
1139
|
+
ON sessions (origin_store, origin_seq)
|
|
1140
|
+
WHERE origin_store IS NOT NULL;
|
|
1141
|
+
|
|
1142
|
+
ALTER TABLE commits ADD COLUMN origin_store TEXT;
|
|
1143
|
+
ALTER TABLE commits ADD COLUMN origin_seq INTEGER;
|
|
1144
|
+
CREATE UNIQUE INDEX commits_by_origin
|
|
1145
|
+
ON commits (origin_store, origin_seq)
|
|
1146
|
+
WHERE origin_store IS NOT NULL;
|
|
1147
|
+
|
|
1148
|
+
ALTER TABLE findings ADD COLUMN origin_store TEXT;
|
|
1149
|
+
ALTER TABLE findings ADD COLUMN origin_seq INTEGER;
|
|
1150
|
+
CREATE UNIQUE INDEX findings_by_origin
|
|
1151
|
+
ON findings (origin_store, origin_seq)
|
|
1152
|
+
WHERE origin_store IS NOT NULL;
|
|
1153
|
+
|
|
1154
|
+
ALTER TABLE blockers ADD COLUMN origin_store TEXT;
|
|
1155
|
+
ALTER TABLE blockers ADD COLUMN origin_seq INTEGER;
|
|
1156
|
+
CREATE UNIQUE INDEX blockers_by_origin
|
|
1157
|
+
ON blockers (origin_store, origin_seq)
|
|
1158
|
+
WHERE origin_store IS NOT NULL;
|
|
1159
|
+
|
|
1160
|
+
ALTER TABLE out_of_scope_bugs ADD COLUMN origin_store TEXT;
|
|
1161
|
+
ALTER TABLE out_of_scope_bugs ADD COLUMN origin_seq INTEGER;
|
|
1162
|
+
CREATE UNIQUE INDEX out_of_scope_bugs_by_origin
|
|
1163
|
+
ON out_of_scope_bugs (origin_store, origin_seq)
|
|
1164
|
+
WHERE origin_store IS NOT NULL;
|
|
1165
|
+
|
|
1166
|
+
ALTER TABLE report_absences ADD COLUMN origin_store TEXT;
|
|
1167
|
+
ALTER TABLE report_absences ADD COLUMN origin_seq INTEGER;
|
|
1168
|
+
CREATE UNIQUE INDEX report_absences_by_origin
|
|
1169
|
+
ON report_absences (origin_store, origin_seq)
|
|
1170
|
+
WHERE origin_store IS NOT NULL;
|
|
1171
|
+
|
|
1172
|
+
ALTER TABLE task_reports ADD COLUMN origin_store TEXT;
|
|
1173
|
+
ALTER TABLE task_reports ADD COLUMN origin_seq INTEGER;
|
|
1174
|
+
CREATE UNIQUE INDEX task_reports_by_origin
|
|
1175
|
+
ON task_reports (origin_store, origin_seq)
|
|
1176
|
+
WHERE origin_store IS NOT NULL;
|
|
1177
|
+
|
|
1178
|
+
ALTER TABLE preflight ADD COLUMN origin_store TEXT;
|
|
1179
|
+
ALTER TABLE preflight ADD COLUMN origin_seq INTEGER;
|
|
1180
|
+
CREATE UNIQUE INDEX preflight_by_origin
|
|
1181
|
+
ON preflight (origin_store, origin_seq)
|
|
1182
|
+
WHERE origin_store IS NOT NULL;
|
|
1183
|
+
|
|
1184
|
+
ALTER TABLE dispatches ADD COLUMN origin_store TEXT;
|
|
1185
|
+
ALTER TABLE dispatches ADD COLUMN origin_seq INTEGER;
|
|
1186
|
+
CREATE UNIQUE INDEX dispatches_by_origin
|
|
1187
|
+
ON dispatches (origin_store, origin_seq)
|
|
1188
|
+
WHERE origin_store IS NOT NULL;
|
|
1189
|
+
|
|
1190
|
+
ALTER TABLE changes ADD COLUMN origin_store TEXT;
|
|
1191
|
+
ALTER TABLE changes ADD COLUMN origin_seq INTEGER;
|
|
1192
|
+
CREATE UNIQUE INDEX changes_by_origin
|
|
1193
|
+
ON changes (origin_store, origin_seq)
|
|
1194
|
+
WHERE origin_store IS NOT NULL;
|
|
1195
|
+
|
|
1196
|
+
ALTER TABLE skill_invocations ADD COLUMN origin_store TEXT;
|
|
1197
|
+
ALTER TABLE skill_invocations ADD COLUMN origin_seq INTEGER;
|
|
1198
|
+
CREATE UNIQUE INDEX skill_invocations_by_origin
|
|
1199
|
+
ON skill_invocations (origin_store, origin_seq)
|
|
1200
|
+
WHERE origin_store IS NOT NULL;
|
|
1201
|
+
|
|
1202
|
+
ALTER TABLE plan_ci ADD COLUMN origin_store TEXT;
|
|
1203
|
+
ALTER TABLE plan_ci ADD COLUMN origin_seq INTEGER;
|
|
1204
|
+
CREATE UNIQUE INDEX plan_ci_by_origin
|
|
1205
|
+
ON plan_ci (origin_store, origin_seq)
|
|
1206
|
+
WHERE origin_store IS NOT NULL;
|
|
1207
|
+
`
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
id: "store-meta",
|
|
1211
|
+
breaks: [],
|
|
1212
|
+
sql: `
|
|
1213
|
+
CREATE TABLE store_meta (
|
|
1214
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1215
|
+
store_id TEXT NOT NULL CHECK (store_id <> ''),
|
|
1216
|
+
project_root_commit TEXT NOT NULL CHECK (project_root_commit <> ''),
|
|
1217
|
+
project_remote TEXT CHECK (project_remote <> ''),
|
|
1218
|
+
host_id TEXT NOT NULL CHECK (host_id <> ''),
|
|
1219
|
+
store_path TEXT NOT NULL CHECK (store_path <> ''),
|
|
1220
|
+
file_dev INTEGER NOT NULL CHECK (typeof(file_dev) = 'integer'),
|
|
1221
|
+
file_ino INTEGER NOT NULL CHECK (typeof(file_ino) = 'integer'),
|
|
1222
|
+
minted_at TEXT NOT NULL CHECK (minted_at <> '')
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
CREATE TABLE merges (
|
|
1226
|
+
seq INTEGER PRIMARY KEY,
|
|
1227
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1228
|
+
other_store TEXT CHECK (other_store <> ''),
|
|
1229
|
+
merged_at TEXT NOT NULL CHECK (merged_at <> ''),
|
|
1230
|
+
rows_added INTEGER NOT NULL CHECK (typeof(rows_added) = 'integer' AND rows_added >= 0),
|
|
1231
|
+
rows_skipped INTEGER NOT NULL CHECK (typeof(rows_skipped) = 'integer' AND rows_skipped >= 0),
|
|
1232
|
+
rows_in_conflict INTEGER NOT NULL CHECK (typeof(rows_in_conflict) = 'integer' AND rows_in_conflict >= 0)
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
CREATE TABLE merge_conflicts (
|
|
1236
|
+
seq INTEGER PRIMARY KEY,
|
|
1237
|
+
merge_id TEXT NOT NULL CHECK (merge_id <> ''),
|
|
1238
|
+
table_name TEXT NOT NULL CHECK (table_name <> ''),
|
|
1239
|
+
local_seq INTEGER NOT NULL CHECK (typeof(local_seq) = 'integer'),
|
|
1240
|
+
field TEXT CHECK (field <> ''),
|
|
1241
|
+
incoming TEXT NOT NULL CHECK (json_valid(incoming) AND json_type(incoming) = 'object'),
|
|
1242
|
+
recorded_at TEXT NOT NULL CHECK (recorded_at <> '')
|
|
1243
|
+
);
|
|
1244
|
+
`
|
|
1245
|
+
}
|
|
374
1246
|
];
|
|
375
1247
|
var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
|
|
1248
|
+
function legacyGate(applied) {
|
|
1249
|
+
return applied.every(({ breaks }) => breaks.length === 0) ? LEGACY_GATE_OPEN : LEGACY_GATE_CLOSED;
|
|
1250
|
+
}
|
|
1251
|
+
function migrationChecksum(migration) {
|
|
1252
|
+
return createHash("sha256").update(migration.sql, "utf8").digest("hex");
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/effort/store/schema-plan.ts
|
|
1256
|
+
var FIX_SCHEMA_STEP = "rafa effort fix-schema --dry-run";
|
|
1257
|
+
function sqliteCatalogue(migrations) {
|
|
1258
|
+
return migrations.map(({ sql, ...spec }) => ({ ...spec, sha256: migrationChecksum({ sql }) }));
|
|
1259
|
+
}
|
|
1260
|
+
function planSchema(store, catalogue, access, caller) {
|
|
1261
|
+
const { path, userVersion, log } = store;
|
|
1262
|
+
if (log === null && (userVersion < 0 || userVersion > LEGACY_GATE_OPEN)) {
|
|
1263
|
+
return refuse("pre-log-unreleased", FIX_SCHEMA_STEP, `effort store: ${path} is at schema version` + ` ${String(userVersion)} with no migration log; no released rafa wrote that.` + " Refusing to read or write it.");
|
|
1264
|
+
}
|
|
1265
|
+
if (log !== null && userVersion !== LEGACY_GATE_OPEN && userVersion !== LEGACY_GATE_CLOSED) {
|
|
1266
|
+
return refuse("gate-mismatch", FIX_SCHEMA_STEP, `effort store: ${path} has a migration log and` + ` schema version ${String(userVersion)}; a build older than the log migrated it after the` + " log was made. Refusing to read or write it.");
|
|
1267
|
+
}
|
|
1268
|
+
const adopted = log === null ? adoptedEntries(catalogue, userVersion) : [];
|
|
1269
|
+
const held = log ?? [];
|
|
1270
|
+
const logRefusal = judgeLog(path, held, catalogue);
|
|
1271
|
+
if (logRefusal !== null)
|
|
1272
|
+
return logRefusal;
|
|
1273
|
+
const heldIds = new Set([...adopted, ...held].map(({ id }) => id));
|
|
1274
|
+
const pending = catalogue.filter(({ id }) => !heldIds.has(id));
|
|
1275
|
+
const unknown = held.filter(({ id }) => !catalogue.some((entry) => entry.id === id));
|
|
1276
|
+
const writes = access === "write" || pending.length > 0;
|
|
1277
|
+
const writersBroken = unknown.find(({ breaks }) => breaks.includes("writers"));
|
|
1278
|
+
if (writes && writersBroken !== undefined) {
|
|
1279
|
+
return refuseUnknown("unknown-breaks-writers", path, writersBroken);
|
|
1280
|
+
}
|
|
1281
|
+
const orderRefusal = judgePending(path, catalogue, heldIds, pending, caller);
|
|
1282
|
+
if (orderRefusal !== null)
|
|
1283
|
+
return orderRefusal;
|
|
1284
|
+
return {
|
|
1285
|
+
verdict: "use",
|
|
1286
|
+
adopted,
|
|
1287
|
+
pending,
|
|
1288
|
+
unknown,
|
|
1289
|
+
gate: legacyGate([...adopted, ...held, ...pending])
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
function adoptedEntries(catalogue, count) {
|
|
1293
|
+
const adopted = catalogue.slice(0, count);
|
|
1294
|
+
if (adopted.length < count) {
|
|
1295
|
+
throw new Error(`schema plan: the catalogue holds ${String(catalogue.length)} migrations,` + ` fewer than the ${String(count)} legacy entries a store at that version holds`);
|
|
1296
|
+
}
|
|
1297
|
+
return adopted;
|
|
1298
|
+
}
|
|
1299
|
+
function judgeLog(path, log, catalogue) {
|
|
1300
|
+
const byId = new Map(catalogue.map((entry) => [entry.id, entry]));
|
|
1301
|
+
for (const row of log) {
|
|
1302
|
+
const known = byId.get(row.id);
|
|
1303
|
+
if (known !== undefined && known.sha256 !== row.sha256) {
|
|
1304
|
+
return refuse("edited", FIX_SCHEMA_STEP, `effort store: ${path} recorded migration ${row.id}` + ` as sha256 ${row.sha256}, and this rafa holds it as ${known.sha256}; one was edited` + " after it ran. Refusing to read or write the store.");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const readersBroken = log.find(({ id, breaks }) => !byId.has(id) && breaks.some((word) => word !== "writers"));
|
|
1308
|
+
return readersBroken === undefined ? null : refuseUnknown("unknown-breaks-readers", path, readersBroken);
|
|
1309
|
+
}
|
|
1310
|
+
function judgePending(path, catalogue, heldIds, pending, caller) {
|
|
1311
|
+
const breaking = pending.filter(({ breaks }) => breaks.length > 0);
|
|
1312
|
+
for (const entry of breaking) {
|
|
1313
|
+
const later = catalogue.slice(catalogue.indexOf(entry) + 1).find(({ id }) => heldIds.has(id));
|
|
1314
|
+
if (later !== undefined) {
|
|
1315
|
+
return refuse("breaking-out-of-order", "rafa effort schema", `effort store: ${path} needs` + ` migration ${entry.id} ahead of ${later.id}, which is already applied; a migration that` + " breaks older runtimes applies only in order. Refusing to read or write the store.");
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const [first] = breaking;
|
|
1319
|
+
if (first === undefined || caller === "migrate")
|
|
1320
|
+
return null;
|
|
1321
|
+
return refuse("breaking-pending", "rafa effort migrate --dry-run", `effort store: ${path} needs` + ` migration ${first.id}, which breaks older runtimes and is applied only by` + " 'rafa effort migrate'; refusing to read or write the store until then.");
|
|
1322
|
+
}
|
|
1323
|
+
function refuseUnknown(reason, path, row) {
|
|
1324
|
+
const ending = reason === "unknown-breaks-readers" ? "which breaks older readers; refusing to read or write it." : "which breaks older writers; refusing to write it.";
|
|
1325
|
+
return refuse(reason, `install a rafa that knows ${row.id}, or ${FIX_SCHEMA_STEP}`, "effort store:" + ` ${path} holds migration ${row.id} (applied by ${row.appliedBy} on ${dateOf(row.appliedAt)}),` + ` which this rafa does not know and ${ending}`);
|
|
1326
|
+
}
|
|
1327
|
+
function dateOf(appliedAt) {
|
|
1328
|
+
return /^\d{4}-\d{2}-\d{2}/.test(appliedAt) ? appliedAt.slice(0, 10) : appliedAt;
|
|
1329
|
+
}
|
|
1330
|
+
function refuse(reason, nextStep, text) {
|
|
1331
|
+
return { verdict: "refuse", reason, message: `${text} Next safe step: ${nextStep}`, nextStep };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/effort/store/bring-forward.ts
|
|
1335
|
+
var MIGRATION_LOG_TABLE = "schema_migrations";
|
|
1336
|
+
var CREATE_MIGRATION_LOG = `
|
|
1337
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATION_LOG_TABLE} (
|
|
1338
|
+
seq INTEGER PRIMARY KEY,
|
|
1339
|
+
id TEXT NOT NULL UNIQUE CHECK (id <> ''),
|
|
1340
|
+
sha256 TEXT NOT NULL CHECK (length(sha256) = 64),
|
|
1341
|
+
breaks TEXT NOT NULL,
|
|
1342
|
+
applied_at TEXT NOT NULL,
|
|
1343
|
+
applied_by TEXT NOT NULL CHECK (applied_by <> '')
|
|
1344
|
+
);
|
|
1345
|
+
`;
|
|
1346
|
+
|
|
1347
|
+
class SchemaRefusedError extends Error {
|
|
1348
|
+
reason;
|
|
1349
|
+
nextStep;
|
|
1350
|
+
constructor(refusal) {
|
|
1351
|
+
super(refusal.message);
|
|
1352
|
+
this.name = "SchemaRefusedError";
|
|
1353
|
+
this.reason = refusal.reason;
|
|
1354
|
+
this.nextStep = refusal.nextStep;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function bringForward(db, path, access, caller, options = {}) {
|
|
1358
|
+
const migrations = options.migrations ?? SQLITE_MIGRATIONS;
|
|
1359
|
+
const catalogue = sqliteCatalogue(migrations);
|
|
1360
|
+
const first = usablePlan(readStoreSchema(db, path), catalogue, access, caller);
|
|
1361
|
+
if (!hasWork(first))
|
|
1362
|
+
return nothingWritten(db);
|
|
1363
|
+
if (options.builtAside !== true)
|
|
1364
|
+
refuseUnownedDevelopmentWrite(path, writeNames(first), options);
|
|
1365
|
+
const apply = db.transaction(() => {
|
|
1366
|
+
const store = readStoreSchema(db, path);
|
|
1367
|
+
const plan = usablePlan(store, catalogue, access, caller);
|
|
1368
|
+
if (!hasWork(plan))
|
|
1369
|
+
return nothingWritten(db);
|
|
1370
|
+
const stamp = {
|
|
1371
|
+
appliedAt: (options.now ?? (() => new Date))().toISOString(),
|
|
1372
|
+
appliedBy: options.appliedBy ?? RAFA_VERSION
|
|
1373
|
+
};
|
|
1374
|
+
db.run(CREATE_MIGRATION_LOG);
|
|
1375
|
+
for (const entry of plan.adopted)
|
|
1376
|
+
logMigration(db, entry, stamp);
|
|
1377
|
+
for (const entry of plan.pending) {
|
|
1378
|
+
db.run(bodyOf(migrations, entry.id));
|
|
1379
|
+
logMigration(db, entry, stamp);
|
|
1380
|
+
}
|
|
1381
|
+
db.run(`PRAGMA user_version = ${String(plan.gate)}`);
|
|
1382
|
+
return {
|
|
1383
|
+
adopted: plan.adopted.map(({ id }) => id),
|
|
1384
|
+
applied: plan.pending.map(({ id }) => id),
|
|
1385
|
+
userVersion: plan.gate
|
|
1386
|
+
};
|
|
1387
|
+
});
|
|
1388
|
+
return apply.immediate();
|
|
1389
|
+
}
|
|
1390
|
+
function readStoreSchema(db, path) {
|
|
1391
|
+
return { path, userVersion: readUserVersion(db), log: readMigrationLog(db, path) };
|
|
1392
|
+
}
|
|
1393
|
+
function usablePlan(store, catalogue, access, caller) {
|
|
1394
|
+
const plan = planSchema(store, catalogue, access, caller);
|
|
1395
|
+
if (plan.verdict === "refuse")
|
|
1396
|
+
throw new SchemaRefusedError(plan);
|
|
1397
|
+
return plan;
|
|
1398
|
+
}
|
|
1399
|
+
function hasWork(plan) {
|
|
1400
|
+
return plan.adopted.length > 0 || plan.pending.length > 0;
|
|
1401
|
+
}
|
|
1402
|
+
function writeNames(plan) {
|
|
1403
|
+
const adoption = plan.adopted.length > 0 ? [ADOPTION_NAME] : [];
|
|
1404
|
+
return [...adoption, ...plan.pending.map(({ id }) => id)];
|
|
1405
|
+
}
|
|
1406
|
+
function nothingWritten(db) {
|
|
1407
|
+
return { adopted: [], applied: [], userVersion: readUserVersion(db) };
|
|
1408
|
+
}
|
|
1409
|
+
function readUserVersion(db) {
|
|
1410
|
+
const row = db.query("PRAGMA user_version").get();
|
|
1411
|
+
return row?.user_version ?? 0;
|
|
1412
|
+
}
|
|
1413
|
+
function readMigrationLog(db, path) {
|
|
1414
|
+
const table = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE);
|
|
1415
|
+
if (table === null)
|
|
1416
|
+
return null;
|
|
1417
|
+
return db.query(`SELECT id, sha256, breaks, applied_at, applied_by FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map((row) => ({
|
|
1418
|
+
id: row.id,
|
|
1419
|
+
sha256: row.sha256,
|
|
1420
|
+
breaks: parseBreaks(row.breaks, row.id, path),
|
|
1421
|
+
appliedAt: row.applied_at,
|
|
1422
|
+
appliedBy: row.applied_by
|
|
1423
|
+
}));
|
|
1424
|
+
}
|
|
1425
|
+
function parseBreaks(text, id, path) {
|
|
1426
|
+
const refused = new Error(`effort store: ${path} logs migration ${id} with breaks ${text},` + " which is not a JSON array of words; refusing to read or write it");
|
|
1427
|
+
let parsed;
|
|
1428
|
+
try {
|
|
1429
|
+
parsed = JSON.parse(text);
|
|
1430
|
+
} catch (cause) {
|
|
1431
|
+
throw new Error(refused.message, { cause });
|
|
1432
|
+
}
|
|
1433
|
+
if (!Array.isArray(parsed) || !parsed.every((word) => typeof word === "string"))
|
|
1434
|
+
throw refused;
|
|
1435
|
+
return parsed;
|
|
1436
|
+
}
|
|
1437
|
+
function bodyOf(migrations, id) {
|
|
1438
|
+
const migration = migrations.find((candidate) => candidate.id === id);
|
|
1439
|
+
if (migration === undefined)
|
|
1440
|
+
throw new Error(`bring forward: no migration ${id} in the catalogue it planned from`);
|
|
1441
|
+
return migration.sql;
|
|
1442
|
+
}
|
|
1443
|
+
function logMigration(db, entry, stamp) {
|
|
1444
|
+
db.run(`INSERT INTO ${MIGRATION_LOG_TABLE} (id, sha256, breaks, applied_at, applied_by) VALUES (?, ?, ?, ?, ?)`, [entry.id, entry.sha256, JSON.stringify(entry.breaks), stamp.appliedAt, stamp.appliedBy]);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/effort/store/origins.ts
|
|
1448
|
+
var ACROSS_STORES_ORDER = "collected_at, origin_store, origin_seq, seq";
|
|
1449
|
+
var STAMPED_COLUMNS = "seq, origin_store, origin_seq";
|
|
1450
|
+
var STORE_ID = "(SELECT store_id FROM store_meta WHERE id = 1)";
|
|
1451
|
+
function stampedValues(table) {
|
|
1452
|
+
const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${table})`;
|
|
1453
|
+
return `${nextSeq}, ${STORE_ID}, CASE WHEN ${STORE_ID} IS NULL THEN NULL ELSE ${nextSeq} END`;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/effort/store/settings.ts
|
|
1457
|
+
var DEFAULT_STORE_SETTINGS = Object.freeze({
|
|
1458
|
+
busyTimeoutMs: CONFIG_DEFAULTS.effortBusyTimeoutMs
|
|
1459
|
+
});
|
|
1460
|
+
var active = DEFAULT_STORE_SETTINGS;
|
|
1461
|
+
function setActiveStoreSettings(settings) {
|
|
1462
|
+
if (settings === null) {
|
|
1463
|
+
active = DEFAULT_STORE_SETTINGS;
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (!isBusyTimeoutMs(settings.busyTimeoutMs)) {
|
|
1467
|
+
throw new RangeError(`effort store: busy timeout ${String(settings.busyTimeoutMs)} is not a whole number ` + `of milliseconds from ${String(BUSY_TIMEOUT_MIN_MS)} to ${String(BUSY_TIMEOUT_MAX_MS)}`);
|
|
1468
|
+
}
|
|
1469
|
+
active = Object.freeze({ busyTimeoutMs: settings.busyTimeoutMs });
|
|
1470
|
+
}
|
|
1471
|
+
function activeStoreSettings() {
|
|
1472
|
+
return active;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// src/effort/store/store-meta.ts
|
|
1476
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1477
|
+
import { dirname as dirname8 } from "path";
|
|
1478
|
+
|
|
1479
|
+
// src/effort/store/store-identity.ts
|
|
1480
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1481
|
+
import { createHmac } from "crypto";
|
|
1482
|
+
import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync } from "fs";
|
|
1483
|
+
import { hostname, platform as currentPlatform } from "os";
|
|
1484
|
+
|
|
1485
|
+
// src/pr/git.ts
|
|
1486
|
+
import { spawnSync } from "child_process";
|
|
1487
|
+
function createGitRunner(cwd) {
|
|
1488
|
+
return (args) => {
|
|
1489
|
+
const result = spawnSync("git", [...args], {
|
|
1490
|
+
cwd,
|
|
1491
|
+
encoding: "utf8",
|
|
1492
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
1493
|
+
});
|
|
1494
|
+
if (result.error !== undefined) {
|
|
1495
|
+
return { ok: false, stdout: "", stderr: `could not run git in ${cwd}: ${messageOf(result.error)}` };
|
|
1496
|
+
}
|
|
1497
|
+
return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function gitSaid(result) {
|
|
1501
|
+
return [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream !== "").join(`
|
|
1502
|
+
`);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// src/schema/project-id.ts
|
|
1506
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1507
|
+
var PROJECT_ID_LENGTH = 12;
|
|
1508
|
+
var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
|
|
1509
|
+
function hasColonAfterAt(url) {
|
|
1510
|
+
const at = url.indexOf("@");
|
|
1511
|
+
return at !== -1 && url.lastIndexOf(":") > at;
|
|
1512
|
+
}
|
|
1513
|
+
function normalizeRemote(url) {
|
|
1514
|
+
const stripped = url.replace(/:\/\/[^@]+@/, "://");
|
|
1515
|
+
const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
|
|
1516
|
+
const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
1517
|
+
return network ? trimmed.toLowerCase() : trimmed;
|
|
1518
|
+
}
|
|
1519
|
+
function gitRemoteUrl(dir) {
|
|
1520
|
+
const result = spawnSync2("git", ["remote", "get-url", "origin"], {
|
|
1521
|
+
cwd: dir,
|
|
1522
|
+
encoding: "utf8",
|
|
1523
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
1524
|
+
});
|
|
1525
|
+
if (result.error !== undefined || result.status !== 0)
|
|
1526
|
+
return null;
|
|
1527
|
+
const url = result.stdout.trim();
|
|
1528
|
+
return url === "" ? null : url;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// src/effort/store/store-identity.ts
|
|
1532
|
+
var HOST_ID_KEY = "rafa effort store host id";
|
|
1533
|
+
var MACHINE_ID_PATH = "/etc/machine-id";
|
|
1534
|
+
var IOREG_ARGS = ["-rd1", "-c", "IOPlatformExpertDevice"];
|
|
1535
|
+
function hostIdFrom(source, value) {
|
|
1536
|
+
return createHmac("sha256", HOST_ID_KEY).update(`${source}:${value}`).digest("hex");
|
|
1537
|
+
}
|
|
1538
|
+
function readMachineIdFile() {
|
|
1539
|
+
try {
|
|
1540
|
+
const value = readFileSync6(MACHINE_ID_PATH, "utf8").trim();
|
|
1541
|
+
return value === "" ? null : value;
|
|
1542
|
+
} catch {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function parseIoregPlatformUuid(output) {
|
|
1547
|
+
const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(output);
|
|
1548
|
+
return match?.[1] ?? null;
|
|
1549
|
+
}
|
|
1550
|
+
function readIoregPlatformUuid() {
|
|
1551
|
+
const result = spawnSync3("ioreg", [...IOREG_ARGS], { encoding: "utf8" });
|
|
1552
|
+
if (result.error !== undefined || result.status !== 0)
|
|
1553
|
+
return null;
|
|
1554
|
+
return parseIoregPlatformUuid(result.stdout);
|
|
1555
|
+
}
|
|
1556
|
+
function readHostId(seams = {}) {
|
|
1557
|
+
const platform = seams.platform ?? currentPlatform();
|
|
1558
|
+
if (platform === "linux") {
|
|
1559
|
+
const machineId = (seams.readMachineId ?? readMachineIdFile)();
|
|
1560
|
+
if (machineId !== null)
|
|
1561
|
+
return hostIdFrom("machine-id", machineId);
|
|
1562
|
+
}
|
|
1563
|
+
if (platform === "darwin") {
|
|
1564
|
+
const platformUuid = (seams.readPlatformUuid ?? readIoregPlatformUuid)();
|
|
1565
|
+
if (platformUuid !== null)
|
|
1566
|
+
return hostIdFrom("platform-uuid", platformUuid);
|
|
1567
|
+
}
|
|
1568
|
+
return hostIdFrom("hostname", (seams.readHostname ?? hostname)());
|
|
1569
|
+
}
|
|
1570
|
+
function readStoreFileFacts(path) {
|
|
1571
|
+
const storePath = realpathSync4(path);
|
|
1572
|
+
const stats = statSync(storePath, { bigint: true });
|
|
1573
|
+
return { storePath, fileDev: stats.dev, fileIno: stats.ino };
|
|
1574
|
+
}
|
|
1575
|
+
function observeStore(path, readHost = readHostId) {
|
|
1576
|
+
return { hostId: readHost(), ...readStoreFileFacts(path) };
|
|
1577
|
+
}
|
|
1578
|
+
function movedFacts(recorded, facts) {
|
|
1579
|
+
const sameFile = BigInt(recorded.fileDev) === facts.fileDev && BigInt(recorded.fileIno) === facts.fileIno;
|
|
1580
|
+
const moved = [
|
|
1581
|
+
["host", recorded.hostId !== facts.hostId],
|
|
1582
|
+
["path", recorded.storePath !== facts.storePath],
|
|
1583
|
+
["file", !sameFile]
|
|
1584
|
+
];
|
|
1585
|
+
return moved.filter(([, differs]) => differs).map(([reason]) => reason);
|
|
1586
|
+
}
|
|
1587
|
+
function decideStoreIdentity(access, recorded, observe) {
|
|
1588
|
+
if (access === "read")
|
|
1589
|
+
return { action: "none" };
|
|
1590
|
+
const facts = observe();
|
|
1591
|
+
if (recorded === null)
|
|
1592
|
+
return { action: "mint", reasons: ["unminted"], facts };
|
|
1593
|
+
const reasons = movedFacts(recorded, facts);
|
|
1594
|
+
return reasons.length === 0 ? { action: "keep", storeId: recorded.storeId } : { action: "mint", reasons, facts };
|
|
1595
|
+
}
|
|
1596
|
+
function smallestRootCommit(git) {
|
|
1597
|
+
const result = git(["rev-list", "--max-parents=0", "HEAD"]);
|
|
1598
|
+
if (!result.ok)
|
|
1599
|
+
return null;
|
|
1600
|
+
const roots = result.stdout.split(`
|
|
1601
|
+
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
1602
|
+
return [...roots].sort()[0] ?? null;
|
|
1603
|
+
}
|
|
1604
|
+
function normalisedRemote(git) {
|
|
1605
|
+
const result = git(["remote", "get-url", "origin"]);
|
|
1606
|
+
if (!result.ok)
|
|
1607
|
+
return null;
|
|
1608
|
+
const remote = normalizeRemote(result.stdout.trim());
|
|
1609
|
+
return remote === "" ? null : remote;
|
|
1610
|
+
}
|
|
1611
|
+
function readProjectIdentity(dir, git = createGitRunner(dir)) {
|
|
1612
|
+
return { rootCommit: smallestRootCommit(git), remote: normalisedRemote(git) };
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/effort/store/store-meta.ts
|
|
1616
|
+
var STORED_INTEGER_BITS = 64;
|
|
1617
|
+
function toStoredInteger(value) {
|
|
1618
|
+
return BigInt.asIntN(STORED_INTEGER_BITS, value);
|
|
1619
|
+
}
|
|
1620
|
+
function fromStoredInteger(text) {
|
|
1621
|
+
return BigInt.asUintN(STORED_INTEGER_BITS, BigInt(text));
|
|
1622
|
+
}
|
|
1623
|
+
function readStoreMeta(db) {
|
|
1624
|
+
const row = db.query("SELECT store_id, project_root_commit, project_remote, host_id, store_path," + " CAST(file_dev AS TEXT) AS file_dev, CAST(file_ino AS TEXT) AS file_ino, minted_at" + " FROM store_meta WHERE id = 1").get();
|
|
1625
|
+
if (row === null)
|
|
1626
|
+
return null;
|
|
1627
|
+
return {
|
|
1628
|
+
storeId: row.store_id,
|
|
1629
|
+
projectRootCommit: row.project_root_commit,
|
|
1630
|
+
projectRemote: row.project_remote,
|
|
1631
|
+
hostId: row.host_id,
|
|
1632
|
+
storePath: row.store_path,
|
|
1633
|
+
fileDev: fromStoredInteger(row.file_dev),
|
|
1634
|
+
fileIno: fromStoredInteger(row.file_ino),
|
|
1635
|
+
mintedAt: row.minted_at
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function projectFor(storePath, recorded, seams) {
|
|
1639
|
+
const read = (seams.readProject ?? readProjectIdentity)(dirname8(storePath));
|
|
1640
|
+
if (read.rootCommit !== null)
|
|
1641
|
+
return { rootCommit: read.rootCommit, remote: read.remote };
|
|
1642
|
+
if (recorded === null)
|
|
1643
|
+
return null;
|
|
1644
|
+
return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
|
|
1645
|
+
}
|
|
1646
|
+
function writeStoreMeta(db, facts, project, seams) {
|
|
1647
|
+
const storeId = (seams.newStoreId ?? randomUUID3)();
|
|
1648
|
+
const mintedAt = (seams.now?.() ?? new Date).toISOString();
|
|
1649
|
+
db.query("INSERT OR REPLACE INTO store_meta (id, store_id, project_root_commit, project_remote," + " host_id, store_path, file_dev, file_ino, minted_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)").run(storeId, project.rootCommit, project.remote, facts.hostId, facts.storePath, toStoredInteger(facts.fileDev), toStoredInteger(facts.fileIno), mintedAt);
|
|
1650
|
+
return storeId;
|
|
1651
|
+
}
|
|
1652
|
+
function unminted(decision) {
|
|
1653
|
+
return decision.action === "keep" ? { action: "keep", storeId: decision.storeId } : { action: "none" };
|
|
1654
|
+
}
|
|
1655
|
+
function settleStoreIdentity(db, path, access, seams = {}) {
|
|
1656
|
+
if (access === "read")
|
|
1657
|
+
return { action: "none" };
|
|
1658
|
+
const observe = () => observeStore(path, seams.readHostId ?? readHostId);
|
|
1659
|
+
const recorded = readStoreMeta(db);
|
|
1660
|
+
const first = decideStoreIdentity(access, recorded, observe);
|
|
1661
|
+
if (first.action !== "mint")
|
|
1662
|
+
return unminted(first);
|
|
1663
|
+
const project = projectFor(first.facts.storePath, recorded, seams);
|
|
1664
|
+
if (project === null)
|
|
1665
|
+
return { action: "no-project", reasons: first.reasons };
|
|
1666
|
+
const mintUnderLock = db.transaction(() => {
|
|
1667
|
+
const second = decideStoreIdentity(access, readStoreMeta(db), observe);
|
|
1668
|
+
if (second.action !== "mint")
|
|
1669
|
+
return unminted(second);
|
|
1670
|
+
const storeId = writeStoreMeta(db, second.facts, project, seams);
|
|
1671
|
+
return { action: "mint", storeId, reasons: second.reasons };
|
|
1672
|
+
});
|
|
1673
|
+
return mintUnderLock.immediate();
|
|
1674
|
+
}
|
|
376
1675
|
|
|
377
1676
|
// src/effort/store/sqlite.ts
|
|
378
|
-
var
|
|
1677
|
+
var SQLITE_STORE_FILE_NAME = "effort.sqlite";
|
|
379
1678
|
function sqliteStorePath(repoRoot) {
|
|
380
|
-
return
|
|
1679
|
+
return join9(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
|
|
381
1680
|
}
|
|
382
1681
|
var KIND_TABLES = {
|
|
383
1682
|
sessions: { name: "sessions", keyColumn: "session_id" },
|
|
@@ -405,51 +1704,31 @@ function batchEntries(kind, rows) {
|
|
|
405
1704
|
}
|
|
406
1705
|
return keyed.map(({ key, row }) => ({ key, body: JSON.stringify(row) }));
|
|
407
1706
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
return row?.user_version ?? 0;
|
|
411
|
-
}
|
|
412
|
-
function checkedVersion(db, path, latest) {
|
|
413
|
-
const version = userVersion(db);
|
|
414
|
-
if (version > latest) {
|
|
415
|
-
throw new Error(`effort store: ${path} is at schema version ${version}, past the` + ` ${latest} this rafa knows; refusing to read or write it`);
|
|
416
|
-
}
|
|
417
|
-
return version;
|
|
418
|
-
}
|
|
419
|
-
function migrateSchema(db, path, migrations = SQLITE_MIGRATIONS) {
|
|
420
|
-
const latest = migrations.length;
|
|
421
|
-
if (checkedVersion(db, path, latest) === latest)
|
|
422
|
-
return;
|
|
423
|
-
const applyPending = db.transaction(() => {
|
|
424
|
-
const from = checkedVersion(db, path, latest);
|
|
425
|
-
for (const sql of migrations.slice(from))
|
|
426
|
-
db.run(sql);
|
|
427
|
-
db.run(`PRAGMA user_version = ${latest}`);
|
|
428
|
-
});
|
|
429
|
-
applyPending.immediate();
|
|
430
|
-
}
|
|
431
|
-
function withSqliteStore(path, create, use) {
|
|
1707
|
+
function withSqliteStore(path, access, create, use, identity = {}) {
|
|
1708
|
+
guardTestProcess(path);
|
|
432
1709
|
if (create)
|
|
433
|
-
|
|
1710
|
+
mkdirSync5(dirname9(path), { recursive: true });
|
|
434
1711
|
const db = new Database(path, { readwrite: true, create });
|
|
435
1712
|
try {
|
|
436
|
-
|
|
1713
|
+
db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
|
|
1714
|
+
bringForward(db, path, access, "open");
|
|
1715
|
+
settleStoreIdentity(db, path, access, identity);
|
|
437
1716
|
return use(db);
|
|
438
1717
|
} finally {
|
|
439
1718
|
db.close();
|
|
440
1719
|
}
|
|
441
1720
|
}
|
|
442
1721
|
function insertBatch(db, table, entries) {
|
|
443
|
-
const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json) VALUES (?,
|
|
1722
|
+
const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json, ${STAMPED_COLUMNS})` + ` VALUES (?, ?, ${stampedValues(table.name)})` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
|
|
444
1723
|
const insertAll = db.transaction(() => entries.reduce((appended, { key, body }) => appended + insert.run(key, body).changes, 0));
|
|
445
1724
|
return insertAll.immediate();
|
|
446
1725
|
}
|
|
447
1726
|
function writeSqliteStore(path, rowCount, empty, insert) {
|
|
448
|
-
const exists =
|
|
1727
|
+
const exists = existsSync5(path);
|
|
449
1728
|
if (rowCount > 0)
|
|
450
|
-
return withSqliteStore(path, !exists, insert);
|
|
1729
|
+
return withSqliteStore(path, "write", !exists, insert);
|
|
451
1730
|
if (exists)
|
|
452
|
-
withSqliteStore(path, false, () => {
|
|
1731
|
+
withSqliteStore(path, "write", false, () => {
|
|
453
1732
|
return;
|
|
454
1733
|
});
|
|
455
1734
|
return empty;
|
|
@@ -461,10 +1740,10 @@ function appendKind2(path, kind, rows) {
|
|
|
461
1740
|
return { path, appended, skipped: entries.length - appended };
|
|
462
1741
|
}
|
|
463
1742
|
function keysOfKind2(path, kind) {
|
|
464
|
-
if (!
|
|
1743
|
+
if (!existsSync5(path))
|
|
465
1744
|
return new Set;
|
|
466
|
-
const { name, keyColumn } = KIND_TABLES[kind];
|
|
467
|
-
const keys = withSqliteStore(path, false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${
|
|
1745
|
+
const { name: name2, keyColumn } = KIND_TABLES[kind];
|
|
1746
|
+
const keys = withSqliteStore(path, "read", false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${name2} ORDER BY seq`).all());
|
|
468
1747
|
return new Set(keys.map(({ key }) => key));
|
|
469
1748
|
}
|
|
470
1749
|
function parsedObject(body) {
|
|
@@ -483,11 +1762,11 @@ function rowOf(body, where) {
|
|
|
483
1762
|
return value;
|
|
484
1763
|
}
|
|
485
1764
|
function readKind2(path, kind) {
|
|
486
|
-
if (!
|
|
1765
|
+
if (!existsSync5(path))
|
|
487
1766
|
return [];
|
|
488
|
-
const { name } = KIND_TABLES[kind];
|
|
489
|
-
const stored = withSqliteStore(path, false, (db) => db.query(`SELECT seq, row_json AS body FROM ${
|
|
490
|
-
return stored.map(({ seq, body }) => rowOf(body, `${path}: ${
|
|
1767
|
+
const { name: name2 } = KIND_TABLES[kind];
|
|
1768
|
+
const stored = withSqliteStore(path, "read", false, (db) => db.query(`SELECT seq, row_json AS body FROM ${name2} ORDER BY seq`).all());
|
|
1769
|
+
return stored.map(({ seq, body }) => rowOf(body, `${path}: ${name2} row ${seq}`));
|
|
491
1770
|
}
|
|
492
1771
|
function openSqliteStore(repoRoot) {
|
|
493
1772
|
const path = sqliteStorePath(repoRoot);
|
|
@@ -501,15 +1780,15 @@ function openSqliteStore(repoRoot) {
|
|
|
501
1780
|
|
|
502
1781
|
// src/adapters/learning/local.ts
|
|
503
1782
|
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
504
|
-
import { join as
|
|
1783
|
+
import { join as join12 } from "path";
|
|
505
1784
|
|
|
506
1785
|
// src/commands/instinct/instinct-records.ts
|
|
507
|
-
import { readdirSync as
|
|
508
|
-
import { join as
|
|
1786
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
|
|
1787
|
+
import { join as join11 } from "path";
|
|
509
1788
|
|
|
510
1789
|
// src/check/layout.ts
|
|
511
|
-
import { readdirSync, statSync } from "fs";
|
|
512
|
-
import { join as
|
|
1790
|
+
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1791
|
+
import { join as join10, relative as relative2 } from "path";
|
|
513
1792
|
var SKILL_FILE = "SKILL.md";
|
|
514
1793
|
var MARKDOWN_EXTENSION = ".md";
|
|
515
1794
|
var LAYOUT_SEVERITY = Object.freeze({
|
|
@@ -527,7 +1806,7 @@ function layoutIssue(code, message) {
|
|
|
527
1806
|
}
|
|
528
1807
|
function entryKind(path) {
|
|
529
1808
|
try {
|
|
530
|
-
const stats =
|
|
1809
|
+
const stats = statSync2(path);
|
|
531
1810
|
if (stats.isFile())
|
|
532
1811
|
return "file";
|
|
533
1812
|
if (stats.isDirectory())
|
|
@@ -538,16 +1817,16 @@ function entryKind(path) {
|
|
|
538
1817
|
}
|
|
539
1818
|
}
|
|
540
1819
|
function visibleNames(dir) {
|
|
541
|
-
return
|
|
1820
|
+
return readdirSync2(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
|
|
542
1821
|
}
|
|
543
|
-
function isMarkdown(
|
|
544
|
-
return
|
|
1822
|
+
function isMarkdown(name2) {
|
|
1823
|
+
return name2.endsWith(MARKDOWN_EXTENSION);
|
|
545
1824
|
}
|
|
546
|
-
function markdownStem(
|
|
547
|
-
return
|
|
1825
|
+
function markdownStem(name2) {
|
|
1826
|
+
return name2.slice(0, -MARKDOWN_EXTENSION.length);
|
|
548
1827
|
}
|
|
549
1828
|
function shown(root, path) {
|
|
550
|
-
return
|
|
1829
|
+
return relative2(root, path) || ".";
|
|
551
1830
|
}
|
|
552
1831
|
function dotfileOnlyEntry(kind, root, dir) {
|
|
553
1832
|
return {
|
|
@@ -569,37 +1848,37 @@ function unreadableEntry(kind, root, path) {
|
|
|
569
1848
|
issues: [layoutIssue("unreadable-entry", `${shown(root, path)} cannot be read (a broken symlink, or a path stat refuses)`)]
|
|
570
1849
|
};
|
|
571
1850
|
}
|
|
572
|
-
function skillFileEntry(path,
|
|
573
|
-
return { kind: "skill", path, isFile: true, shape: "skill-file", name, issues: [] };
|
|
1851
|
+
function skillFileEntry(path, name2) {
|
|
1852
|
+
return { kind: "skill", path, isFile: true, shape: "skill-file", name: name2, issues: [] };
|
|
574
1853
|
}
|
|
575
|
-
function flatGroupedEntry(root, path,
|
|
1854
|
+
function flatGroupedEntry(root, path, name2) {
|
|
576
1855
|
return {
|
|
577
1856
|
kind: "skill",
|
|
578
1857
|
path,
|
|
579
1858
|
isFile: true,
|
|
580
1859
|
shape: "flat-grouped",
|
|
581
|
-
name,
|
|
582
|
-
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>/${
|
|
1860
|
+
name: name2,
|
|
1861
|
+
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>/${name2}/${SKILL_FILE}`)]
|
|
583
1862
|
};
|
|
584
1863
|
}
|
|
585
|
-
function nestedGroupEntry(root, path,
|
|
1864
|
+
function nestedGroupEntry(root, path, name2) {
|
|
586
1865
|
return {
|
|
587
1866
|
kind: "skill",
|
|
588
1867
|
path,
|
|
589
1868
|
isFile: true,
|
|
590
1869
|
shape: "nested-group",
|
|
591
|
-
name,
|
|
592
|
-
issues: [layoutIssue("nested-group-layout", `${shown(root, path)} is the nested layout <dir>/<group>/<name>/${SKILL_FILE},` + ` which does not register; move it to <dir>/${
|
|
1870
|
+
name: name2,
|
|
1871
|
+
issues: [layoutIssue("nested-group-layout", `${shown(root, path)} is the nested layout <dir>/<group>/<name>/${SKILL_FILE},` + ` which does not register; move it to <dir>/${name2}/${SKILL_FILE}`)]
|
|
593
1872
|
};
|
|
594
1873
|
}
|
|
595
|
-
function looseFileEntry(root, path,
|
|
1874
|
+
function looseFileEntry(root, path, name2) {
|
|
596
1875
|
return {
|
|
597
1876
|
kind: "skill",
|
|
598
1877
|
path,
|
|
599
1878
|
isFile: true,
|
|
600
1879
|
shape: "loose-file",
|
|
601
|
-
name,
|
|
602
|
-
issues: [layoutIssue("loose-file", `${shown(root, path)} sits directly under the skills directory, which registers` + ` only <dir>/<name>/${SKILL_FILE}; move it to <dir>/${
|
|
1880
|
+
name: name2,
|
|
1881
|
+
issues: [layoutIssue("loose-file", `${shown(root, path)} sits directly under the skills directory, which registers` + ` only <dir>/<name>/${SKILL_FILE}; move it to <dir>/${name2}/${SKILL_FILE}`)]
|
|
603
1882
|
};
|
|
604
1883
|
}
|
|
605
1884
|
function noSkillFileEntry(root, dir) {
|
|
@@ -612,30 +1891,30 @@ function noSkillFileEntry(root, dir) {
|
|
|
612
1891
|
issues: [layoutIssue("missing-skill-file", `${shown(root, dir)}/ holds no ${SKILL_FILE}, so it registers no skill`)]
|
|
613
1892
|
};
|
|
614
1893
|
}
|
|
615
|
-
function nestedInstinctEntry(root, path,
|
|
1894
|
+
function nestedInstinctEntry(root, path, name2) {
|
|
616
1895
|
return {
|
|
617
1896
|
kind: "instinct",
|
|
618
1897
|
path,
|
|
619
1898
|
isFile: true,
|
|
620
1899
|
shape: "nested-instinct",
|
|
621
|
-
name,
|
|
622
|
-
issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${
|
|
1900
|
+
name: name2,
|
|
1901
|
+
issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${name2}${MARKDOWN_EXTENSION}`)]
|
|
623
1902
|
};
|
|
624
1903
|
}
|
|
625
|
-
function scanGroupMember(root, dir,
|
|
1904
|
+
function scanGroupMember(root, dir, name2) {
|
|
626
1905
|
const children = visibleNames(dir);
|
|
627
1906
|
if (children.length === 0)
|
|
628
1907
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
629
|
-
const skillPath =
|
|
1908
|
+
const skillPath = join10(dir, SKILL_FILE);
|
|
630
1909
|
if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
|
|
631
|
-
return [nestedGroupEntry(root, skillPath,
|
|
1910
|
+
return [nestedGroupEntry(root, skillPath, name2)];
|
|
632
1911
|
}
|
|
633
1912
|
return [noSkillFileEntry(root, dir)];
|
|
634
1913
|
}
|
|
635
1914
|
function scanSkillGroup(root, dir, children) {
|
|
636
1915
|
const entries = [];
|
|
637
1916
|
for (const child of children) {
|
|
638
|
-
const path =
|
|
1917
|
+
const path = join10(dir, child);
|
|
639
1918
|
const found = entryKind(path);
|
|
640
1919
|
if (found === null) {
|
|
641
1920
|
entries.push(unreadableEntry("skill", root, path));
|
|
@@ -652,31 +1931,31 @@ function scanSkillGroup(root, dir, children) {
|
|
|
652
1931
|
return [noSkillFileEntry(root, dir)];
|
|
653
1932
|
return entries;
|
|
654
1933
|
}
|
|
655
|
-
function scanSkillDirectory(root, dir,
|
|
1934
|
+
function scanSkillDirectory(root, dir, name2) {
|
|
656
1935
|
const children = visibleNames(dir);
|
|
657
1936
|
if (children.length === 0)
|
|
658
1937
|
return [dotfileOnlyEntry("skill", root, dir)];
|
|
659
|
-
const skillPath =
|
|
1938
|
+
const skillPath = join10(dir, SKILL_FILE);
|
|
660
1939
|
if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
|
|
661
|
-
return [skillFileEntry(skillPath,
|
|
1940
|
+
return [skillFileEntry(skillPath, name2)];
|
|
662
1941
|
}
|
|
663
1942
|
return scanSkillGroup(root, dir, children);
|
|
664
1943
|
}
|
|
665
1944
|
function scanSkillRoot(root) {
|
|
666
1945
|
const entries = [];
|
|
667
|
-
for (const
|
|
668
|
-
const path =
|
|
1946
|
+
for (const name2 of visibleNames(root)) {
|
|
1947
|
+
const path = join10(root, name2);
|
|
669
1948
|
const found = entryKind(path);
|
|
670
1949
|
if (found === null) {
|
|
671
1950
|
entries.push(unreadableEntry("skill", root, path));
|
|
672
1951
|
continue;
|
|
673
1952
|
}
|
|
674
1953
|
if (found === "directory") {
|
|
675
|
-
entries.push(...scanSkillDirectory(root, path,
|
|
1954
|
+
entries.push(...scanSkillDirectory(root, path, name2));
|
|
676
1955
|
continue;
|
|
677
1956
|
}
|
|
678
|
-
if (isMarkdown(
|
|
679
|
-
entries.push(looseFileEntry(root, path, markdownStem(
|
|
1957
|
+
if (isMarkdown(name2))
|
|
1958
|
+
entries.push(looseFileEntry(root, path, markdownStem(name2)));
|
|
680
1959
|
}
|
|
681
1960
|
return entries;
|
|
682
1961
|
}
|
|
@@ -686,7 +1965,7 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
686
1965
|
return [dotfileOnlyEntry("instinct", root, dir)];
|
|
687
1966
|
const entries = [];
|
|
688
1967
|
for (const child of children) {
|
|
689
|
-
const path =
|
|
1968
|
+
const path = join10(dir, child);
|
|
690
1969
|
const found = entryKind(path);
|
|
691
1970
|
if (found === null) {
|
|
692
1971
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -703,8 +1982,8 @@ function scanInstinctSubdirectory(root, dir) {
|
|
|
703
1982
|
}
|
|
704
1983
|
function scanInstinctRoot(root) {
|
|
705
1984
|
const entries = [];
|
|
706
|
-
for (const
|
|
707
|
-
const path =
|
|
1985
|
+
for (const name2 of visibleNames(root)) {
|
|
1986
|
+
const path = join10(root, name2);
|
|
708
1987
|
const found = entryKind(path);
|
|
709
1988
|
if (found === null) {
|
|
710
1989
|
entries.push(unreadableEntry("instinct", root, path));
|
|
@@ -714,14 +1993,14 @@ function scanInstinctRoot(root) {
|
|
|
714
1993
|
entries.push(...scanInstinctSubdirectory(root, path));
|
|
715
1994
|
continue;
|
|
716
1995
|
}
|
|
717
|
-
if (!isMarkdown(
|
|
1996
|
+
if (!isMarkdown(name2))
|
|
718
1997
|
continue;
|
|
719
1998
|
entries.push({
|
|
720
1999
|
kind: "instinct",
|
|
721
2000
|
path,
|
|
722
2001
|
isFile: true,
|
|
723
2002
|
shape: "instinct-file",
|
|
724
|
-
name: markdownStem(
|
|
2003
|
+
name: markdownStem(name2),
|
|
725
2004
|
issues: []
|
|
726
2005
|
});
|
|
727
2006
|
}
|
|
@@ -741,9 +2020,9 @@ function scanLayout(root, kind) {
|
|
|
741
2020
|
function declaredNameIssue(entry, declared) {
|
|
742
2021
|
if (entry.name === null || declared === null || declared === entry.name)
|
|
743
2022
|
return null;
|
|
744
|
-
const
|
|
2023
|
+
const field2 = entry.kind === "skill" ? "name" : "id";
|
|
745
2024
|
const source = entry.kind === "skill" ? "directory name" : "file name";
|
|
746
|
-
return layoutIssue("name-mismatch", `frontmatter ${
|
|
2025
|
+
return layoutIssue("name-mismatch", `frontmatter ${field2} "${declared}" is not the ${source} "${entry.name}"`);
|
|
747
2026
|
}
|
|
748
2027
|
|
|
749
2028
|
// src/schema/frontmatter.ts
|
|
@@ -763,7 +2042,7 @@ function scanLine(text, offset) {
|
|
|
763
2042
|
next: index + 1
|
|
764
2043
|
};
|
|
765
2044
|
}
|
|
766
|
-
function
|
|
2045
|
+
function isRecord3(value) {
|
|
767
2046
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
768
2047
|
}
|
|
769
2048
|
function parseBlock(yaml) {
|
|
@@ -774,7 +2053,7 @@ function parseBlock(yaml) {
|
|
|
774
2053
|
} catch {
|
|
775
2054
|
return null;
|
|
776
2055
|
}
|
|
777
|
-
return
|
|
2056
|
+
return isRecord3(parsed) ? parsed : null;
|
|
778
2057
|
}
|
|
779
2058
|
function readFrontmatterDocument(text) {
|
|
780
2059
|
const opening = scanLine(text, 0);
|
|
@@ -831,32 +2110,6 @@ function writeFrontmatter(document, data) {
|
|
|
831
2110
|
].join("");
|
|
832
2111
|
}
|
|
833
2112
|
|
|
834
|
-
// src/schema/project-id.ts
|
|
835
|
-
import { spawnSync } from "child_process";
|
|
836
|
-
var PROJECT_ID_LENGTH = 12;
|
|
837
|
-
var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
|
|
838
|
-
function hasColonAfterAt(url) {
|
|
839
|
-
const at = url.indexOf("@");
|
|
840
|
-
return at !== -1 && url.lastIndexOf(":") > at;
|
|
841
|
-
}
|
|
842
|
-
function normalizeRemote(url) {
|
|
843
|
-
const stripped = url.replace(/:\/\/[^@]+@/, "://");
|
|
844
|
-
const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
|
|
845
|
-
const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
846
|
-
return network ? trimmed.toLowerCase() : trimmed;
|
|
847
|
-
}
|
|
848
|
-
function gitRemoteUrl(dir) {
|
|
849
|
-
const result = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
850
|
-
cwd: dir,
|
|
851
|
-
encoding: "utf8",
|
|
852
|
-
env: { ...process.env, LC_ALL: "C" }
|
|
853
|
-
});
|
|
854
|
-
if (result.error !== undefined || result.status !== 0)
|
|
855
|
-
return null;
|
|
856
|
-
const url = result.stdout.trim();
|
|
857
|
-
return url === "" ? null : url;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
2113
|
// src/schema/instinct.ts
|
|
861
2114
|
var INSTINCT_KINDS = FINDING_KINDS;
|
|
862
2115
|
var INSTINCT_SIGNALS = FINDING_SIGNALS;
|
|
@@ -889,8 +2142,8 @@ var INSTINCT_KEY_ORDER = [
|
|
|
889
2142
|
"created_at",
|
|
890
2143
|
"updated_at"
|
|
891
2144
|
];
|
|
892
|
-
function issue(code,
|
|
893
|
-
return { code, field, message };
|
|
2145
|
+
function issue(code, field2, message) {
|
|
2146
|
+
return { code, field: field2, message };
|
|
894
2147
|
}
|
|
895
2148
|
function typeName(value) {
|
|
896
2149
|
if (value === null)
|
|
@@ -905,25 +2158,25 @@ function typeName(value) {
|
|
|
905
2158
|
return "a number";
|
|
906
2159
|
return `a ${typeof value}`;
|
|
907
2160
|
}
|
|
908
|
-
function requiredString(data,
|
|
909
|
-
const value = data[
|
|
2161
|
+
function requiredString(data, field2) {
|
|
2162
|
+
const value = data[field2];
|
|
910
2163
|
if (value === undefined || value === null) {
|
|
911
|
-
return [issue("missing-field",
|
|
2164
|
+
return [issue("missing-field", field2, `${field2} is required and is absent`)];
|
|
912
2165
|
}
|
|
913
2166
|
if (typeof value !== "string") {
|
|
914
|
-
return [issue("wrong-type",
|
|
2167
|
+
return [issue("wrong-type", field2, `${field2} must be a string, not ${typeName(value)}`)];
|
|
915
2168
|
}
|
|
916
2169
|
if (value.trim() === "") {
|
|
917
|
-
return [issue("missing-field",
|
|
2170
|
+
return [issue("missing-field", field2, `${field2} is required and is empty`)];
|
|
918
2171
|
}
|
|
919
2172
|
return [];
|
|
920
2173
|
}
|
|
921
|
-
function requiredChoice(data,
|
|
922
|
-
const issues = requiredString(data,
|
|
2174
|
+
function requiredChoice(data, field2, allowed, code) {
|
|
2175
|
+
const issues = requiredString(data, field2);
|
|
923
2176
|
if (issues.length > 0)
|
|
924
2177
|
return issues;
|
|
925
|
-
const value = data[
|
|
926
|
-
return allowed.includes(value) ? [] : [issue(code,
|
|
2178
|
+
const value = data[field2];
|
|
2179
|
+
return allowed.includes(value) ? [] : [issue(code, field2, `${field2} "${value}" is not one of ${allowed.join(", ")}`)];
|
|
927
2180
|
}
|
|
928
2181
|
function checkId(data) {
|
|
929
2182
|
const issues = requiredString(data, "id");
|
|
@@ -959,11 +2212,11 @@ function checkSources(data) {
|
|
|
959
2212
|
return [issue("wrong-type", "sources", `sources must be a list, not ${typeName(value)}`)];
|
|
960
2213
|
}
|
|
961
2214
|
return value.flatMap((entry, index) => {
|
|
962
|
-
const
|
|
2215
|
+
const field2 = `sources[${index}]`;
|
|
963
2216
|
if (typeof entry !== "string") {
|
|
964
|
-
return [issue("wrong-type",
|
|
2217
|
+
return [issue("wrong-type", field2, `sources entries must be strings, and this one is ${typeName(entry)}`)];
|
|
965
2218
|
}
|
|
966
|
-
return entry.trim() === "" ? [issue("wrong-type",
|
|
2219
|
+
return entry.trim() === "" ? [issue("wrong-type", field2, "sources entries must name a source, and this one is blank")] : [];
|
|
967
2220
|
});
|
|
968
2221
|
}
|
|
969
2222
|
function checkPromotedTo(data) {
|
|
@@ -992,26 +2245,26 @@ function checkProjectId(data) {
|
|
|
992
2245
|
}
|
|
993
2246
|
return PROJECT_ID_PATTERN.test(value) ? [] : [issue("invalid-project-id", "project_id", `project_id "${value}" is not ${PROJECT_ID_LENGTH} lowercase hex characters`)];
|
|
994
2247
|
}
|
|
995
|
-
function checkTimestamp(data,
|
|
996
|
-
const issues = requiredString(data,
|
|
2248
|
+
function checkTimestamp(data, field2) {
|
|
2249
|
+
const issues = requiredString(data, field2);
|
|
997
2250
|
if (issues.length > 0)
|
|
998
2251
|
return issues;
|
|
999
|
-
const value = data[
|
|
1000
|
-
return TIMESTAMP_PATTERN.test(value) && !Number.isNaN(Date.parse(value)) ? [] : [issue("invalid-timestamp",
|
|
2252
|
+
const value = data[field2];
|
|
2253
|
+
return TIMESTAMP_PATTERN.test(value) && !Number.isNaN(Date.parse(value)) ? [] : [issue("invalid-timestamp", field2, `${field2} "${value}" is not an ISO 8601 instant such as 2026-09-11T10:00:00Z`)];
|
|
1001
2254
|
}
|
|
1002
2255
|
function isScalar(value) {
|
|
1003
2256
|
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1004
2257
|
}
|
|
1005
2258
|
function checkEvidenceEntry(entry, index) {
|
|
1006
|
-
const
|
|
2259
|
+
const field2 = `evidence[${index}]`;
|
|
1007
2260
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
1008
|
-
return [issue("wrong-type",
|
|
2261
|
+
return [issue("wrong-type", field2, `evidence entries must be mappings, and this one is ${typeName(entry)}`)];
|
|
1009
2262
|
}
|
|
1010
2263
|
const entries = Object.entries(entry);
|
|
1011
2264
|
if (entries.length === 0) {
|
|
1012
|
-
return [issue("wrong-type",
|
|
2265
|
+
return [issue("wrong-type", field2, "evidence entries must say something, and this one is empty")];
|
|
1013
2266
|
}
|
|
1014
|
-
return entries.flatMap(([key, value]) => isScalar(value) ? [] : [issue("wrong-type", `${
|
|
2267
|
+
return entries.flatMap(([key, value]) => isScalar(value) ? [] : [issue("wrong-type", `${field2}.${key}`, `evidence values must be text, numbers or booleans, and this one is ${typeName(value)}`)]);
|
|
1015
2268
|
}
|
|
1016
2269
|
function checkEvidence(data) {
|
|
1017
2270
|
const value = data["evidence"];
|
|
@@ -1171,11 +2424,11 @@ function writeInstinct(instinct, newline = `
|
|
|
1171
2424
|
}
|
|
1172
2425
|
|
|
1173
2426
|
// src/commands/instinct/instinct-records.ts
|
|
1174
|
-
function isRecordFile(
|
|
1175
|
-
return !
|
|
2427
|
+
function isRecordFile(name2) {
|
|
2428
|
+
return !name2.startsWith(".") && name2.endsWith(MARKDOWN_EXTENSION) && name2 !== MARKDOWN_EXTENSION;
|
|
1176
2429
|
}
|
|
1177
|
-
function recordId(
|
|
1178
|
-
return
|
|
2430
|
+
function recordId(name2) {
|
|
2431
|
+
return name2.slice(0, -MARKDOWN_EXTENSION.length);
|
|
1179
2432
|
}
|
|
1180
2433
|
function unreadableIssue(message) {
|
|
1181
2434
|
return { code: "missing-frontmatter", field: "file", message: `the file could not be read: ${message}` };
|
|
@@ -1183,7 +2436,7 @@ function unreadableIssue(message) {
|
|
|
1183
2436
|
function readRecord(path, scope, id) {
|
|
1184
2437
|
let text;
|
|
1185
2438
|
try {
|
|
1186
|
-
text =
|
|
2439
|
+
text = readFileSync7(path, "utf8");
|
|
1187
2440
|
} catch (error) {
|
|
1188
2441
|
const message = error instanceof Error ? error.message : String(error);
|
|
1189
2442
|
return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
|
|
@@ -1193,13 +2446,13 @@ function readRecord(path, scope, id) {
|
|
|
1193
2446
|
}
|
|
1194
2447
|
function isFile(path) {
|
|
1195
2448
|
try {
|
|
1196
|
-
return
|
|
2449
|
+
return statSync3(path).isFile();
|
|
1197
2450
|
} catch {
|
|
1198
2451
|
return false;
|
|
1199
2452
|
}
|
|
1200
2453
|
}
|
|
1201
2454
|
function recordFiles(dir) {
|
|
1202
|
-
return
|
|
2455
|
+
return readdirSync3(dir).filter((name2) => isRecordFile(name2) && isFile(join11(dir, name2))).sort((left, right) => left.localeCompare(right));
|
|
1203
2456
|
}
|
|
1204
2457
|
function readScope(scope, dir) {
|
|
1205
2458
|
if (!tierExists(dir))
|
|
@@ -1208,7 +2461,7 @@ function readScope(scope, dir) {
|
|
|
1208
2461
|
scope,
|
|
1209
2462
|
dir,
|
|
1210
2463
|
exists: true,
|
|
1211
|
-
records: recordFiles(dir).map((
|
|
2464
|
+
records: recordFiles(dir).map((name2) => readRecord(join11(dir, name2), scope, recordId(name2)))
|
|
1212
2465
|
};
|
|
1213
2466
|
}
|
|
1214
2467
|
function readScopes(seams) {
|
|
@@ -1220,9 +2473,9 @@ function allRecords(listings) {
|
|
|
1220
2473
|
function findRecords(listings, id) {
|
|
1221
2474
|
return allRecords(listings).filter((entry) => entry.id === id);
|
|
1222
2475
|
}
|
|
1223
|
-
function instinctProject(context,
|
|
2476
|
+
function instinctProject(context, name2) {
|
|
1224
2477
|
if (context.project === null)
|
|
1225
|
-
throw new Error(`${
|
|
2478
|
+
throw new Error(`${name2} runs inside a project, and was handed none`);
|
|
1226
2479
|
return context.project;
|
|
1227
2480
|
}
|
|
1228
2481
|
|
|
@@ -1305,7 +2558,7 @@ var PREFIX = "local learning";
|
|
|
1305
2558
|
var PUSH_REFUSAL = `${PREFIX}: refused to store a push`;
|
|
1306
2559
|
var INSTINCTS_FILE = "instincts.ndjson";
|
|
1307
2560
|
var FLAGS_FILE = "flags.ndjson";
|
|
1308
|
-
var
|
|
2561
|
+
var RECORD_EXTENSION2 = ".md";
|
|
1309
2562
|
var MIN_CONFIDENCE = 0.3;
|
|
1310
2563
|
var MAX_CONFIDENCE = 0.9;
|
|
1311
2564
|
var SIGNALS = ["loud", "silent"];
|
|
@@ -1489,7 +2742,7 @@ function producedRecords(result) {
|
|
|
1489
2742
|
for (const record of result.decisions.flatMap((decision) => decision.produced)) {
|
|
1490
2743
|
const found = byId.get(record.id);
|
|
1491
2744
|
if (found !== undefined && found !== record) {
|
|
1492
|
-
throw new Error(`${PUSH_REFUSAL}: two records would be written to ${record.id}${
|
|
2745
|
+
throw new Error(`${PUSH_REFUSAL}: two records would be written to ${record.id}${RECORD_EXTENSION2}`);
|
|
1493
2746
|
}
|
|
1494
2747
|
byId.set(record.id, record);
|
|
1495
2748
|
}
|
|
@@ -1501,7 +2754,7 @@ function heldChanges(dir, held, result, described) {
|
|
|
1501
2754
|
const writes = [];
|
|
1502
2755
|
const kept = new Set;
|
|
1503
2756
|
for (const record of producedRecords(result)) {
|
|
1504
|
-
const file = `${record.id}${
|
|
2757
|
+
const file = `${record.id}${RECORD_EXTENSION2}`;
|
|
1505
2758
|
const instinct = toHeldInstinct(record, heldInstincts, payloadDescription(record, described));
|
|
1506
2759
|
if (instinct === null) {
|
|
1507
2760
|
throw new Error(`${PUSH_REFUSAL}: ${file} has no held member and no payload description to be written with`);
|
|
@@ -1511,7 +2764,7 @@ function heldChanges(dir, held, result, described) {
|
|
|
1511
2764
|
if (issue2 !== undefined) {
|
|
1512
2765
|
throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue2.field}: ${issue2.message}`);
|
|
1513
2766
|
}
|
|
1514
|
-
const path =
|
|
2767
|
+
const path = join12(dir, file);
|
|
1515
2768
|
if (held.unreadable.has(path)) {
|
|
1516
2769
|
throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
|
|
1517
2770
|
}
|
|
@@ -1541,18 +2794,18 @@ function pushLogLines(sourceId, described, result) {
|
|
|
1541
2794
|
});
|
|
1542
2795
|
}
|
|
1543
2796
|
function localInstinctsDir(repoRoot) {
|
|
1544
|
-
return
|
|
2797
|
+
return join12(repoRoot, RAFA_INSTINCTS_PATH);
|
|
1545
2798
|
}
|
|
1546
2799
|
function userInstinctsDir(home) {
|
|
1547
|
-
return
|
|
2800
|
+
return join12(home, RAFA_INSTINCTS_PATH);
|
|
1548
2801
|
}
|
|
1549
2802
|
function createLocalLearning(options) {
|
|
1550
2803
|
const { instinctsDir, minConfidence } = options;
|
|
1551
2804
|
const userDir = userInstinctsDir(options.home);
|
|
1552
2805
|
const now = options.now ?? (() => new Date().toISOString());
|
|
1553
2806
|
const warn = options.warn ?? warnThroughActiveOutput;
|
|
1554
|
-
const instinctsPath =
|
|
1555
|
-
const flagsPath =
|
|
2807
|
+
const instinctsPath = join12(instinctsDir, INSTINCTS_FILE);
|
|
2808
|
+
const flagsPath = join12(instinctsDir, FLAGS_FILE);
|
|
1556
2809
|
function readHeld(scope, dir) {
|
|
1557
2810
|
const { records } = readScope(scope, dir);
|
|
1558
2811
|
const unreadable = new Set;
|
|
@@ -1669,9 +2922,9 @@ function createJsonOutput({
|
|
|
1669
2922
|
}
|
|
1670
2923
|
|
|
1671
2924
|
// src/adapters/planner/claude.ts
|
|
1672
|
-
import { existsSync as
|
|
2925
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1673
2926
|
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
1674
|
-
import { posix, resolve } from "path";
|
|
2927
|
+
import { posix, resolve as resolve3 } from "path";
|
|
1675
2928
|
|
|
1676
2929
|
// src/board/spec-review.ts
|
|
1677
2930
|
var SPEC_REVIEW_KIND = "spec-review";
|
|
@@ -1693,7 +2946,7 @@ var VERDICT_BREAK = /[\s_]+/gu;
|
|
|
1693
2946
|
function isMapping2(value) {
|
|
1694
2947
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1695
2948
|
}
|
|
1696
|
-
function
|
|
2949
|
+
function describeValue3(value) {
|
|
1697
2950
|
if (typeof value === "string")
|
|
1698
2951
|
return JSON.stringify(value);
|
|
1699
2952
|
if (value === null)
|
|
@@ -1726,39 +2979,39 @@ function verdictOf(value) {
|
|
|
1726
2979
|
const word = spelled.replace(VERDICT_BREAK, "-");
|
|
1727
2980
|
return SPEC_REVIEW_VERDICTS.includes(word) ? word : null;
|
|
1728
2981
|
}
|
|
1729
|
-
function issueAt(
|
|
1730
|
-
return { field, text };
|
|
2982
|
+
function issueAt(field2, text) {
|
|
2983
|
+
return { field: field2, text };
|
|
1731
2984
|
}
|
|
1732
|
-
function readBlocking(item,
|
|
2985
|
+
function readBlocking(item, field2, issues) {
|
|
1733
2986
|
const value = valueAt(item, "blocking");
|
|
1734
2987
|
if (typeof value === "boolean")
|
|
1735
2988
|
return value;
|
|
1736
|
-
const text = `${
|
|
1737
|
-
issues.push(issueAt(`${
|
|
2989
|
+
const text = `${field2}.blocking is ${describeValue3(value)}, not true or false; ` + "the gap is read as blocking";
|
|
2990
|
+
issues.push(issueAt(`${field2}.blocking`, text));
|
|
1738
2991
|
return true;
|
|
1739
2992
|
}
|
|
1740
|
-
function readGap(item,
|
|
2993
|
+
function readGap(item, field2, issues) {
|
|
1741
2994
|
if (!isMapping2(item)) {
|
|
1742
|
-
const text = `${
|
|
1743
|
-
issues.push(issueAt(
|
|
2995
|
+
const text = `${field2} is ${describeValue3(item)}, not a mapping of heading and what; dropped`;
|
|
2996
|
+
issues.push(issueAt(field2, text));
|
|
1744
2997
|
return null;
|
|
1745
2998
|
}
|
|
1746
2999
|
const what = stringAt(item, "what");
|
|
1747
3000
|
if (what === null) {
|
|
1748
|
-
const text = `${
|
|
1749
|
-
issues.push(issueAt(`${
|
|
3001
|
+
const text = `${field2}.what is ${describeValue3(valueAt(item, "what"))}, ` + "not a sentence; the gap is dropped";
|
|
3002
|
+
issues.push(issueAt(`${field2}.what`, text));
|
|
1750
3003
|
return null;
|
|
1751
3004
|
}
|
|
1752
3005
|
const heading = stringAt(item, "heading");
|
|
1753
3006
|
if (heading === null) {
|
|
1754
|
-
const text = `${
|
|
1755
|
-
issues.push(issueAt(`${
|
|
3007
|
+
const text = `${field2}.heading is ${describeValue3(valueAt(item, "heading"))}, ` + `not a heading; the gap is filed under ${REVIEW_HEADING}`;
|
|
3008
|
+
issues.push(issueAt(`${field2}.heading`, text));
|
|
1756
3009
|
}
|
|
1757
|
-
const blocking = readBlocking(item,
|
|
3010
|
+
const blocking = readBlocking(item, field2, issues);
|
|
1758
3011
|
const assumption = stringAt(item, "assumption");
|
|
1759
3012
|
if (!blocking && assumption === null) {
|
|
1760
|
-
const text = `${
|
|
1761
|
-
issues.push(issueAt(`${
|
|
3013
|
+
const text = `${field2}.assumption is ${describeValue3(valueAt(item, "assumption"))}, ` + "not what a non-blocking gap would be planned under; the gap is read as blocking";
|
|
3014
|
+
issues.push(issueAt(`${field2}.assumption`, text));
|
|
1762
3015
|
return { heading: heading ?? REVIEW_HEADING, what, blocking: true, assumption: null };
|
|
1763
3016
|
}
|
|
1764
3017
|
return { heading: heading ?? REVIEW_HEADING, what, blocking, assumption };
|
|
@@ -1768,7 +3021,7 @@ function readGaps(document, issues) {
|
|
|
1768
3021
|
if (value === null)
|
|
1769
3022
|
return [];
|
|
1770
3023
|
if (!Array.isArray(value)) {
|
|
1771
|
-
const text = `gaps is ${
|
|
3024
|
+
const text = `gaps is ${describeValue3(value)}, not a list; no gap is read from it`;
|
|
1772
3025
|
issues.push(issueAt("gaps", text));
|
|
1773
3026
|
return [];
|
|
1774
3027
|
}
|
|
@@ -1824,12 +3077,12 @@ function parseSpecReview(output) {
|
|
|
1824
3077
|
return unreadable("malformed", block, text);
|
|
1825
3078
|
}
|
|
1826
3079
|
if (!isMapping2(document)) {
|
|
1827
|
-
const text = `the rafa:spec-review block at line ${line} holds ${
|
|
3080
|
+
const text = `the rafa:spec-review block at line ${line} holds ${describeValue3(document)}, ` + "not a mapping of verdict and gaps";
|
|
1828
3081
|
return unreadable("malformed", block, text);
|
|
1829
3082
|
}
|
|
1830
3083
|
const verdict = verdictOf(valueAt(document, "verdict"));
|
|
1831
3084
|
if (verdict === null) {
|
|
1832
|
-
const text = `the rafa:spec-review block at line ${line} has a verdict of ` + `${
|
|
3085
|
+
const text = `the rafa:spec-review block at line ${line} has a verdict of ` + `${describeValue3(valueAt(document, "verdict"))}, ` + `not one of ${SPEC_REVIEW_VERDICTS.join(", ")}`;
|
|
1833
3086
|
return unreadable("malformed", block, text);
|
|
1834
3087
|
}
|
|
1835
3088
|
const issues = [];
|
|
@@ -1905,11 +3158,11 @@ class UndeclaredSpendError extends Error {
|
|
|
1905
3158
|
name = "UndeclaredSpendError";
|
|
1906
3159
|
}
|
|
1907
3160
|
function carriesFlag(flags, flag) {
|
|
1908
|
-
const
|
|
1909
|
-
const value = flags[
|
|
3161
|
+
const name2 = flag.replace(/^--/, "");
|
|
3162
|
+
const value = flags[name2];
|
|
1910
3163
|
if (value !== undefined && value !== false)
|
|
1911
3164
|
return true;
|
|
1912
|
-
return
|
|
3165
|
+
return name2.startsWith("no-") && flags[name2.slice(3)] === false;
|
|
1913
3166
|
}
|
|
1914
3167
|
function spendRefusal(running2) {
|
|
1915
3168
|
if (running2 === null)
|
|
@@ -2035,12 +3288,12 @@ function createClaudePlanner(options) {
|
|
|
2035
3288
|
const create = async ({ specPath, stub }) => {
|
|
2036
3289
|
const planPath = planFilePath(planDir, `PLAN-${stub}.md`);
|
|
2037
3290
|
const prerequisitesPath = planFilePath(planDir, `PREREQUISITES-${stub}.md`);
|
|
2038
|
-
const isWritten = (path) =>
|
|
3291
|
+
const isWritten = (path) => existsSync6(resolve3(repoRoot, path));
|
|
2039
3292
|
if (isWritten(planPath)) {
|
|
2040
3293
|
throw new ClaudePlannerError(`${planPath} already exists`, FAILURE_EXIT_CODE);
|
|
2041
3294
|
}
|
|
2042
|
-
const specContent = await readFile2(
|
|
2043
|
-
await mkdir2(
|
|
3295
|
+
const specContent = await readFile2(resolve3(repoRoot, specPath), "utf8");
|
|
3296
|
+
await mkdir2(resolve3(repoRoot, planDir), { recursive: true });
|
|
2044
3297
|
const session = await runClaudeCaptured(buildPrompt(specContent, stub), settingSources, [], spawn);
|
|
2045
3298
|
const review = parseSpecReview(session.stdout);
|
|
2046
3299
|
const { exitCode } = session;
|
|
@@ -2151,8 +3404,8 @@ async function withDeadline(work, timeoutMs) {
|
|
|
2151
3404
|
if (timeoutMs === undefined)
|
|
2152
3405
|
return work;
|
|
2153
3406
|
let timer;
|
|
2154
|
-
const deadline = new Promise((
|
|
2155
|
-
timer = setTimeout(() =>
|
|
3407
|
+
const deadline = new Promise((resolve4) => {
|
|
3408
|
+
timer = setTimeout(() => resolve4(TIMED_OUT), timeoutMs);
|
|
2156
3409
|
});
|
|
2157
3410
|
try {
|
|
2158
3411
|
return await Promise.race([work, deadline]);
|
|
@@ -2167,7 +3420,7 @@ function notOnPath(command, cwd, path) {
|
|
|
2167
3420
|
function isOneOf(members, value) {
|
|
2168
3421
|
return typeof value === "string" && members.includes(value);
|
|
2169
3422
|
}
|
|
2170
|
-
function
|
|
3423
|
+
function isIssueNumber3(value) {
|
|
2171
3424
|
return typeof value === "string" && ISSUE_NUMBER.test(value) && Number.isSafeInteger(Number(value));
|
|
2172
3425
|
}
|
|
2173
3426
|
function isRepoName(value) {
|
|
@@ -2179,8 +3432,8 @@ function isRepoName(value) {
|
|
|
2179
3432
|
function isLabelPart(value) {
|
|
2180
3433
|
return typeof value === "string" && !value.includes(",");
|
|
2181
3434
|
}
|
|
2182
|
-
function optionalFlag(
|
|
2183
|
-
return value === undefined ? [] : [
|
|
3435
|
+
function optionalFlag(name2, value) {
|
|
3436
|
+
return value === undefined ? [] : [name2, value];
|
|
2184
3437
|
}
|
|
2185
3438
|
function detailOf(result) {
|
|
2186
3439
|
return result.stderr.trim() || result.stdout.trim() || "it exited non-zero and wrote nothing";
|
|
@@ -2222,7 +3475,7 @@ function issueNumberOfRef(ref) {
|
|
|
2222
3475
|
if (ref.kind !== "github") {
|
|
2223
3476
|
throw new Error(`${PREFIX2}: refused a ref of kind ${describeValue(ref.kind)}; this tracker reads github refs only`);
|
|
2224
3477
|
}
|
|
2225
|
-
if (!
|
|
3478
|
+
if (!isIssueNumber3(ref.externalId)) {
|
|
2226
3479
|
throw new Error(`${PREFIX2}: externalId ${describeValue(ref.externalId)} is not a GitHub issue number,` + " expected a positive whole number with no leading zero");
|
|
2227
3480
|
}
|
|
2228
3481
|
if (ref.repo !== undefined && !isRepoName(ref.repo)) {
|
|
@@ -2243,7 +3496,7 @@ function labelNames(value) {
|
|
|
2243
3496
|
if (!Array.isArray(value))
|
|
2244
3497
|
return null;
|
|
2245
3498
|
const names = value.map((label) => isMapping(label) && typeof label["name"] === "string" ? label["name"] : null);
|
|
2246
|
-
return names.every((
|
|
3499
|
+
return names.every((name2) => name2 !== null) ? names : null;
|
|
2247
3500
|
}
|
|
2248
3501
|
function viewProblem(payload, number) {
|
|
2249
3502
|
if (!isMapping(payload))
|
|
@@ -2279,18 +3532,18 @@ function listedIssues(stdout) {
|
|
|
2279
3532
|
throw new Error(`${PREFIX2}: ${command} answered ${describeValue(payload)}, expected a list`);
|
|
2280
3533
|
}
|
|
2281
3534
|
return payload.map((row, index) => {
|
|
2282
|
-
const
|
|
3535
|
+
const refuse2 = (problem) => new Error(`${PREFIX2}: ${command} answered row ${index} ${problem}`);
|
|
2283
3536
|
if (!isMapping(row))
|
|
2284
|
-
throw
|
|
3537
|
+
throw refuse2(`${describeValue(row)}, expected a mapping`);
|
|
2285
3538
|
const { number, url } = row;
|
|
2286
3539
|
const labels = labelNames(row["labels"]);
|
|
2287
|
-
if (typeof number !== "number" || !
|
|
2288
|
-
throw
|
|
3540
|
+
if (typeof number !== "number" || !isIssueNumber3(String(number))) {
|
|
3541
|
+
throw refuse2(`with number ${describeValue(number)}, expected an issue number`);
|
|
2289
3542
|
}
|
|
2290
3543
|
if (typeof url !== "string")
|
|
2291
|
-
throw
|
|
3544
|
+
throw refuse2(`with url ${describeValue(url)}, expected a string`);
|
|
2292
3545
|
if (labels === null)
|
|
2293
|
-
throw
|
|
3546
|
+
throw refuse2("with labels that are not a list of named labels");
|
|
2294
3547
|
return { number, url, labels };
|
|
2295
3548
|
});
|
|
2296
3549
|
}
|
|
@@ -2319,7 +3572,7 @@ function issueOf(ref, viewed) {
|
|
|
2319
3572
|
module: moduleOfLabels(viewed.labels),
|
|
2320
3573
|
priority: isOneOf(ISSUE_PRIORITIES, priority) ? priority : null,
|
|
2321
3574
|
project: null,
|
|
2322
|
-
blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(
|
|
3575
|
+
blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(isIssueNumber3).map(Number),
|
|
2323
3576
|
ref: { ...ref, url: viewed.url },
|
|
2324
3577
|
state: stateOf(viewed)
|
|
2325
3578
|
};
|
|
@@ -2344,9 +3597,9 @@ function createGithubTracker(options) {
|
|
|
2344
3597
|
return result.stdout;
|
|
2345
3598
|
}
|
|
2346
3599
|
async function ensureLabels(names) {
|
|
2347
|
-
const missing = [...new Set(names)].filter((
|
|
2348
|
-
for (const
|
|
2349
|
-
await run(["label", "create",
|
|
3600
|
+
const missing = [...new Set(names)].filter((name2) => !knownLabels.has(name2));
|
|
3601
|
+
for (const name2 of missing) {
|
|
3602
|
+
await run(["label", "create", name2, "--force"], `gh label create ${describeValue(name2)}`);
|
|
2350
3603
|
}
|
|
2351
3604
|
knownLabels = new Set([...knownLabels, ...missing]);
|
|
2352
3605
|
}
|
|
@@ -2457,7 +3710,7 @@ function createGithubTracker(options) {
|
|
|
2457
3710
|
|
|
2458
3711
|
// src/adapters/tracker/local.ts
|
|
2459
3712
|
import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
2460
|
-
import { join as
|
|
3713
|
+
import { join as join13 } from "path";
|
|
2461
3714
|
var PREFIX3 = "local tracker";
|
|
2462
3715
|
var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
2463
3716
|
var ISSUE_NUMBER2 = /^[1-9]\d*$/;
|
|
@@ -2510,11 +3763,11 @@ function parseIssueNumber(text) {
|
|
|
2510
3763
|
const number = Number(text);
|
|
2511
3764
|
return Number.isSafeInteger(number) ? number : null;
|
|
2512
3765
|
}
|
|
2513
|
-
function issueNumberOfFile(
|
|
2514
|
-
return
|
|
3766
|
+
function issueNumberOfFile(name2) {
|
|
3767
|
+
return name2.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name2.slice(0, -ISSUE_EXTENSION.length)) : null;
|
|
2515
3768
|
}
|
|
2516
3769
|
function issuePath(issuesDir, number) {
|
|
2517
|
-
return
|
|
3770
|
+
return join13(issuesDir, `${number}${ISSUE_EXTENSION}`);
|
|
2518
3771
|
}
|
|
2519
3772
|
async function issueNumbers(issuesDir) {
|
|
2520
3773
|
let names;
|
|
@@ -2528,7 +3781,7 @@ async function issueNumbers(issuesDir) {
|
|
|
2528
3781
|
return names.map(issueNumberOfFile).filter((number) => number !== null).sort((a, b) => a - b);
|
|
2529
3782
|
}
|
|
2530
3783
|
function localIssuesDir(repoRoot) {
|
|
2531
|
-
return
|
|
3784
|
+
return join13(repoRoot, ".rafa", "issues");
|
|
2532
3785
|
}
|
|
2533
3786
|
function renderLocalIssue(record) {
|
|
2534
3787
|
const { draft } = record;
|
|
@@ -2849,7 +4102,7 @@ var CORE_ADAPTERS = [
|
|
|
2849
4102
|
var CORE_ADAPTER_REGISTRY = createAdapterRegistry(CORE_ADAPTERS);
|
|
2850
4103
|
|
|
2851
4104
|
// src/effort/store/index.ts
|
|
2852
|
-
function
|
|
4105
|
+
function describeValue4(value) {
|
|
2853
4106
|
if (typeof value === "string")
|
|
2854
4107
|
return JSON.stringify(value);
|
|
2855
4108
|
if (typeof value === "object" && value !== null)
|
|
@@ -2860,9 +4113,9 @@ function selectEffortStore(repoRoot, config) {
|
|
|
2860
4113
|
const adapter = CORE_ADAPTER_REGISTRY.find("store", config.store);
|
|
2861
4114
|
if (adapter === undefined) {
|
|
2862
4115
|
const expected = CORE_ADAPTER_REGISTRY.kinds("store").join(", ");
|
|
2863
|
-
throw new TypeError(`effort store: store is ${
|
|
4116
|
+
throw new TypeError(`effort store: store is ${describeValue4(config.store)},` + ` expected one of: ${expected}`);
|
|
2864
4117
|
}
|
|
2865
4118
|
return adapter.create({ repoRoot });
|
|
2866
4119
|
}
|
|
2867
4120
|
|
|
2868
|
-
export { EFFORT_STORE_DIR,
|
|
4121
|
+
export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, positionAt, rehome, hop, goHome, HOP_FILE, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, SessionConflictError, SessionStateError, isSessionId, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, beginSession, updateSession, readRuntimeIdentity, DEVELOPMENT_NEXT_STEP, ADOPTION_NAME, DEVELOPMENT_MARK, appliedByName, DevelopmentBuildRefusedError, storeProjectRoot, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, MIGRATION_LOG_TABLE, SchemaRefusedError, bringForward, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, setActiveStoreSettings, activeStoreSettings, createGitRunner, gitSaid, normalizeRemote, gitRemoteUrl, readProjectIdentity, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, sessionSpawnEnv, getClaudeUsagePercent, checkUsage, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };
|