sphica 0.6.13 → 0.6.15
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/db/migrations/0005.check.sql +36 -0
- package/db/migrations/0005.sql +1172 -0
- package/db/schema.sql +230 -72
- package/dist/capture.js +81 -13
- package/dist/cli.js +179 -29
- package/dist/deliver.js +81 -13
- package/dist/mcp-record.js +184 -52
- package/dist/mcp.js +2 -4
- package/package.json +1 -1
- package/skills/glean/SKILL.md +1 -1
package/dist/mcp-record.js
CHANGED
|
@@ -45869,7 +45869,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
45869
45869
|
|
|
45870
45870
|
// server/src/sqlite.ts
|
|
45871
45871
|
var SCHEMA_GENERATION = 2;
|
|
45872
|
-
var SCHEMA_REVISION =
|
|
45872
|
+
var SCHEMA_REVISION = 5;
|
|
45873
45873
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
45874
45874
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
45875
45875
|
function requireRuntime() {
|
|
@@ -46047,24 +46047,92 @@ function captureAuthorizer(own2, action, p1, p2, triggerOrView) {
|
|
|
46047
46047
|
return C2.SQLITE_DENY;
|
|
46048
46048
|
}
|
|
46049
46049
|
var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
|
|
46050
|
-
var
|
|
46051
|
-
|
|
46052
|
-
|
|
46050
|
+
var INGEST_INSERTS = new Set([
|
|
46051
|
+
"project",
|
|
46052
|
+
"ingest_source",
|
|
46053
|
+
"extraction_run",
|
|
46054
|
+
"source_processing",
|
|
46055
|
+
"unit",
|
|
46056
|
+
"unit_option",
|
|
46057
|
+
"unit_evidence",
|
|
46058
|
+
"unit_adoption",
|
|
46059
|
+
"unit_link",
|
|
46060
|
+
"unit_state",
|
|
46061
|
+
"unit_anchor",
|
|
46062
|
+
"unit_alias",
|
|
46063
|
+
"field_def",
|
|
46064
|
+
"unit_field",
|
|
46065
|
+
"work",
|
|
46066
|
+
"artifact_link"
|
|
46067
|
+
]);
|
|
46068
|
+
var RETRACTION = [
|
|
46069
|
+
"retracted_at",
|
|
46070
|
+
"retraction_reason",
|
|
46071
|
+
"retraction_source_id",
|
|
46072
|
+
"retraction_span_start",
|
|
46073
|
+
"retraction_span_end"
|
|
46074
|
+
];
|
|
46075
|
+
var INGEST_UPDATES = {
|
|
46076
|
+
extraction_run: new Set(["status", "finished_at"]),
|
|
46077
|
+
unit_anchor: new Set(["retired_at", "replaced_by"]),
|
|
46078
|
+
unit_link: new Set(["resolved_at", "resolution"]),
|
|
46079
|
+
unit_evidence: new Set(RETRACTION),
|
|
46080
|
+
unit_adoption: new Set(RETRACTION),
|
|
46081
|
+
work: new Set(["title", "goal", "current", "next", "status", "branch", "run_id", "updated_at"])
|
|
46082
|
+
};
|
|
46083
|
+
var INGEST_DELETES = new Set(["artifact_link"]);
|
|
46084
|
+
var INGEST_TRIGGER_WRITES = {
|
|
46085
|
+
ingest_source_insert: ["insert source"],
|
|
46086
|
+
source_fts_ai: ["insert source_fts"],
|
|
46087
|
+
source_fts_ad: ["delete source_fts"],
|
|
46088
|
+
unit_state_apply: ["update unit"],
|
|
46089
|
+
unit_state_restore: ["insert unit_state"],
|
|
46090
|
+
...Object.fromEntries([
|
|
46091
|
+
"evidence_i",
|
|
46092
|
+
"evidence_u",
|
|
46093
|
+
"evidence_d",
|
|
46094
|
+
"adoption_i",
|
|
46095
|
+
"adoption_u",
|
|
46096
|
+
"adoption_d",
|
|
46097
|
+
"link_i",
|
|
46098
|
+
"link_u",
|
|
46099
|
+
"anchor_i",
|
|
46100
|
+
"anchor_u",
|
|
46101
|
+
"alias_i",
|
|
46102
|
+
"field_i",
|
|
46103
|
+
"field_d"
|
|
46104
|
+
].map((t) => [`unit_rev_${t}`, ["update unit"]])),
|
|
46105
|
+
unit_fts_ai: ["insert unit_fts"],
|
|
46106
|
+
unit_fts_ad: ["delete unit_fts"],
|
|
46107
|
+
...Object.fromEntries(["option_i", "anchor_i", "anchor_u", "anchor_d", "alias_i", "alias_d", "field_i", "field_d"].map((t) => [
|
|
46108
|
+
`unit_fts_${t}`,
|
|
46109
|
+
["delete unit_fts", "insert unit_fts"]
|
|
46110
|
+
]))
|
|
46111
|
+
};
|
|
46112
|
+
var opOf = (action) => action === C2.SQLITE_INSERT ? "insert" : action === C2.SQLITE_UPDATE ? "update" : action === C2.SQLITE_DELETE ? "delete" : undefined;
|
|
46113
|
+
function ingestAuthorizer(action, p1, p2, trigger) {
|
|
46053
46114
|
if (DDL().has(action))
|
|
46054
46115
|
return C2.SQLITE_DENY;
|
|
46055
|
-
|
|
46056
|
-
|
|
46057
|
-
|
|
46058
|
-
|
|
46116
|
+
const op = opOf(action);
|
|
46117
|
+
if (op) {
|
|
46118
|
+
const table = p1 ?? "";
|
|
46119
|
+
if (SHADOW.test(table))
|
|
46120
|
+
return C2.SQLITE_OK;
|
|
46121
|
+
if (trigger !== null)
|
|
46122
|
+
return INGEST_TRIGGER_WRITES[trigger]?.includes(`${op} ${table}`) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
46123
|
+
const allowed = op === "insert" ? INGEST_INSERTS.has(table) : op === "update" ? INGEST_UPDATES[table]?.has(p2 ?? "") : INGEST_DELETES.has(table);
|
|
46124
|
+
return allowed ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
46125
|
+
}
|
|
46059
46126
|
if (action === C2.SQLITE_PRAGMA)
|
|
46060
46127
|
return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
46061
|
-
|
|
46128
|
+
if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_RECURSIVE || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
|
|
46129
|
+
return C2.SQLITE_OK;
|
|
46130
|
+
return C2.SQLITE_DENY;
|
|
46062
46131
|
}
|
|
46063
46132
|
var FORGET_WRITES = {
|
|
46064
46133
|
[C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts", "unit_fts"]),
|
|
46065
46134
|
[C2.SQLITE_DELETE]: new Set([
|
|
46066
46135
|
"source",
|
|
46067
|
-
"external_reference",
|
|
46068
46136
|
"unit_evidence",
|
|
46069
46137
|
"unit_adoption",
|
|
46070
46138
|
"source_processing",
|
|
@@ -46104,7 +46172,7 @@ function connectWriter(role, file2 = dbFile(), create = false) {
|
|
|
46104
46172
|
throw e;
|
|
46105
46173
|
}
|
|
46106
46174
|
if (role === "ingest")
|
|
46107
|
-
raw.setAuthorizer(ingestAuthorizer);
|
|
46175
|
+
raw.setAuthorizer((action, p1, p2, _db, trigger) => ingestAuthorizer(action, p1, p2, trigger));
|
|
46108
46176
|
else if (role === "forget")
|
|
46109
46177
|
raw.setAuthorizer(forgetAuthorizer);
|
|
46110
46178
|
else if (role === "capture") {
|
|
@@ -46348,7 +46416,7 @@ async function projectId(db, key) {
|
|
|
46348
46416
|
function relativeTo(root, file2, cwd = root) {
|
|
46349
46417
|
const abs = path4.resolve(cwd, file2);
|
|
46350
46418
|
const rel = path4.relative(root, abs);
|
|
46351
|
-
if (!rel || leaves(rel))
|
|
46419
|
+
if (!rel || leaves(rel) || /\p{Cc}/u.test(rel))
|
|
46352
46420
|
return null;
|
|
46353
46421
|
return rel.split(path4.sep).join("/");
|
|
46354
46422
|
}
|
|
@@ -46380,7 +46448,7 @@ var git2 = (root, args) => {
|
|
|
46380
46448
|
return null;
|
|
46381
46449
|
}
|
|
46382
46450
|
};
|
|
46383
|
-
var usable = (p) => p !== "" && !p.includes("\\");
|
|
46451
|
+
var usable = (p) => p !== "" && !p.includes("\\") && !/\p{Cc}/u.test(p);
|
|
46384
46452
|
function snapshot(root) {
|
|
46385
46453
|
const out = git2(root, ["status", "--porcelain=v2", "-z", "--untracked-files=all"]);
|
|
46386
46454
|
if (out === null)
|
|
@@ -47030,7 +47098,7 @@ var item = (v) => ({
|
|
|
47030
47098
|
...v
|
|
47031
47099
|
});
|
|
47032
47100
|
var sha = (s) => s && /^[0-9a-f]{40}$/.test(s) ? s : null;
|
|
47033
|
-
var cleanPath = (p) => p && !p.startsWith("/") && !p.includes("\\") && !p.split("/").some((x) => x === "" || x === "." || x === "..") ? p : null;
|
|
47101
|
+
var cleanPath = (p) => p && !p.startsWith("/") && !p.includes("\\") && !/\p{Cc}/u.test(p) && !p.split("/").some((x) => x === "" || x === "." || x === "..") ? p : null;
|
|
47034
47102
|
async function readPull(get, number5, repo = null) {
|
|
47035
47103
|
const p = await get(`pulls/${number5}`);
|
|
47036
47104
|
const artifact = `pr:${number5}`;
|
|
@@ -47090,7 +47158,7 @@ async function readPull(get, number5, repo = null) {
|
|
|
47090
47158
|
createdAt: c.created_at,
|
|
47091
47159
|
text: c.body,
|
|
47092
47160
|
path: cleanPath(c.path),
|
|
47093
|
-
lines: end && start ? [start, end] : null,
|
|
47161
|
+
lines: cleanPath(c.path) && end && start ? [start, end] : null,
|
|
47094
47162
|
hunk: c.diff_hunk ?? null,
|
|
47095
47163
|
commit: sha(c.commit_id)
|
|
47096
47164
|
}));
|
|
@@ -47173,7 +47241,7 @@ async function storeItems(db, projectId2, items) {
|
|
|
47173
47241
|
for (const it of items) {
|
|
47174
47242
|
const kept = fit(it.text);
|
|
47175
47243
|
const hash2 = sha256(kept.body);
|
|
47176
|
-
const latest = await db.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", it.kind).where("external_id", "=", it.externalId).orderBy("revision", "desc").executeTakeFirst();
|
|
47244
|
+
const latest = await db.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", it.kind).where("external_id", "=", it.externalId).where("session_id", "is", null).orderBy("revision", "desc").executeTakeFirst();
|
|
47177
47245
|
const forgotten = await db.selectFrom("source_forgotten").select(["revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", it.kind).where("external_id", "=", it.externalId).execute();
|
|
47178
47246
|
const lastForgotten = Math.max(0, ...forgotten.map((f) => f.revision));
|
|
47179
47247
|
if (latest && latest.revision > lastForgotten && Buffer.from(latest.content_hash).equals(hash2)) {
|
|
@@ -47188,19 +47256,20 @@ async function storeItems(db, projectId2, items) {
|
|
|
47188
47256
|
const bound = authorId !== null && owners.has(authorId) && SPOKEN.has(it.kind);
|
|
47189
47257
|
const kind = bound ? "owner" : it.author?.type === "Bot" ? "bot" : "person";
|
|
47190
47258
|
const created = iso(it.createdAt);
|
|
47191
|
-
const
|
|
47259
|
+
const revision = Math.max(latest?.revision ?? 0, lastForgotten) + 1;
|
|
47260
|
+
await db.insertInto("ingest_source").values({
|
|
47192
47261
|
project_id: projectId2,
|
|
47193
47262
|
kind: it.kind,
|
|
47194
47263
|
artifact: it.artifact,
|
|
47195
47264
|
external_id: it.externalId,
|
|
47196
|
-
revision
|
|
47265
|
+
revision,
|
|
47197
47266
|
author_kind: kind,
|
|
47198
47267
|
author_login: it.author?.login ?? null,
|
|
47199
47268
|
author_external_id: authorId,
|
|
47200
47269
|
author_association: it.association,
|
|
47201
47270
|
parent_external_id: it.parent,
|
|
47202
47271
|
event_kind: it.event,
|
|
47203
|
-
url: it.url,
|
|
47272
|
+
url: it.url && /^https?:\/\//.test(it.url) ? it.url : null,
|
|
47204
47273
|
created_at: created,
|
|
47205
47274
|
available_at: latest || lastForgotten ? null : created,
|
|
47206
47275
|
captured_at: now,
|
|
@@ -47215,11 +47284,15 @@ async function storeItems(db, projectId2, items) {
|
|
|
47215
47284
|
diff_hunk: it.hunk === null ? null : fit(it.hunk).body,
|
|
47216
47285
|
commit_sha: it.commit,
|
|
47217
47286
|
indexed: it.kind === "pr_event" ? 0 : 1
|
|
47218
|
-
}).
|
|
47219
|
-
ids.push(
|
|
47287
|
+
}).execute();
|
|
47288
|
+
ids.push(await itemId(db, projectId2, it.kind, it.externalId, revision));
|
|
47220
47289
|
}
|
|
47221
47290
|
return ids;
|
|
47222
47291
|
}
|
|
47292
|
+
async function itemId(db, projectId2, kind, externalId, revision) {
|
|
47293
|
+
const row = await db.selectFrom("source").select("id").where("project_id", "=", projectId2).where("kind", "=", kind).where("external_id", "=", externalId).where("revision", "=", revision).where("session_id", "is", null).executeTakeFirstOrThrow();
|
|
47294
|
+
return row.id;
|
|
47295
|
+
}
|
|
47223
47296
|
async function linkIssues(db, projectId2, number5, closes) {
|
|
47224
47297
|
let stale = db.deleteFrom("artifact_link").where("project_id", "=", projectId2).where("from_artifact", "=", `pr:${number5}`).where("kind", "=", "closes");
|
|
47225
47298
|
if (closes.length)
|
|
@@ -47526,7 +47599,8 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
47526
47599
|
const linked = [
|
|
47527
47600
|
...new Set(record3.units.flatMap((u) => [...u.supersedes ? [u.supersedes] : [], ...u.conflicts]))
|
|
47528
47601
|
];
|
|
47529
|
-
const others = new Map((linked.length ? await db.selectFrom("unit").select(["id", "key", "lifecycle"]).where("project_id", "=", target.projectId).where("key", "in", linked).execute() : []).map((u) => [u.key, u]));
|
|
47602
|
+
const others = new Map((linked.length ? await db.selectFrom("unit").select(["id", "key", "kind", "lifecycle"]).where("project_id", "=", target.projectId).where("key", "in", linked).execute() : []).map((u) => [u.key, u]));
|
|
47603
|
+
const holders = new Map((others.size ? await db.selectFrom("unit_link as l").innerJoin("unit as n", "n.id", "l.from_unit").select(["l.to_unit", "n.key", "n.lifecycle"]).where("l.kind", "=", "supersedes").where("l.to_unit", "in", [...others.values()].map((o) => o.id)).where("n.lifecycle", "<>", "withdrawn").where("n.extraction", "=", "supported").where("n.unsourced", "=", 0).execute() : []).map((h) => [h.to_unit, h]));
|
|
47530
47604
|
const maintainers = new Set(record3.units.some((u) => u.supersedes || u.conflicts.length) ? (await db.selectFrom("source").select("author_login").distinct().where("project_id", "=", target.projectId).where("author_association", "in", [...MAINTAINERS]).execute()).flatMap((r) => r.author_login ? [r.author_login] : []) : []);
|
|
47531
47605
|
const units = [];
|
|
47532
47606
|
const claimed = new Set;
|
|
@@ -47647,6 +47721,22 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
47647
47721
|
problems.push(`${key}: another path-only anchor on ${x.path} already covers it; left out`);
|
|
47648
47722
|
anchors.splice(anchors.indexOf(x), 1);
|
|
47649
47723
|
}
|
|
47724
|
+
const places = new Set;
|
|
47725
|
+
for (const x of [...anchors]) {
|
|
47726
|
+
const at = [
|
|
47727
|
+
x.path,
|
|
47728
|
+
x.role,
|
|
47729
|
+
x.commit ?? "",
|
|
47730
|
+
x.symbol ?? "",
|
|
47731
|
+
x.symbol ? "" : x.lines ? place(x) : ""
|
|
47732
|
+
].join("\x00");
|
|
47733
|
+
if (!places.has(at)) {
|
|
47734
|
+
places.add(at);
|
|
47735
|
+
continue;
|
|
47736
|
+
}
|
|
47737
|
+
problems.push(`${key}: the anchor on ${x.path}${x.symbol ? ` ${x.symbol}` : ""} appears twice; left out`);
|
|
47738
|
+
anchors.splice(anchors.indexOf(x), 1);
|
|
47739
|
+
}
|
|
47650
47740
|
const aliases = [...new Set(u.aliases.map((a) => a.trim()))];
|
|
47651
47741
|
const bad = aliases.filter((a) => !a || a.length > 40);
|
|
47652
47742
|
if (bad.length)
|
|
@@ -47658,11 +47748,16 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
47658
47748
|
errors3.push(`${key}: supersedes ${u.supersedes}, which is not a record of this project`);
|
|
47659
47749
|
else if (!["active", "candidate"].includes(old.lifecycle))
|
|
47660
47750
|
errors3.push(`${key}: ${u.supersedes} is already ${old.lifecycle}`);
|
|
47661
|
-
else if (claimed.has(old.id))
|
|
47751
|
+
else if (claimed.has(old.id) && !quarantine.length)
|
|
47662
47752
|
errors3.push(`${key}: another record in this save already supersedes ${u.supersedes}`);
|
|
47663
|
-
else
|
|
47753
|
+
else if (!replaceable(u.kind, old.kind))
|
|
47754
|
+
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)`);
|
|
47755
|
+
else if (holders.has(old.id) && !quarantine.length) {
|
|
47756
|
+
const h = holders.get(old.id);
|
|
47757
|
+
errors3.push(`${key}: ${u.supersedes} already has a successor, ${h?.key} (${h?.lifecycle}); withdraw it first, or supersede it instead`);
|
|
47758
|
+
} else
|
|
47664
47759
|
supersedes = old.id;
|
|
47665
|
-
if (supersedes !== null)
|
|
47760
|
+
if (supersedes !== null && !quarantine.length)
|
|
47666
47761
|
claimed.add(supersedes);
|
|
47667
47762
|
}
|
|
47668
47763
|
const conflicts = u.conflicts.flatMap((k) => {
|
|
@@ -47764,6 +47859,7 @@ var contentHash = (u) => sha256(JSON.stringify([
|
|
|
47764
47859
|
u.revisit_when ?? null,
|
|
47765
47860
|
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])
|
|
47766
47861
|
]));
|
|
47862
|
+
var replaceable = (successor, old) => successor === old || ["decision", "constraint"].includes(successor) && ["decision", "constraint"].includes(old);
|
|
47767
47863
|
var ACTIVATION = /needs|cannot become active/;
|
|
47768
47864
|
async function saveRecord(trx, target, runId, checked, looked) {
|
|
47769
47865
|
if (checked.errors.length)
|
|
@@ -47966,9 +48062,14 @@ ${checked.errors.map((e) => ` ${e}`).join(`
|
|
|
47966
48062
|
}
|
|
47967
48063
|
for (const s of new Set(looked))
|
|
47968
48064
|
await trx.insertInto("source_processing").values({ source_id: s, run_id: runId, outcome: cited.has(s) ? "units" : "no_unit" }).onConflict((oc) => oc.doNothing()).execute();
|
|
47969
|
-
await trx.updateTable("extraction_run").set({ status: "saved", finished_at: now }).where("id", "=", runId).execute();
|
|
47970
48065
|
return saved;
|
|
47971
48066
|
}
|
|
48067
|
+
async function finishRun(trx, runId) {
|
|
48068
|
+
await trx.updateTable("extraction_run").set((eb) => ({
|
|
48069
|
+
status: "saved",
|
|
48070
|
+
finished_at: eb.fn("max", [eb.val(iso(Date.now())), eb.ref("started_at")])
|
|
48071
|
+
})).where("id", "=", runId).execute();
|
|
48072
|
+
}
|
|
47972
48073
|
|
|
47973
48074
|
// server/src/glean.ts
|
|
47974
48075
|
var MAX_FILE = 1024 * 1024;
|
|
@@ -48175,7 +48276,7 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
48175
48276
|
}
|
|
48176
48277
|
return { s, at };
|
|
48177
48278
|
};
|
|
48178
|
-
const replaced = new
|
|
48279
|
+
const replaced = new Map;
|
|
48179
48280
|
const anchored = new Set;
|
|
48180
48281
|
const places = [];
|
|
48181
48282
|
for (const [i, op] of parsed.data.ops.entries()) {
|
|
@@ -48245,17 +48346,20 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
48245
48346
|
if (!from)
|
|
48246
48347
|
errors3.push(`${what}: the from path is not inside the repository`);
|
|
48247
48348
|
else {
|
|
48248
|
-
let q = db.selectFrom("unit_anchor").select("id").where("unit_id", "=", u.id).where("path", "=", from).where("retired_at", "is", null);
|
|
48349
|
+
let q = db.selectFrom("unit_anchor").select(["id", "path", "symbol", "role", "commit_sha"]).where("unit_id", "=", u.id).where("path", "=", from).where("retired_at", "is", null);
|
|
48249
48350
|
if (op.from.symbol)
|
|
48250
48351
|
q = q.where("symbol", "=", op.from.symbol);
|
|
48251
48352
|
const live = await q.execute();
|
|
48252
48353
|
const name = `${from}${op.from.symbol ? ` ${op.from.symbol}` : ""}`;
|
|
48253
48354
|
if (live.length === 1) {
|
|
48254
48355
|
replaces = live[0]?.id ?? null;
|
|
48356
|
+
const held = live[0];
|
|
48357
|
+
if (held && held.commit_sha === null && held.path === repoPath(op.to.path) && held.symbol === (op.to.symbol ?? null) && held.role === op.to.role)
|
|
48358
|
+
errors3.push(`${what}: the anchor on ${name} is already that place`);
|
|
48255
48359
|
if (replaces !== null && replaced.has(replaces))
|
|
48256
48360
|
errors3.push(`${what}: another operation in this batch already replaces ${name}`);
|
|
48257
48361
|
if (replaces !== null)
|
|
48258
|
-
replaced.
|
|
48362
|
+
replaced.set(replaces, i);
|
|
48259
48363
|
} else if (!live.length)
|
|
48260
48364
|
errors3.push(`${what}: no live anchor on ${name}`);
|
|
48261
48365
|
else
|
|
@@ -48271,13 +48375,26 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
48271
48375
|
if (anchored.has(k))
|
|
48272
48376
|
errors3.push(`${what}: another operation in this batch already anchors ${name}`);
|
|
48273
48377
|
anchored.add(k);
|
|
48274
|
-
places.push({
|
|
48378
|
+
places.push({
|
|
48379
|
+
what,
|
|
48380
|
+
unit: u.id,
|
|
48381
|
+
rel,
|
|
48382
|
+
symbol: dest.symbol ?? null,
|
|
48383
|
+
role: dest.role,
|
|
48384
|
+
commit,
|
|
48385
|
+
name,
|
|
48386
|
+
replacing: op.op === "replace_anchor" ? i : null
|
|
48387
|
+
});
|
|
48275
48388
|
}
|
|
48276
48389
|
if (op.op === "retract_evidence" || op.op === "retract_adoption" || op.op === "withdraw" || op.op === "resolve_conflict") {
|
|
48277
48390
|
const got = await span(op.reason_source, op.reason_quote, what);
|
|
48278
48391
|
if (got && got.s.author_kind !== "owner")
|
|
48279
48392
|
errors3.push(`${what}: only the owner's words can retract, withdraw, or resolve`);
|
|
48280
48393
|
}
|
|
48394
|
+
if (op.op === "withdraw") {
|
|
48395
|
+
if (u.lifecycle === "superseded")
|
|
48396
|
+
errors3.push(`${what}: ${op.unit} is superseded, so there is nothing live to withdraw (withdraw the record that replaced it, if that one no longer holds)`);
|
|
48397
|
+
}
|
|
48281
48398
|
if (op.op === "resolve_conflict") {
|
|
48282
48399
|
const open2 = await db.selectFrom("unit_link as l").innerJoin("unit as a", "a.id", "l.from_unit").innerJoin("unit as b", "b.id", "l.to_unit").select("l.from_unit").where("l.kind", "=", "conflicts").where("l.resolved_at", "is", null).where((eb) => eb.or([
|
|
48283
48400
|
eb.and([eb("a.id", "=", u.id), eb("b.key", "=", op.with)]),
|
|
@@ -48308,11 +48425,14 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
48308
48425
|
ops.push({ input: op, unitId: u.id, lifecycle: u.lifecycle, excerpt, retracts, replaces });
|
|
48309
48426
|
}
|
|
48310
48427
|
for (const x of places) {
|
|
48311
|
-
|
|
48428
|
+
const gone = [...replaced].filter(([, by]) => x.replacing === null || by < x.replacing).map(([id]) => id);
|
|
48429
|
+
let q = db.selectFrom("unit_anchor").select("id").where("unit_id", "=", x.unit).where("path", "=", x.rel).where("role", "=", x.role).where("retired_at", "is", null).where("id", "not in", [...gone, -1]);
|
|
48312
48430
|
q = x.symbol ? q.where("symbol", "=", x.symbol) : q.where("symbol", "is", null);
|
|
48313
48431
|
q = x.commit ? q.where("commit_sha", "=", x.commit) : q.where("commit_sha", "is", null);
|
|
48314
|
-
|
|
48315
|
-
|
|
48432
|
+
const held = await q.executeTakeFirst();
|
|
48433
|
+
if (!held || x.replacing !== null && replaced.get(held.id) === x.replacing)
|
|
48434
|
+
continue;
|
|
48435
|
+
errors3.push(replaced.has(held.id) ? `${x.what}: a later operation of this batch moves the anchor off ${x.name}; put that replacement before this one, or make the moves in two saves` : `${x.what}: the record already has a live anchor on ${x.name}`);
|
|
48316
48436
|
}
|
|
48317
48437
|
return { errors: errors3, problems, units, ops };
|
|
48318
48438
|
}
|
|
@@ -48327,19 +48447,20 @@ async function forgottenExcerpt(db, projectId2, x) {
|
|
|
48327
48447
|
async function excerptSource(trx, projectId2, x) {
|
|
48328
48448
|
const external2 = `file:${x.path}@${x.blob}#L${x.lines[0]}-${x.lines[1]}`;
|
|
48329
48449
|
const hash2 = sha256(x.text);
|
|
48330
|
-
const found = await trx.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", "file_excerpt").where("external_id", "=", external2).orderBy("revision", "desc").executeTakeFirst();
|
|
48450
|
+
const found = await trx.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", "file_excerpt").where("external_id", "=", external2).where("session_id", "is", null).orderBy("revision", "desc").executeTakeFirst();
|
|
48331
48451
|
const forgotten = await forgottenExcerpt(trx, projectId2, x);
|
|
48332
48452
|
if (found && found.revision > forgotten.last && found.content_hash.equals(hash2))
|
|
48333
48453
|
return found.id;
|
|
48334
48454
|
if (forgotten.same)
|
|
48335
48455
|
throw new Error(`the owner forgot ${x.path} lines ${x.lines.join("-")}; cite something else`);
|
|
48336
48456
|
const now = iso(Date.now());
|
|
48337
|
-
const
|
|
48457
|
+
const revision2 = Math.max(found?.revision ?? 0, forgotten.last) + 1;
|
|
48458
|
+
await trx.insertInto("ingest_source").values({
|
|
48338
48459
|
project_id: projectId2,
|
|
48339
48460
|
kind: "file_excerpt",
|
|
48340
48461
|
artifact: `file:${x.path}`,
|
|
48341
48462
|
external_id: external2,
|
|
48342
|
-
revision:
|
|
48463
|
+
revision: revision2,
|
|
48343
48464
|
author_kind: "person",
|
|
48344
48465
|
created_at: now,
|
|
48345
48466
|
captured_at: now,
|
|
@@ -48354,8 +48475,8 @@ async function excerptSource(trx, projectId2, x) {
|
|
|
48354
48475
|
commit_sha: x.commit,
|
|
48355
48476
|
blob_sha: x.blob,
|
|
48356
48477
|
indexed: 1
|
|
48357
|
-
}).
|
|
48358
|
-
return
|
|
48478
|
+
}).execute();
|
|
48479
|
+
return itemId(trx, projectId2, "file_excerpt", external2, revision2);
|
|
48359
48480
|
}
|
|
48360
48481
|
async function move(trx, unitId, to, reason2, sourceId, runId) {
|
|
48361
48482
|
const u = await trx.selectFrom("unit").select("lifecycle").where("id", "=", unitId).executeTakeFirstOrThrow();
|
|
@@ -48392,7 +48513,11 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
48392
48513
|
return { id: s.id, start: at[0], end: at[1] };
|
|
48393
48514
|
};
|
|
48394
48515
|
const touched = new Map;
|
|
48395
|
-
|
|
48516
|
+
const ordered = [
|
|
48517
|
+
...c.ops.filter((p) => p.input.op === "replace_anchor"),
|
|
48518
|
+
...c.ops.filter((p) => p.input.op !== "replace_anchor")
|
|
48519
|
+
];
|
|
48520
|
+
for (const p of ordered) {
|
|
48396
48521
|
const op = p.input;
|
|
48397
48522
|
touched.set(p.unitId, op.unit);
|
|
48398
48523
|
if (op.op === "add_evidence") {
|
|
@@ -48431,6 +48556,11 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
48431
48556
|
refresh(c.units.facts, rel);
|
|
48432
48557
|
const symbol2 = to.symbol && !symbolMasked(c.units.facts, rel, to.symbol) ? to.symbol : null;
|
|
48433
48558
|
const at = symbol2 ? symbolAt(c.units.facts, rel, symbol2) : null;
|
|
48559
|
+
if (op.op === "replace_anchor") {
|
|
48560
|
+
const held = await trx.selectFrom("unit_anchor as a").innerJoin("unit as u", "u.id", "a.unit_id").select(["u.kind", "u.lifecycle", "a.role", "a.commit_sha", "a.edit_observation_id"]).where("a.id", "=", p.replaces ?? -1).executeTakeFirst();
|
|
48561
|
+
if (held?.kind === "implementation" && held.lifecycle === "active" && held.role === "evidence" && (held.commit_sha !== null || held.edit_observation_id !== null))
|
|
48562
|
+
await move(trx, p.unitId, "candidate", "glean: code anchor replaced, support checked again", null, runId);
|
|
48563
|
+
}
|
|
48434
48564
|
const added = await trx.insertInto("unit_anchor").values({
|
|
48435
48565
|
unit_id: p.unitId,
|
|
48436
48566
|
path: rel,
|
|
@@ -48445,11 +48575,6 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
48445
48575
|
}).returning("id").executeTakeFirstOrThrow();
|
|
48446
48576
|
if (op.op === "replace_anchor")
|
|
48447
48577
|
await trx.updateTable("unit_anchor").set({ retired_at: now, replaced_by: added.id }).where("id", "=", p.replaces ?? -1).where("retired_at", "is", null).execute();
|
|
48448
|
-
if (op.op === "replace_anchor") {
|
|
48449
|
-
const held = await trx.selectFrom("unit_anchor as a").innerJoin("unit as u", "u.id", "a.unit_id").select(["u.kind", "u.lifecycle", "a.role", "a.commit_sha", "a.edit_observation_id"]).where("a.id", "=", p.replaces ?? -1).executeTakeFirst();
|
|
48450
|
-
if (held?.kind === "implementation" && held.lifecycle === "active" && held.role === "evidence" && (held.commit_sha !== null || held.edit_observation_id !== null))
|
|
48451
|
-
await move(trx, p.unitId, "candidate", "glean: code anchor replaced, support checked again", null, runId);
|
|
48452
|
-
}
|
|
48453
48578
|
changed2.push(`${op.unit}: anchor ${op.op === "anchor" ? "added" : "replaced"}`);
|
|
48454
48579
|
} else {
|
|
48455
48580
|
const reason2 = await spanOf(op.reason_source, op.reason_quote);
|
|
@@ -48470,9 +48595,22 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
48470
48595
|
continue;
|
|
48471
48596
|
}
|
|
48472
48597
|
if (op.op === "withdraw") {
|
|
48598
|
+
touched.delete(p.unitId);
|
|
48599
|
+
const held = await trx.selectFrom("unit").select("lifecycle").where("id", "=", p.unitId).executeTakeFirstOrThrow();
|
|
48600
|
+
if (held.lifecycle === "superseded") {
|
|
48601
|
+
changed2.push(`${op.unit}: superseded by a record of this save, so not withdrawn`);
|
|
48602
|
+
continue;
|
|
48603
|
+
}
|
|
48604
|
+
const replaced = await trx.selectFrom("unit_link as l").innerJoin("unit as o", "o.id", "l.to_unit").select(["o.id", "o.key"]).where("l.from_unit", "=", p.unitId).where("l.kind", "=", "supersedes").where("o.lifecycle", "=", "superseded").execute();
|
|
48473
48605
|
await move(trx, p.unitId, "withdrawn", `withdrawn: ${head(op.reason_quote, 200)}`, reason2.id, runId);
|
|
48474
48606
|
changed2.push(`${op.unit}: withdrawn`);
|
|
48475
|
-
|
|
48607
|
+
for (const o of replaced) {
|
|
48608
|
+
const now2 = await trx.selectFrom("unit").select("lifecycle").where("id", "=", o.id).executeTakeFirstOrThrow();
|
|
48609
|
+
if (now2.lifecycle !== "candidate")
|
|
48610
|
+
continue;
|
|
48611
|
+
changed2.push(`${o.key}: no longer superseded`);
|
|
48612
|
+
touched.set(o.id, o.key);
|
|
48613
|
+
}
|
|
48476
48614
|
continue;
|
|
48477
48615
|
}
|
|
48478
48616
|
const current2 = await trx.selectFrom("unit").select("lifecycle").where("id", "=", p.unitId).executeTakeFirstOrThrow();
|
|
@@ -48500,7 +48638,6 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
48500
48638
|
changed2.push(`${o.key}: superseded`);
|
|
48501
48639
|
}
|
|
48502
48640
|
}
|
|
48503
|
-
await trx.updateTable("extraction_run").set({ status: "saved", finished_at: now }).where("id", "=", runId).execute();
|
|
48504
48641
|
return { units, changed: changed2 };
|
|
48505
48642
|
}
|
|
48506
48643
|
|
|
@@ -48868,6 +49005,7 @@ async function saveText(db, id, projectId2, root, record3, probe) {
|
|
|
48868
49005
|
].map((q) => q.source));
|
|
48869
49006
|
return saveRecord(trx, scope.target, run.id, checked, scope.looked.filter((s) => shown.has(s) || cited.has(s)));
|
|
48870
49007
|
});
|
|
49008
|
+
await finishRun(trx, run.id);
|
|
48871
49009
|
return [
|
|
48872
49010
|
...saved.active.map((k) => `✓ ${k} active`),
|
|
48873
49011
|
...saved.superseded.map((k) => `✓ ${k} superseded`),
|
|
@@ -48925,7 +49063,6 @@ async function forgetIn(trx, projectId2, ids, at) {
|
|
|
48925
49063
|
sources: rows.map((r) => ({ id: r.id, kind: r.kind, artifact: r.artifact, bytes: r.bytes })),
|
|
48926
49064
|
already,
|
|
48927
49065
|
units: [],
|
|
48928
|
-
references: 0,
|
|
48929
49066
|
fields: { definitions: 0, values: 0 }
|
|
48930
49067
|
};
|
|
48931
49068
|
if (!rows.length)
|
|
@@ -48959,9 +49096,6 @@ async function forgetIn(trx, projectId2, ids, at) {
|
|
|
48959
49096
|
content_hash: r.content_hash,
|
|
48960
49097
|
batch_id: batch
|
|
48961
49098
|
}))).execute();
|
|
48962
|
-
for (const t of ["unit_evidence", "unit_adoption"])
|
|
48963
|
-
await trx.deleteFrom(t).where("retraction_source_id", "in", targets).where("retracted_at", "is not", null).where("source_id", "not in", targets).execute();
|
|
48964
|
-
outcome.references = Number((await trx.deleteFrom("external_reference").where("owner_source_id", "in", targets).executeTakeFirst()).numDeletedRows);
|
|
48965
49099
|
await trx.deleteFrom("source").where("id", "in", targets).execute();
|
|
48966
49100
|
const reason2 = `sources ${targets.map((id) => `s${id}`).join(", ")} forgotten by the owner`;
|
|
48967
49101
|
for (const u of touched) {
|
|
@@ -49045,8 +49179,6 @@ function forgetText(o, file3) {
|
|
|
49045
49179
|
const change = u.before === u.after ? `stays ${u.after}` : `${u.before} → ${u.after}`;
|
|
49046
49180
|
lines.push(`- record ${inline(u.key).slice(0, 120)}: ${change}, loses ${plural2(u.removed, "citation")}`);
|
|
49047
49181
|
}
|
|
49048
|
-
if (o.references)
|
|
49049
|
-
lines.push(`- ${plural2(o.references, "unfetched reference")} the forgotten messages gave`);
|
|
49050
49182
|
if (o.fields.definitions || o.fields.values)
|
|
49051
49183
|
lines.push(`- ${plural2(o.fields.definitions, "field definition")} and ${plural2(o.fields.values, "field value")} go with them`);
|
|
49052
49184
|
if (o.units.length || o.fields.values)
|
package/dist/mcp.js
CHANGED
|
@@ -46030,7 +46030,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
|
46030
46030
|
|
|
46031
46031
|
// server/src/sqlite.ts
|
|
46032
46032
|
var SCHEMA_GENERATION = 2;
|
|
46033
|
-
var SCHEMA_REVISION =
|
|
46033
|
+
var SCHEMA_REVISION = 5;
|
|
46034
46034
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
46035
46035
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
46036
46036
|
function requireRuntime() {
|
|
@@ -46354,10 +46354,8 @@ async function describe3(db, u, root, asOf) {
|
|
|
46354
46354
|
for (const l of links) {
|
|
46355
46355
|
if (l.kind === "supersedes")
|
|
46356
46356
|
out.push(l.from_id === u.id ? `Supersedes ${l.to_key}` : `Superseded by ${l.from_key}`);
|
|
46357
|
-
else if (l.kind === "conflicts")
|
|
46358
|
-
out.push(`Conflicts with ${l.from_id === u.id ? l.to_key : l.from_key}${l.resolved_at ? " (resolved)" : " (unresolved)"}`);
|
|
46359
46357
|
else
|
|
46360
|
-
out.push(
|
|
46358
|
+
out.push(`Conflicts with ${l.from_id === u.id ? l.to_key : l.from_key}${l.resolved_at ? " (resolved)" : " (unresolved)"}`);
|
|
46361
46359
|
}
|
|
46362
46360
|
out.push(`History: ${history.map((s) => `${s.to_state} ${s.at} (${s.reason})`).join("; ")}`);
|
|
46363
46361
|
return out.join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sphica",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
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
|
@@ -73,7 +73,7 @@ read tools `search` and `read`. Pass the repository root as `cwd` to every tool.
|
|
|
73
73
|
| `anchor` / `replace_anchor` | Adds a code location, or replaces one whose code moved (`from` and `to`, citing the owner's words); the old one is kept as history. A replacement carries no commit, so an implementation whose proof was the replaced anchor goes back to candidate: add an `anchor` op with `commit` in the same batch to keep it active. A `symbol` that is a key or a value Sphica masks is refused: anchor a name, or the path alone |
|
|
74
74
|
| `retract_evidence` / `retract_adoption` | Marks a link mistaken, citing the owner's words (`reason_source`, `reason_quote`). When the record cites the same source more than once, add `quote` to say which one. It is kept as history, and the record is judged again |
|
|
75
75
|
| `resolve_conflict` | Ends an unresolved conflict between `unit` and `with`, citing the owner's words (`reason_source`, `reason_quote`). Until then neither record is shown on its own |
|
|
76
|
-
| `withdraw` | Withdraws a record the owner says no longer holds, citing the owner's words |
|
|
76
|
+
| `withdraw` | Withdraws a record the owner says no longer holds, citing the owner's words. A superseded record is refused: withdraw the record that replaced it |
|
|
77
77
|
|
|
78
78
|
Every op carries the `revision` read printed; a record changed since is refused, so read it again. To correct what a record says, write a new
|
|
79
79
|
record in `units` (trace's shape, keys saved as `glean:<key>`) with `supersedes` naming the old one (citing the owner's words, a maintainer's, or the owner's session, not other people's text); records are never rewritten.
|