sphica 0.6.35 → 0.6.37
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/db/migrations/0011.sql +26 -0
- package/db/schema.sql +16 -2
- package/dist/capture.js +3 -1
- package/dist/cli.js +5 -57
- package/dist/deliver.js +3 -1
- package/dist/mcp-record.js +184 -133
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
- package/skills/glean/SKILL.md +3 -2
- package/skills/harvest/SKILL.md +2 -1
- package/skills/trace/SKILL.md +3 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "sphica",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.37",
|
|
5
5
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "iroha924",
|
package/README.md
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Revision 10 → 11 of generation 2: a harvest run keeps the sources it may cite, chosen when it begins (harvest_run_source). A running
|
|
2
|
+
-- harvest has none, so it is removed and its session gets "Begin again"; nothing cites a running run (a save also finishes it).
|
|
3
|
+
-- Every statement that stays is the same as in the schema at revision 11.
|
|
4
|
+
|
|
5
|
+
create temp table sphica_migration_note (rule text, item text, action text);
|
|
6
|
+
|
|
7
|
+
create table harvest_run_source (
|
|
8
|
+
run_id integer not null references extraction_run (id) on delete cascade,
|
|
9
|
+
source_id integer not null references source (id) on delete cascade,
|
|
10
|
+
primary key (run_id, source_id)
|
|
11
|
+
) strict;
|
|
12
|
+
create index harvest_run_source_source on harvest_run_source (source_id);
|
|
13
|
+
|
|
14
|
+
create trigger harvest_run_source_run before insert on harvest_run_source begin
|
|
15
|
+
select raise(abort, 'only a harvest run keeps sources, from its own project')
|
|
16
|
+
where (select origin from extraction_run where id = new.run_id) is not 'harvest'
|
|
17
|
+
or (select project_id from source where id = new.source_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
18
|
+
end;
|
|
19
|
+
|
|
20
|
+
insert into sphica_migration_note
|
|
21
|
+
select 'a harvest run still running, begun before its sources were kept', 'run ' || id || ' ' || target || ' begun ' || started_at,
|
|
22
|
+
'removed; begin the harvest again'
|
|
23
|
+
from extraction_run where origin = 'harvest' and status = 'running' order by id;
|
|
24
|
+
delete from extraction_run where origin = 'harvest' and status = 'running';
|
|
25
|
+
|
|
26
|
+
pragma user_version = 11;
|
package/db/schema.sql
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
-- captured sources session, source, artifact_link, edit_observation: what was said or written, never rewritten
|
|
7
7
|
-- (the owner can forget chosen sources: forget_batch and source_forgotten keep what was removed, without its text)
|
|
8
8
|
-- extracted units unit and its option, evidence, adoption, link, state, anchor, alias tables: what was decided or implemented
|
|
9
|
-
-- processing extraction_run, source_processing: what has been looked at and saved, so gaps are counted
|
|
9
|
+
-- processing extraction_run, source_processing, harvest_run_source: what has been looked at and saved, so gaps are counted
|
|
10
10
|
-- work and delivery work, delivery, delivery_unit: the current work status and what the hooks injected
|
|
11
11
|
-- Every table is STRICT and every primary key is not null. Times are ISO 8601 UTC (`Date#toISOString()`); `strftime(...) is column` rejects others.
|
|
12
12
|
-- Paths (a source's, an edit observation's, an anchor's) are repository-relative with forward slashes, in one form, so one place
|
|
@@ -293,6 +293,15 @@ create table source_processing (
|
|
|
293
293
|
) strict;
|
|
294
294
|
create index source_processing_run on source_processing (run_id);
|
|
295
295
|
|
|
296
|
+
-- The sources a harvest run may show and cite, chosen when it begins, so another harvest of the same pull request cannot change them.
|
|
297
|
+
-- Ids only; a source the owner forgets leaves with its row
|
|
298
|
+
create table harvest_run_source (
|
|
299
|
+
run_id integer not null references extraction_run (id) on delete cascade,
|
|
300
|
+
source_id integer not null references source (id) on delete cascade,
|
|
301
|
+
primary key (run_id, source_id)
|
|
302
|
+
) strict;
|
|
303
|
+
create index harvest_run_source_source on harvest_run_source (source_id);
|
|
304
|
+
|
|
296
305
|
-- An extracted unit. Its text is never rewritten: corrections are successors, withdrawals, retractions, and anchor replacements.
|
|
297
306
|
-- extraction: supported (every evidence span was found in retained text) or quarantined (with reason).
|
|
298
307
|
-- lifecycle changes only through unit_state (its trigger sets this column). revision rises with every change to the unit's relations,
|
|
@@ -981,6 +990,11 @@ create trigger source_processing_project before insert on source_processing begi
|
|
|
981
990
|
select raise(abort, 'source and run belong to different projects')
|
|
982
991
|
where (select project_id from source where id = new.source_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
983
992
|
end;
|
|
993
|
+
create trigger harvest_run_source_run before insert on harvest_run_source begin
|
|
994
|
+
select raise(abort, 'only a harvest run keeps sources, from its own project')
|
|
995
|
+
where (select origin from extraction_run where id = new.run_id) is not 'harvest'
|
|
996
|
+
or (select project_id from source where id = new.source_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
997
|
+
end;
|
|
984
998
|
|
|
985
999
|
-- Every change to a unit's relations raises its revision (stale drafts are refused against it)
|
|
986
1000
|
create trigger unit_rev_evidence_i after insert on unit_evidence begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
@@ -1211,4 +1225,4 @@ create trigger capture_delivery_prune_insert instead of insert on capture_delive
|
|
|
1211
1225
|
order by d.at, d.id limit 200);
|
|
1212
1226
|
end;
|
|
1213
1227
|
|
|
1214
|
-
pragma user_version =
|
|
1228
|
+
pragma user_version = 11;
|
package/dist/capture.js
CHANGED
|
@@ -11396,7 +11396,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
11396
11396
|
|
|
11397
11397
|
// server/src/sqlite.ts
|
|
11398
11398
|
var SCHEMA_GENERATION = 2;
|
|
11399
|
-
var SCHEMA_REVISION =
|
|
11399
|
+
var SCHEMA_REVISION = 11;
|
|
11400
11400
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
11401
11401
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
11402
11402
|
function requireRuntime() {
|
|
@@ -11597,6 +11597,7 @@ var INGEST_INSERTS = new Set([
|
|
|
11597
11597
|
"ingest_source",
|
|
11598
11598
|
"extraction_run",
|
|
11599
11599
|
"source_processing",
|
|
11600
|
+
"harvest_run_source",
|
|
11600
11601
|
"unit",
|
|
11601
11602
|
"unit_option",
|
|
11602
11603
|
"unit_evidence",
|
|
@@ -11692,6 +11693,7 @@ var FORGET_WRITES = {
|
|
|
11692
11693
|
"unit_evidence",
|
|
11693
11694
|
"unit_adoption",
|
|
11694
11695
|
"source_processing",
|
|
11696
|
+
"harvest_run_source",
|
|
11695
11697
|
"source_fts",
|
|
11696
11698
|
"field_def",
|
|
11697
11699
|
"unit_field",
|
package/dist/cli.js
CHANGED
|
@@ -93,7 +93,6 @@ var require_src = __commonJS(function(exports, module) {
|
|
|
93
93
|
|
|
94
94
|
// server/src/cli.ts
|
|
95
95
|
import fs10 from "node:fs";
|
|
96
|
-
import os4 from "node:os";
|
|
97
96
|
import path10 from "node:path";
|
|
98
97
|
|
|
99
98
|
// server/node_modules/@clack/core/dist/index.mjs
|
|
@@ -4912,7 +4911,7 @@ ${indent}${caution(note)}` : ""}`);
|
|
|
4912
4911
|
|
|
4913
4912
|
// server/src/sqlite.ts
|
|
4914
4913
|
var SCHEMA_GENERATION = 2;
|
|
4915
|
-
var SCHEMA_REVISION =
|
|
4914
|
+
var SCHEMA_REVISION = 11;
|
|
4916
4915
|
var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os2.homedir(), ".sphica");
|
|
4917
4916
|
var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
|
|
4918
4917
|
function requireRuntime() {
|
|
@@ -15957,6 +15956,7 @@ var INGEST_INSERTS = new Set([
|
|
|
15957
15956
|
"ingest_source",
|
|
15958
15957
|
"extraction_run",
|
|
15959
15958
|
"source_processing",
|
|
15959
|
+
"harvest_run_source",
|
|
15960
15960
|
"unit",
|
|
15961
15961
|
"unit_option",
|
|
15962
15962
|
"unit_evidence",
|
|
@@ -16052,6 +16052,7 @@ var FORGET_WRITES = {
|
|
|
16052
16052
|
"unit_evidence",
|
|
16053
16053
|
"unit_adoption",
|
|
16054
16054
|
"source_processing",
|
|
16055
|
+
"harvest_run_source",
|
|
16055
16056
|
"source_fts",
|
|
16056
16057
|
"field_def",
|
|
16057
16058
|
"unit_field",
|
|
@@ -16120,7 +16121,6 @@ var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), hos
|
|
|
16120
16121
|
// server/src/project.ts
|
|
16121
16122
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
16122
16123
|
import fs6 from "node:fs";
|
|
16123
|
-
import os3 from "node:os";
|
|
16124
16124
|
import path6 from "node:path";
|
|
16125
16125
|
|
|
16126
16126
|
// server/src/anchors.ts
|
|
@@ -16348,48 +16348,6 @@ function nameLocal(dir, name) {
|
|
|
16348
16348
|
});
|
|
16349
16349
|
return { key: `local:${name}`, root, name };
|
|
16350
16350
|
}
|
|
16351
|
-
function underHome(p, homes, api = path6) {
|
|
16352
|
-
for (const h2 of homes) {
|
|
16353
|
-
const rel = api.relative(h2, p);
|
|
16354
|
-
if (rel === "")
|
|
16355
|
-
return "~";
|
|
16356
|
-
if (rel !== ".." && !rel.startsWith(`..${api.sep}`) && !api.isAbsolute(rel))
|
|
16357
|
-
return `~${api.sep}${rel}`;
|
|
16358
|
-
}
|
|
16359
|
-
return p;
|
|
16360
|
-
}
|
|
16361
|
-
var projectsDir = () => path6.join(os3.homedir(), "Projects");
|
|
16362
|
-
function localRoots(roots = [projectsDir()]) {
|
|
16363
|
-
const seen = new Map;
|
|
16364
|
-
const add = (p) => {
|
|
16365
|
-
if (p)
|
|
16366
|
-
seen.set(p.key, [...new Set([...seen.get(p.key) ?? [], p.root])]);
|
|
16367
|
-
};
|
|
16368
|
-
for (const r2 of roots) {
|
|
16369
|
-
let entries = [];
|
|
16370
|
-
try {
|
|
16371
|
-
entries = fs6.readdirSync(r2, { withFileTypes: true });
|
|
16372
|
-
} catch {
|
|
16373
|
-
continue;
|
|
16374
|
-
}
|
|
16375
|
-
for (const e of entries)
|
|
16376
|
-
if (e.isDirectory() && !e.name.startsWith("."))
|
|
16377
|
-
add(identify(path6.join(r2, e.name)));
|
|
16378
|
-
}
|
|
16379
|
-
for (const [root, name] of Object.entries(localMap())) {
|
|
16380
|
-
if (LOCAL_KEY.test(name) && fs6.existsSync(root))
|
|
16381
|
-
add({ key: `local:${name}`, root, name });
|
|
16382
|
-
}
|
|
16383
|
-
const found = new Map;
|
|
16384
|
-
const ambiguous = new Map;
|
|
16385
|
-
for (const [key, dirs] of seen) {
|
|
16386
|
-
if (dirs.length === 1 && dirs[0])
|
|
16387
|
-
found.set(key, dirs[0]);
|
|
16388
|
-
else
|
|
16389
|
-
ambiguous.set(key, dirs);
|
|
16390
|
-
}
|
|
16391
|
-
return { found, ambiguous };
|
|
16392
|
-
}
|
|
16393
16351
|
function relativeTo(root, file, cwd = root) {
|
|
16394
16352
|
const abs = path6.resolve(cwd, file);
|
|
16395
16353
|
const rel = path6.relative(root, abs);
|
|
@@ -18769,14 +18727,7 @@ async function doctor(cwd) {
|
|
|
18769
18727
|
if (twice.length)
|
|
18770
18728
|
say2("warn", "Records", `${plural(twice.length, "set")} of live records hold the same words. Read them with Sphica's read and withdraw the extra ones with /sphica:glean`);
|
|
18771
18729
|
await adoptionStops(db, q.waiting, say2);
|
|
18772
|
-
const { found, ambiguous } = localRoots();
|
|
18773
|
-
const homes = [os4.homedir()];
|
|
18774
|
-
try {
|
|
18775
|
-
homes.push(fs10.realpathSync.native(os4.homedir()));
|
|
18776
|
-
} catch {}
|
|
18777
|
-
const tilde = (p) => underHome(p, homes);
|
|
18778
18730
|
const rows = await db.selectFrom("project as p").select((eb) => [
|
|
18779
|
-
"p.key",
|
|
18780
18731
|
"p.name",
|
|
18781
18732
|
eb.selectFrom("unit as u").select((u3) => u3.fn.countAll().as("n")).whereRef("u.project_id", "=", "p.id").as("records"),
|
|
18782
18733
|
eb.selectFrom("extraction_run as r").select((r2) => r2.fn.max("r.finished_at").as("at")).whereRef("r.project_id", "=", "p.id").where("r.status", "=", "saved").where("r.origin", "<>", "migration").as("extracted")
|
|
@@ -18784,11 +18735,8 @@ async function doctor(cwd) {
|
|
|
18784
18735
|
if (rows.length)
|
|
18785
18736
|
console.log(section("Projects", true));
|
|
18786
18737
|
const column = Math.max(...rows.map((x) => width(inline(x.name)))) + 2;
|
|
18787
|
-
for (const x of rows)
|
|
18788
|
-
|
|
18789
|
-
const where = copies ? ` (${copies.length} copies: ${copies.map((c) => inline(tilde(c))).sort().join(", ")})` : found.get(x.key) ? "" : ` (not found in ${inline(tilde(projectsDir()))} or the named projects)`;
|
|
18790
|
-
console.log(indent(` ${mark("none")} ${pad(inline(x.name), column)}${plural(Number(x.records ?? 0), "record")}${x.extracted ? ` / last extraction ${new Date(x.extracted).toLocaleString("sv-SE")}` : ""}${where}`));
|
|
18791
|
-
}
|
|
18738
|
+
for (const x of rows)
|
|
18739
|
+
console.log(indent(` ${mark("none")} ${pad(inline(x.name), column)}${plural(Number(x.records ?? 0), "record")}${x.extracted ? ` / last extraction ${new Date(x.extracted).toLocaleString("sv-SE")}` : ""}`));
|
|
18792
18740
|
});
|
|
18793
18741
|
} catch (e) {
|
|
18794
18742
|
say2("fail", "DB", `cannot read: ${plain(reason(e))}`);
|
package/dist/deliver.js
CHANGED
|
@@ -11427,7 +11427,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
11427
11427
|
|
|
11428
11428
|
// server/src/sqlite.ts
|
|
11429
11429
|
var SCHEMA_GENERATION = 2;
|
|
11430
|
-
var SCHEMA_REVISION =
|
|
11430
|
+
var SCHEMA_REVISION = 11;
|
|
11431
11431
|
var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os.homedir(), ".sphica");
|
|
11432
11432
|
var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
|
|
11433
11433
|
function requireRuntime() {
|
|
@@ -11652,6 +11652,7 @@ var INGEST_INSERTS = new Set([
|
|
|
11652
11652
|
"ingest_source",
|
|
11653
11653
|
"extraction_run",
|
|
11654
11654
|
"source_processing",
|
|
11655
|
+
"harvest_run_source",
|
|
11655
11656
|
"unit",
|
|
11656
11657
|
"unit_option",
|
|
11657
11658
|
"unit_evidence",
|
|
@@ -11747,6 +11748,7 @@ var FORGET_WRITES = {
|
|
|
11747
11748
|
"unit_evidence",
|
|
11748
11749
|
"unit_adoption",
|
|
11749
11750
|
"source_processing",
|
|
11751
|
+
"harvest_run_source",
|
|
11750
11752
|
"source_fts",
|
|
11751
11753
|
"field_def",
|
|
11752
11754
|
"unit_field",
|
package/dist/mcp-record.js
CHANGED
|
@@ -46183,7 +46183,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
46183
46183
|
|
|
46184
46184
|
// server/src/sqlite.ts
|
|
46185
46185
|
var SCHEMA_GENERATION = 2;
|
|
46186
|
-
var SCHEMA_REVISION =
|
|
46186
|
+
var SCHEMA_REVISION = 11;
|
|
46187
46187
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
46188
46188
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
46189
46189
|
function requireRuntime() {
|
|
@@ -46274,6 +46274,20 @@ async function inTransaction(db, fn) {
|
|
|
46274
46274
|
}
|
|
46275
46275
|
});
|
|
46276
46276
|
}
|
|
46277
|
+
async function inRolledBack(db, fn) {
|
|
46278
|
+
return db.connection().execute(async (c) => {
|
|
46279
|
+
await sql`begin immediate`.execute(c);
|
|
46280
|
+
let out;
|
|
46281
|
+
try {
|
|
46282
|
+
out = await fn(c);
|
|
46283
|
+
} catch (e) {
|
|
46284
|
+
await sql`rollback`.execute(c).catch(() => {});
|
|
46285
|
+
throw e;
|
|
46286
|
+
}
|
|
46287
|
+
await sql`rollback`.execute(c);
|
|
46288
|
+
return out;
|
|
46289
|
+
});
|
|
46290
|
+
}
|
|
46277
46291
|
var iso = (d) => {
|
|
46278
46292
|
const t = new Date(d);
|
|
46279
46293
|
if (Number.isNaN(t.getTime()))
|
|
@@ -46384,6 +46398,7 @@ var INGEST_INSERTS = new Set([
|
|
|
46384
46398
|
"ingest_source",
|
|
46385
46399
|
"extraction_run",
|
|
46386
46400
|
"source_processing",
|
|
46401
|
+
"harvest_run_source",
|
|
46387
46402
|
"unit",
|
|
46388
46403
|
"unit_option",
|
|
46389
46404
|
"unit_evidence",
|
|
@@ -46479,6 +46494,7 @@ var FORGET_WRITES = {
|
|
|
46479
46494
|
"unit_evidence",
|
|
46480
46495
|
"unit_adoption",
|
|
46481
46496
|
"source_processing",
|
|
46497
|
+
"harvest_run_source",
|
|
46482
46498
|
"source_fts",
|
|
46483
46499
|
"field_def",
|
|
46484
46500
|
"unit_field",
|
|
@@ -47787,6 +47803,7 @@ var item = (v) => ({
|
|
|
47787
47803
|
commit: null,
|
|
47788
47804
|
...v
|
|
47789
47805
|
});
|
|
47806
|
+
var said = (body) => body?.trim() ? body : "";
|
|
47790
47807
|
var sha = (s) => s && /^[0-9a-f]{40}$/.test(s) ? s : null;
|
|
47791
47808
|
var cleanPath = (p) => p && !p.startsWith("/") && !p.includes("\\") && !/\p{Cc}/u.test(p) && !p.split("/").some((x) => x === "" || x === "." || x === "..") ? p : null;
|
|
47792
47809
|
async function readPull(get, number5, repo = null) {
|
|
@@ -47807,52 +47824,49 @@ async function readPull(get, number5, repo = null) {
|
|
|
47807
47824
|
association: p.author_association ?? null,
|
|
47808
47825
|
url: p.html_url,
|
|
47809
47826
|
createdAt: p.created_at,
|
|
47810
|
-
text: p.body
|
|
47827
|
+
text: said(p.body)
|
|
47811
47828
|
}));
|
|
47812
47829
|
for (const c of comments)
|
|
47813
|
-
|
|
47814
|
-
|
|
47815
|
-
|
|
47816
|
-
|
|
47817
|
-
|
|
47818
|
-
|
|
47819
|
-
|
|
47820
|
-
|
|
47821
|
-
|
|
47822
|
-
|
|
47823
|
-
}));
|
|
47830
|
+
items.push(item({
|
|
47831
|
+
kind: "pr_comment",
|
|
47832
|
+
artifact,
|
|
47833
|
+
externalId: `comment:${c.id}`,
|
|
47834
|
+
author: c.user ?? null,
|
|
47835
|
+
association: c.author_association ?? null,
|
|
47836
|
+
url: c.html_url,
|
|
47837
|
+
createdAt: c.created_at,
|
|
47838
|
+
text: said(c.body)
|
|
47839
|
+
}));
|
|
47824
47840
|
for (const r of reviews)
|
|
47825
|
-
|
|
47826
|
-
|
|
47827
|
-
|
|
47828
|
-
|
|
47829
|
-
|
|
47830
|
-
|
|
47831
|
-
|
|
47832
|
-
|
|
47833
|
-
|
|
47834
|
-
|
|
47835
|
-
|
|
47836
|
-
|
|
47837
|
-
|
|
47838
|
-
|
|
47839
|
-
|
|
47840
|
-
|
|
47841
|
-
|
|
47842
|
-
|
|
47843
|
-
|
|
47844
|
-
|
|
47845
|
-
|
|
47846
|
-
|
|
47847
|
-
|
|
47848
|
-
|
|
47849
|
-
|
|
47850
|
-
|
|
47851
|
-
|
|
47852
|
-
|
|
47853
|
-
|
|
47854
|
-
}));
|
|
47855
|
-
}
|
|
47841
|
+
items.push(item({
|
|
47842
|
+
kind: "review",
|
|
47843
|
+
artifact,
|
|
47844
|
+
externalId: `review:${r.id}`,
|
|
47845
|
+
author: r.user ?? null,
|
|
47846
|
+
association: r.author_association ?? null,
|
|
47847
|
+
url: r.html_url,
|
|
47848
|
+
createdAt: r.submitted_at ?? p.created_at,
|
|
47849
|
+
text: said(r.body)
|
|
47850
|
+
}));
|
|
47851
|
+
for (const c of reviewComments) {
|
|
47852
|
+
const end = c.line ?? null;
|
|
47853
|
+
const start = c.start_line && c.start_line <= (end ?? 0) && (c.start_side ?? c.side) === c.side ? c.start_line : end;
|
|
47854
|
+
items.push(item({
|
|
47855
|
+
kind: "review_comment",
|
|
47856
|
+
artifact,
|
|
47857
|
+
externalId: `review_comment:${c.id}`,
|
|
47858
|
+
author: c.user ?? null,
|
|
47859
|
+
association: c.author_association ?? null,
|
|
47860
|
+
parent: c.in_reply_to_id ? `review_comment:${c.in_reply_to_id}` : null,
|
|
47861
|
+
url: c.html_url,
|
|
47862
|
+
createdAt: c.created_at,
|
|
47863
|
+
text: said(c.body),
|
|
47864
|
+
path: cleanPath(c.path),
|
|
47865
|
+
lines: cleanPath(c.path) && end && start ? [start, end] : null,
|
|
47866
|
+
hunk: c.diff_hunk ?? null,
|
|
47867
|
+
commit: sha(c.commit_id)
|
|
47868
|
+
}));
|
|
47869
|
+
}
|
|
47856
47870
|
for (const c of commits)
|
|
47857
47871
|
items.push(item({
|
|
47858
47872
|
kind: "commit_message",
|
|
@@ -47885,29 +47899,27 @@ async function readIssue(get, n) {
|
|
|
47885
47899
|
const issue2 = await get(`issues/${n}`);
|
|
47886
47900
|
if (issue2.pull_request)
|
|
47887
47901
|
return items;
|
|
47888
|
-
|
|
47902
|
+
items.push(item({
|
|
47903
|
+
kind: "issue_body",
|
|
47904
|
+
artifact: `issue:${n}`,
|
|
47905
|
+
externalId: `issue:${n}`,
|
|
47906
|
+
author: issue2.user ?? null,
|
|
47907
|
+
association: issue2.author_association ?? null,
|
|
47908
|
+
url: issue2.html_url,
|
|
47909
|
+
createdAt: issue2.created_at,
|
|
47910
|
+
text: said(issue2.body)
|
|
47911
|
+
}));
|
|
47912
|
+
for (const c of await get(`issues/${n}/comments?per_page=100`, true))
|
|
47889
47913
|
items.push(item({
|
|
47890
|
-
kind: "
|
|
47914
|
+
kind: "issue_comment",
|
|
47891
47915
|
artifact: `issue:${n}`,
|
|
47892
|
-
externalId: `
|
|
47893
|
-
author:
|
|
47894
|
-
association:
|
|
47895
|
-
url:
|
|
47896
|
-
createdAt:
|
|
47897
|
-
text:
|
|
47916
|
+
externalId: `comment:${c.id}`,
|
|
47917
|
+
author: c.user ?? null,
|
|
47918
|
+
association: c.author_association ?? null,
|
|
47919
|
+
url: c.html_url,
|
|
47920
|
+
createdAt: c.created_at,
|
|
47921
|
+
text: said(c.body)
|
|
47898
47922
|
}));
|
|
47899
|
-
for (const c of await get(`issues/${n}/comments?per_page=100`, true))
|
|
47900
|
-
if (c.body?.trim())
|
|
47901
|
-
items.push(item({
|
|
47902
|
-
kind: "issue_comment",
|
|
47903
|
-
artifact: `issue:${n}`,
|
|
47904
|
-
externalId: `comment:${c.id}`,
|
|
47905
|
-
author: c.user ?? null,
|
|
47906
|
-
association: c.author_association ?? null,
|
|
47907
|
-
url: c.html_url,
|
|
47908
|
-
createdAt: c.created_at,
|
|
47909
|
-
text: c.body
|
|
47910
|
-
}));
|
|
47911
47923
|
return items;
|
|
47912
47924
|
}
|
|
47913
47925
|
function githubTarget(repo, url2) {
|
|
@@ -47996,12 +48008,16 @@ async function linkIssues(db, projectId2, number5, closes) {
|
|
|
47996
48008
|
kind: "closes"
|
|
47997
48009
|
}).onConflict((oc) => oc.doNothing()).execute();
|
|
47998
48010
|
}
|
|
47999
|
-
async function
|
|
48011
|
+
async function pullSourceIds(db, projectId2, number5) {
|
|
48000
48012
|
const artifacts = [
|
|
48001
48013
|
`pr:${number5}`,
|
|
48002
48014
|
...(await db.selectFrom("artifact_link").select("to_artifact").where("project_id", "=", projectId2).where("from_artifact", "=", `pr:${number5}`).execute()).map((l) => l.to_artifact)
|
|
48003
48015
|
];
|
|
48004
|
-
|
|
48016
|
+
const rows = await db.selectFrom("source as s").where("s.project_id", "=", projectId2).where("s.artifact", "in", artifacts).where(({ not, exists, selectFrom }) => not(exists(selectFrom("source as n").select("n.id").whereRef("n.project_id", "=", "s.project_id").whereRef("n.kind", "=", "s.kind").whereRef("n.external_id", "=", "s.external_id").whereRef("n.revision", ">", "s.revision")))).where(({ not, exists, selectFrom }) => not(exists(selectFrom("source_forgotten as f").select("f.source_id").whereRef("f.project_id", "=", "s.project_id").whereRef("f.kind", "=", "s.kind").whereRef("f.external_id", "=", "s.external_id").whereRef("f.revision", ">", "s.revision")))).select("s.id").execute();
|
|
48017
|
+
return rows.map((r) => r.id);
|
|
48018
|
+
}
|
|
48019
|
+
async function runSources(db, runId) {
|
|
48020
|
+
return db.selectFrom("harvest_run_source as h").innerJoin("source as s", "s.id", "h.source_id").where("h.run_id", "=", runId).where(({ not, exists, selectFrom }) => not(exists(selectFrom("source_forgotten as f").select("f.source_id").whereRef("f.project_id", "=", "s.project_id").whereRef("f.kind", "=", "s.kind").whereRef("f.external_id", "=", "s.external_id").whereRef("f.revision", ">", "s.revision")))).select([
|
|
48005
48021
|
"s.id",
|
|
48006
48022
|
"s.kind",
|
|
48007
48023
|
"s.artifact",
|
|
@@ -48010,7 +48026,6 @@ async function pullSources(db, projectId2, number5) {
|
|
|
48010
48026
|
"s.author_login",
|
|
48011
48027
|
"s.author_association",
|
|
48012
48028
|
"s.created_at",
|
|
48013
|
-
"s.captured_at",
|
|
48014
48029
|
"s.path",
|
|
48015
48030
|
"s.line_start",
|
|
48016
48031
|
"s.text"
|
|
@@ -48690,9 +48705,7 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
48690
48705
|
...new Set(record4.units.flatMap((u) => [...u.supersedes ? [u.supersedes] : [], ...u.conflicts]))
|
|
48691
48706
|
];
|
|
48692
48707
|
const others = new Map((linked.length ? await db.selectFrom("unit as o").select(["o.id", "o.key", "o.kind", "o.lifecycle"]).where("o.project_id", "=", target.projectId).where("o.key", "in", linked).execute() : []).map((u) => [u.key, u]));
|
|
48693
|
-
const holders = new Map((others.size ? await db.selectFrom("unit_replacement as h").innerJoin("unit as n", "n.id", "h.from_unit").select(["h.to_unit", "n.key", "n.lifecycle"]).where("h.ended_at", "is", null).where("h.to_unit", "in", [...others.values()].map((o) => o.id)).execute() : []).map((h) => [h.to_unit, h]));
|
|
48694
48708
|
const units = [];
|
|
48695
|
-
const claimed = new Set;
|
|
48696
48709
|
for (const [i, u] of record4.units.entries()) {
|
|
48697
48710
|
const key = keys[i] ?? "";
|
|
48698
48711
|
const quarantine = [];
|
|
@@ -48850,23 +48863,14 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
48850
48863
|
let supersedes = null;
|
|
48851
48864
|
if (u.supersedes) {
|
|
48852
48865
|
const old = others.get(u.supersedes);
|
|
48853
|
-
const implemented = () => evidence.some((e) => e.role === "implements" && (["commit_message", "file_excerpt"].includes(sources.get(e.source)?.kind ?? "") || anchors.some((a) => a.role === "evidence" && (a.commit || a.observation !== null && sources.get(e.source)?.session_id === target.sessionId)))) || anchors.some((a) => a.role === "evidence" && a.commit);
|
|
48854
|
-
const takes = ["decision", "constraint"].includes(u.kind) ? adoption.some((x) => x.route !== "agent") : u.kind !== "implementation" || implemented();
|
|
48855
48866
|
if (!old)
|
|
48856
48867
|
errors3.push(`${key}: supersedes ${u.supersedes}, which is not a record of this project`);
|
|
48857
48868
|
else if (!["active", "candidate"].includes(old.lifecycle))
|
|
48858
48869
|
errors3.push(`${key}: ${u.supersedes} is already ${old.lifecycle}`);
|
|
48859
|
-
else if (claimed.has(old.id) && !quarantine.length && takes)
|
|
48860
|
-
errors3.push(`${key}: another record in this save already supersedes ${u.supersedes}`);
|
|
48861
48870
|
else if (!replaceable(u.kind, old.kind))
|
|
48862
48871
|
errors3.push(`${key}: a ${u.kind} cannot supersede ${u.supersedes}, a ${old.kind} (a record supersedes one of its own kind; a decision and a constraint can replace each other)`);
|
|
48863
|
-
else
|
|
48864
|
-
const h = holders.get(old.id);
|
|
48865
|
-
errors3.push(`${key}: ${u.supersedes} already has a successor, ${h?.key} (${h?.lifecycle}); withdraw it first, or supersede it instead`);
|
|
48866
|
-
} else
|
|
48872
|
+
else
|
|
48867
48873
|
supersedes = old.id;
|
|
48868
|
-
if (supersedes !== null && !quarantine.length && takes)
|
|
48869
|
-
claimed.add(supersedes);
|
|
48870
48874
|
}
|
|
48871
48875
|
const conflicts = u.conflicts.flatMap((k) => {
|
|
48872
48876
|
const other = others.get(k);
|
|
@@ -49000,12 +49004,20 @@ var contentHash = (u) => sha256(JSON.stringify([
|
|
|
49000
49004
|
u.revisit_when ?? null,
|
|
49001
49005
|
u.options.map((o) => o.reconsider_when === undefined ? [o.text, o.outcome, o.why ?? null] : [o.text, o.outcome, o.why ?? null, o.reconsider_when])
|
|
49002
49006
|
]));
|
|
49007
|
+
|
|
49008
|
+
class SaveRefused extends Error {
|
|
49009
|
+
}
|
|
49003
49010
|
async function settleSaved(trx, runId, saved, more = {}) {
|
|
49004
49011
|
const written = new Map(saved.written.map((w) => [w.id, w]));
|
|
49005
49012
|
const settled2 = await reconcile(trx, [...written.keys(), ...more.seeds ?? []], { runId }, {
|
|
49006
49013
|
hints: new Map(saved.written.map((w) => [w.id, w.hint])),
|
|
49007
49014
|
withdraw: more.withdraw
|
|
49008
49015
|
});
|
|
49016
|
+
const mine = new Set([...written.keys(), ...more.adopted ?? []]);
|
|
49017
|
+
const key = (id) => settled2.keys.get(id) ?? `u${id}`;
|
|
49018
|
+
for (const [id, [to, holder]] of settled2.held)
|
|
49019
|
+
if (mine.has(id))
|
|
49020
|
+
throw new SaveRefused(mine.has(holder) ? `${key(id)}: another record in this save already supersedes ${key(to)}` : `${key(id)}: ${key(to)} already has a successor, ${key(holder)} (in effect); withdraw it first, or supersede it instead`);
|
|
49009
49021
|
const now = new Map(settled2.changes.map((c) => [c.id, c.after]));
|
|
49010
49022
|
for (const w of saved.written) {
|
|
49011
49023
|
if (saved.quarantined.some((q) => q.startsWith(`${w.key} (`)))
|
|
@@ -49178,7 +49190,6 @@ ${checked.errors.map((e) => ` ${e}`).join(`
|
|
|
49178
49190
|
saved.written.push({
|
|
49179
49191
|
id,
|
|
49180
49192
|
key: p.key,
|
|
49181
|
-
adopted: p.adoption.length > 0,
|
|
49182
49193
|
hint: {
|
|
49183
49194
|
reason: p.adoption.length ? "evidence and adoption found" : "evidence found",
|
|
49184
49195
|
source: p.adoption[0]?.source ?? p.evidence[0]?.source ?? null
|
|
@@ -49476,9 +49487,6 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
49476
49487
|
errors3.push(`${what}: the merge does not adopt a proposal`);
|
|
49477
49488
|
else if (got && got.s.author_kind !== "owner" && !MAINTAINERS2.has(got.s.author_association ?? ""))
|
|
49478
49489
|
errors3.push(`${what}: only the owner or a maintainer can adopt`);
|
|
49479
|
-
const held = await db.selectFrom("unit_link as l").innerJoin("unit_replacement as r", (j) => j.onRef("r.to_unit", "=", "l.to_unit").on("r.ended_at", "is", null)).innerJoin("unit as h", "h.id", "r.from_unit").innerJoin("unit as t", "t.id", "l.to_unit").where("l.from_unit", "=", u.id).where("l.kind", "=", "supersedes").where("r.from_unit", "!=", u.id).select(["h.key as holder", "t.key as replaced"]).executeTakeFirst();
|
|
49480
|
-
if (held && !parsed.data.ops.some((o) => o.op === "withdraw" && o.unit === held.holder))
|
|
49481
|
-
errors3.push(`${what}: ${held.replaced} already has a successor, ${held.holder} (in effect); withdraw it first, or supersede it instead`);
|
|
49482
49490
|
}
|
|
49483
49491
|
if (op.op === "anchor" && !repoPath(op.path))
|
|
49484
49492
|
errors3.push(`${what}: the path is not inside the repository`);
|
|
@@ -49495,8 +49503,8 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
49495
49503
|
if (op.op === "replace_anchor") {
|
|
49496
49504
|
if (!repoPath(op.to.path))
|
|
49497
49505
|
errors3.push(`${what}: the path is not inside the repository`);
|
|
49498
|
-
const
|
|
49499
|
-
if (
|
|
49506
|
+
const said2 = await span(op.source, op.quote, what);
|
|
49507
|
+
if (said2 && said2.s.author_kind !== "owner")
|
|
49500
49508
|
errors3.push(`${what}: only the owner's words can move an anchor`);
|
|
49501
49509
|
const from = repoPath(op.from.path);
|
|
49502
49510
|
if (!from)
|
|
@@ -49772,16 +49780,11 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
49772
49780
|
}
|
|
49773
49781
|
const settled2 = await settleSaved(trx, runId, units, {
|
|
49774
49782
|
seeds: [...touched.keys(), ...withdraw.keys()],
|
|
49775
|
-
withdraw
|
|
49783
|
+
withdraw,
|
|
49784
|
+
adopted
|
|
49776
49785
|
});
|
|
49777
49786
|
for (const id of settled2.redundant)
|
|
49778
49787
|
changed2.push(`${settled2.keys.get(id)}: superseded by a record of this save, so not withdrawn`);
|
|
49779
|
-
for (const w of units.written)
|
|
49780
|
-
if (w.adopted)
|
|
49781
|
-
adopted.add(w.id);
|
|
49782
|
-
for (const [id, [to, holder]] of settled2.held)
|
|
49783
|
-
if (adopted.has(id))
|
|
49784
|
-
throw new Error(`${settled2.keys.get(id)}: ${settled2.keys.get(to)} already has a successor, ${settled2.keys.get(holder)} (in effect); withdraw it first, or supersede it instead`);
|
|
49785
49788
|
const written = new Set(units.written.map((w) => w.id));
|
|
49786
49789
|
for (const ch of settled2.changes) {
|
|
49787
49790
|
if (written.has(ch.id))
|
|
@@ -49974,7 +49977,7 @@ async function beginHarvest(db, projectId2, number5, get, beginCall) {
|
|
|
49974
49977
|
const kept = (await storeItems(trx, projectId2, pull.items)).filter((id) => id !== null).length;
|
|
49975
49978
|
await linkIssues(trx, projectId2, number5, pull.closes);
|
|
49976
49979
|
const run = newRunId();
|
|
49977
|
-
await openRun(trx, {
|
|
49980
|
+
const runId = await openRun(trx, {
|
|
49978
49981
|
projectId: projectId2,
|
|
49979
49982
|
origin: "harvest",
|
|
49980
49983
|
target: `pr:${number5}`,
|
|
@@ -49982,6 +49985,8 @@ async function beginHarvest(db, projectId2, number5, get, beginCall) {
|
|
|
49982
49985
|
draftId: run,
|
|
49983
49986
|
beginCall
|
|
49984
49987
|
});
|
|
49988
|
+
for (const sourceId of await pullSourceIds(trx, projectId2, number5))
|
|
49989
|
+
await trx.insertInto("harvest_run_source").values({ run_id: runId, source_id: sourceId }).execute();
|
|
49985
49990
|
return { run, sources: kept };
|
|
49986
49991
|
});
|
|
49987
49992
|
}
|
|
@@ -50036,7 +50041,7 @@ async function agentRun(db, run, call) {
|
|
|
50036
50041
|
async function scopeOf(db, run, root) {
|
|
50037
50042
|
if (run.origin === "harvest") {
|
|
50038
50043
|
const number5 = Number(run.target.slice("pr:".length));
|
|
50039
|
-
const sources2 =
|
|
50044
|
+
const sources2 = await runSources(db, run.id);
|
|
50040
50045
|
return {
|
|
50041
50046
|
target: {
|
|
50042
50047
|
projectId: run.project_id,
|
|
@@ -50240,42 +50245,43 @@ function contextOnly(id, items, c) {
|
|
|
50240
50245
|
...c.work && !c.units.length ? ["work: an automatic run updates work only beside a record of the messages it traces"] : []
|
|
50241
50246
|
];
|
|
50242
50247
|
}
|
|
50243
|
-
async function
|
|
50244
|
-
const run = await bound(db, id, projectId2);
|
|
50245
|
-
const { target, items } = await scopeOf(db, run, root);
|
|
50246
|
-
target.agent = await agentRun(db, run, call);
|
|
50247
|
-
const c = run.origin === "glean" ? await checkGlean(db, target, record4) : await checkRecord(db, target, record4);
|
|
50248
|
-
if (!("ops" in c))
|
|
50249
|
-
c.errors.push(...contextOnly(id, items, c));
|
|
50250
|
-
const units = "ops" in c ? c.units.units : c.units;
|
|
50251
|
-
const lines = [
|
|
50252
|
-
...c.errors.map((e) => `✗ ${e}`),
|
|
50253
|
-
...c.problems.map((p) => `△ ${p}`),
|
|
50254
|
-
...units.filter((u) => u.quarantine.length).map((u) => `△ ${u.key} will be quarantined: ${u.quarantine.join("; ")}`)
|
|
50255
|
-
];
|
|
50256
|
-
const summary = c.errors.length ? `✗ ${plural2(c.errors.length, "error")}; fix the record and check again` : `✓ ${plural2(units.length, "record")}${"ops" in c ? ` and ${plural2(c.ops.length, "change")}` : ""} can be saved`;
|
|
50257
|
-
return { ok: c.errors.length === 0, text: [...lines, summary].join(`
|
|
50258
|
-
`) };
|
|
50259
|
-
}
|
|
50260
|
-
async function saveText(db, id, projectId2, root, record4, probe, call) {
|
|
50248
|
+
async function prepare2(db, id, projectId2, root, record4, probe, call) {
|
|
50261
50249
|
const begun = await bound(db, id, projectId2);
|
|
50262
50250
|
if (call !== undefined && begun.begin_call_id !== null)
|
|
50263
50251
|
await sameCaller(db, begun.begin_call_id, call);
|
|
50264
|
-
const
|
|
50265
|
-
|
|
50266
|
-
|
|
50267
|
-
|
|
50268
|
-
|
|
50269
|
-
|
|
50270
|
-
|
|
50271
|
-
|
|
50272
|
-
|
|
50273
|
-
const
|
|
50274
|
-
|
|
50275
|
-
|
|
50276
|
-
|
|
50277
|
-
|
|
50278
|
-
|
|
50252
|
+
const gleanFacts = begun.origin === "glean" ? prepareGlean(root, record4, probe) : undefined;
|
|
50253
|
+
return { facts: gleanFacts ?? prepareRecord(root, record4, probe), gleanFacts };
|
|
50254
|
+
}
|
|
50255
|
+
async function validate2(trx, id, projectId2, root, record4, prepared, call) {
|
|
50256
|
+
const run = await bound(trx, id, projectId2);
|
|
50257
|
+
const scope = await scopeOf(trx, run, root);
|
|
50258
|
+
scope.target.agent = await agentRun(trx, run, call);
|
|
50259
|
+
const quarantine = (units) => units.filter((u) => u.quarantine.length).map((u) => `${u.key} will be quarantined: ${u.quarantine.join("; ")}`);
|
|
50260
|
+
if (run.origin === "glean") {
|
|
50261
|
+
const c = await checkGlean(trx, scope.target, record4, prepared.gleanFacts);
|
|
50262
|
+
return {
|
|
50263
|
+
run,
|
|
50264
|
+
errors: c.errors,
|
|
50265
|
+
problems: c.problems,
|
|
50266
|
+
quarantine: quarantine(c.units.units),
|
|
50267
|
+
units: c.units.units.length,
|
|
50268
|
+
ops: c.ops.length,
|
|
50269
|
+
write: async (w) => {
|
|
50270
|
+
const g = await saveGlean(w, scope.target, run.id, c);
|
|
50271
|
+
return { saved: g.units, changed: g.changed };
|
|
50272
|
+
}
|
|
50273
|
+
};
|
|
50274
|
+
}
|
|
50275
|
+
const checked = await checkRecord(trx, scope.target, record4, prepared.facts);
|
|
50276
|
+
checked.errors.push(...contextOnly(id, scope.items, checked));
|
|
50277
|
+
return {
|
|
50278
|
+
run,
|
|
50279
|
+
errors: checked.errors,
|
|
50280
|
+
problems: checked.problems,
|
|
50281
|
+
quarantine: quarantine(checked.units),
|
|
50282
|
+
units: checked.units.length,
|
|
50283
|
+
ops: null,
|
|
50284
|
+
write: async (w) => {
|
|
50279
50285
|
const shown = shownTo.get(id)?.sources ?? new Set;
|
|
50280
50286
|
const cited = new Set([
|
|
50281
50287
|
...checked.units.flatMap((u) => [
|
|
@@ -50286,17 +50292,62 @@ async function saveText(db, id, projectId2, root, record4, probe, call) {
|
|
|
50286
50292
|
]),
|
|
50287
50293
|
...checked.fieldDefs
|
|
50288
50294
|
].map((q) => q.source));
|
|
50289
|
-
|
|
50290
|
-
|
|
50291
|
-
|
|
50295
|
+
const saved = await saveRecord(w, scope.target, run.id, checked, scope.looked.filter((s) => shown.has(s) || cited.has(s)));
|
|
50296
|
+
return { saved, changed: [] };
|
|
50297
|
+
}
|
|
50298
|
+
};
|
|
50299
|
+
}
|
|
50300
|
+
async function checkText(db, id, projectId2, root, record4, call, probe) {
|
|
50301
|
+
const prepared = await prepare2(db, id, projectId2, root, record4, probe, call);
|
|
50302
|
+
const { v, done, refused } = await inRolledBack(db, async (trx) => {
|
|
50303
|
+
const v2 = await validate2(trx, id, projectId2, root, record4, prepared, call);
|
|
50304
|
+
if (v2.errors.length)
|
|
50305
|
+
return { v: v2, done: null, refused: null };
|
|
50306
|
+
try {
|
|
50307
|
+
const done2 = await v2.write(trx);
|
|
50308
|
+
await finishRun(trx, v2.run.id);
|
|
50309
|
+
return { v: v2, done: done2, refused: null };
|
|
50310
|
+
} catch (e) {
|
|
50311
|
+
if (!(e instanceof SaveRefused))
|
|
50312
|
+
throw e;
|
|
50313
|
+
return { v: v2, done: null, refused: e.message };
|
|
50314
|
+
}
|
|
50315
|
+
});
|
|
50316
|
+
const errors3 = [...v.errors, ...refused === null ? [] : [refused]];
|
|
50317
|
+
const lines = [
|
|
50318
|
+
...errors3.map((e) => `✗ ${e}`),
|
|
50319
|
+
...[...new Set(v.problems)].map((p) => `△ ${p}`),
|
|
50320
|
+
...done ? [
|
|
50321
|
+
...done.saved.active.map((k) => `✓ would be active: ${k}`),
|
|
50322
|
+
...done.saved.superseded.map((k) => `✓ would be superseded: ${k}`),
|
|
50323
|
+
...done.saved.candidates.map((c) => `△ would stay a candidate: ${c.key}: ${c.why}`),
|
|
50324
|
+
...done.saved.quarantined.map((q) => `△ would be quarantined: ${q}`),
|
|
50325
|
+
...[...new Set(done.saved.anchorProblems)].filter((a) => !v.problems.some((p) => p === a || p.endsWith(` ${a}`))).map((a) => `△ ${a}`),
|
|
50326
|
+
...done.changed.map((c) => `✓ would: ${c}`)
|
|
50327
|
+
] : v.quarantine.map((q) => `△ ${q}`)
|
|
50328
|
+
];
|
|
50329
|
+
const summary = errors3.length ? `✗ ${plural2(errors3.length, "error")}; fix the record and check again` : `✓ ${plural2(v.units, "record")}${v.ops === null ? "" : ` and ${plural2(v.ops, "change")}`} can be saved`;
|
|
50330
|
+
return { ok: errors3.length === 0, text: [...lines, summary].join(`
|
|
50331
|
+
`) };
|
|
50332
|
+
}
|
|
50333
|
+
async function saveText(db, id, projectId2, root, record4, probe, call) {
|
|
50334
|
+
const prepared = await prepare2(db, id, projectId2, root, record4, probe, call);
|
|
50335
|
+
const text3 = await inTransaction(db, async (trx) => {
|
|
50336
|
+
const v = await validate2(trx, id, projectId2, root, record4, prepared, call);
|
|
50337
|
+
if (v.errors.length)
|
|
50338
|
+
throw new Error(`The record is not valid:
|
|
50339
|
+
${v.errors.map((e) => ` ${e}`).join(`
|
|
50340
|
+
`)}`);
|
|
50341
|
+
const { saved, changed: changed2 } = await v.write(trx);
|
|
50342
|
+
await finishRun(trx, v.run.id);
|
|
50292
50343
|
return [
|
|
50293
50344
|
...saved.active.map((k) => `✓ ${k} active`),
|
|
50294
50345
|
...saved.superseded.map((k) => `✓ ${k} superseded`),
|
|
50295
50346
|
...saved.candidates.map((c) => `△ ${c.key} candidate: ${c.why}`),
|
|
50296
50347
|
...saved.quarantined.map((q) => `△ ${q} quarantined`),
|
|
50297
50348
|
...saved.anchorProblems.map((a) => `△ ${a}`),
|
|
50298
|
-
...
|
|
50299
|
-
...
|
|
50349
|
+
...v.ops === null ? v.problems.map((n) => `△ ${n}`) : [],
|
|
50350
|
+
...changed2.map((c) => `✓ ${c}`),
|
|
50300
50351
|
"✓ saved"
|
|
50301
50352
|
].join(`
|
|
50302
50353
|
`);
|
|
@@ -50595,7 +50646,7 @@ server.registerTool("record_context", {
|
|
|
50595
50646
|
}));
|
|
50596
50647
|
server.registerTool("record_check", {
|
|
50597
50648
|
title: "Check a record",
|
|
50598
|
-
description: "Checks a record
|
|
50649
|
+
description: "Checks a record by running the save in a transaction that rolls back: it refuses what record_save would refuse and says what would become of each record (would be active, would stay a candidate, would be superseded), writing nothing.",
|
|
50599
50650
|
inputSchema: exports_external.object({ run: RUN, record: RECORD, cwd: CWD }).strict(),
|
|
50600
50651
|
annotations: READ
|
|
50601
50652
|
}, async (a, extra) => tool(async () => {
|
package/dist/mcp.js
CHANGED
|
@@ -46004,7 +46004,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
46004
46004
|
|
|
46005
46005
|
// server/src/sqlite.ts
|
|
46006
46006
|
var SCHEMA_GENERATION = 2;
|
|
46007
|
-
var SCHEMA_REVISION =
|
|
46007
|
+
var SCHEMA_REVISION = 11;
|
|
46008
46008
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
46009
46009
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
46010
46010
|
function requireRuntime() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sphica",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.37",
|
|
4
4
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/skills/glean/SKILL.md
CHANGED
|
@@ -49,8 +49,9 @@ read tools `search` and `read`. Pass the repository root as `cwd` to every tool.
|
|
|
49
49
|
the owner's message that quotes it
|
|
50
50
|
5. **Read**: `record_context` with the run: the owner's messages in this session with their refs. When a page ends with
|
|
51
51
|
`call record_context with after: "s<N>"`, call it again with that `after`
|
|
52
|
-
6. **Check**: `record_check` with the run and the record below.
|
|
53
|
-
|
|
52
|
+
6. **Check**: `record_check` with the run and the record below. It runs the save in a transaction that rolls back, so it refuses what the
|
|
53
|
+
save would refuse and lists what would change (`would be active`, `would: <change>`) without writing it. Fix errors and check again. A
|
|
54
|
+
note to ask the owner for a source means step 2 is not done
|
|
54
55
|
7. **Save**: `record_save`. **Report** what changed, copying save's lines
|
|
55
56
|
|
|
56
57
|
## The record
|
package/skills/harvest/SKILL.md
CHANGED
|
@@ -37,7 +37,8 @@ as `cwd` to every tool.
|
|
|
37
37
|
sources an earlier harvest already looked at: what they decided may already be saved, so search before recording it again. Read all of it before writing:
|
|
38
38
|
when a page ends with `call record_context with after: "s<N>"`, call it again with that `after`, until the last page
|
|
39
39
|
4. **Check**: `record_check` with the run and the record as `record`. The shape and fields are trace's ([../trace/SKILL.md](../trace/SKILL.md),
|
|
40
|
-
"The record"), with `work` left out. Keys are saved as `harvest:<number>/<key>`.
|
|
40
|
+
"The record"), with `work` left out. Keys are saved as `harvest:<number>/<key>`. As in trace, check runs the save in a transaction that
|
|
41
|
+
rolls back and says what would become of each record (`would be active`, ...). Fix and check again until there are no errors.
|
|
41
42
|
A file the pull request touched may have moved since: on a warning that a path is not in the working tree, give the commit that holds it,
|
|
42
43
|
or anchor where the code is now
|
|
43
44
|
5. **Save**: `record_save` with the same run and record
|
package/skills/trace/SKILL.md
CHANGED
|
@@ -41,7 +41,9 @@ Everything goes through Sphica's `record` MCP server (its tools are `trace_pendi
|
|
|
41
41
|
the last page (the one with the live records). Saving marks as looked at only the messages you were shown and those you quote; the
|
|
42
42
|
rest stay pending.
|
|
43
43
|
Use `search` and `read` to look at older records this session may replace
|
|
44
|
-
4. **Check**: `record_check` with the run and the record below as `record`.
|
|
44
|
+
4. **Check**: `record_check` with the run and the record below as `record`. It runs the save in a transaction that rolls back, so nothing is
|
|
45
|
+
written: it refuses what the save would refuse, and says what would become of each record (`would be active`, `would stay a candidate` with
|
|
46
|
+
the reason, `would be superseded`, `would be quarantined`). Errors refuse the save; fix and check again. Warnings say what will be
|
|
45
47
|
left out, quarantined, or kept as a candidate, and why. An anchor warning (a path not in the working tree, with near paths; a directory; a
|
|
46
48
|
symbol not in the file) means fix the anchor and check again; keep it only when you know it is right
|
|
47
49
|
5. **Save**: `record_save` with the same run and record. A run saves once
|