sphica 0.6.3 → 0.6.5
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 +2 -0
- package/db/migrations/0002.sql +1 -1
- package/db/migrations/0003.sql +193 -0
- package/db/schema.sql +19 -6
- package/dist/capture.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/deliver.js +23 -5
- package/dist/mcp-record.js +98 -28
- package/dist/mcp.js +520 -124
- package/package.json +1 -1
- package/skills/forget/SKILL.md +1 -1
- package/skills/glean/SKILL.md +1 -1
- package/skills/harvest/SKILL.md +1 -1
- package/skills/review/SKILL.md +1 -0
- package/skills/rules/SKILL.md +52 -0
- package/skills/rules/agents/openai.yaml +2 -0
- package/skills/trace/SKILL.md +7 -4
package/dist/mcp.js
CHANGED
|
@@ -45726,7 +45726,7 @@ async function liveSuccessors(db, id) {
|
|
|
45726
45726
|
async function judgeUnits(db, rows, wanted, hits) {
|
|
45727
45727
|
const ids = rows.map((r) => r.id);
|
|
45728
45728
|
const [options, anchors, aliases] = ids.length ? await Promise.all([
|
|
45729
|
-
db.selectFrom("unit_option").select(["unit_id", "text", "outcome", "why"]).where("unit_id", "in", ids).orderBy("position").execute(),
|
|
45729
|
+
db.selectFrom("unit_option").select(["unit_id", "text", "outcome", "why", "reconsider_when"]).where("unit_id", "in", ids).orderBy("position").execute(),
|
|
45730
45730
|
db.selectFrom("unit_anchor").select(["unit_id", "path", "symbol", "role"]).where("unit_id", "in", ids).where("retired_at", "is", null).orderBy("id").execute(),
|
|
45731
45731
|
db.selectFrom("unit_alias").select(["unit_id", "terms", "content_hash"]).where("unit_id", "in", ids).orderBy("id", "desc").execute()
|
|
45732
45732
|
]) : [[], [], []];
|
|
@@ -45740,7 +45740,7 @@ async function judgeUnits(db, rows, wanted, hits) {
|
|
|
45740
45740
|
r.why,
|
|
45741
45741
|
r.scope_note,
|
|
45742
45742
|
r.revisit_when,
|
|
45743
|
-
...opts.flatMap((o) => [o.text, o.why]),
|
|
45743
|
+
...opts.flatMap((o) => [o.text, o.why, o.reconsider_when]),
|
|
45744
45744
|
...anch.flatMap((a) => [a.path, a.symbol])
|
|
45745
45745
|
].filter(Boolean).join(`
|
|
45746
45746
|
`)));
|
|
@@ -46000,7 +46000,7 @@ import os from "node:os";
|
|
|
46000
46000
|
import path from "node:path";
|
|
46001
46001
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
46002
46002
|
var SCHEMA_GENERATION = 2;
|
|
46003
|
-
var SCHEMA_REVISION =
|
|
46003
|
+
var SCHEMA_REVISION = 3;
|
|
46004
46004
|
var sphicaHome = () => process.env.SPHICA_HOME || path.join(os.homedir(), ".sphica");
|
|
46005
46005
|
var dbFile = () => process.env.SPHICA_DB || path.join(sphicaHome(), "sphica.db");
|
|
46006
46006
|
function requireRuntime() {
|
|
@@ -46122,54 +46122,74 @@ var LIFECYCLES = ["candidate", "active", "superseded", "withdrawn"];
|
|
|
46122
46122
|
var HOSTS = ["claude-code", "codex"];
|
|
46123
46123
|
var sessionId = (projectId, host, externalId) => uuidFrom(String(projectId), host, externalId);
|
|
46124
46124
|
|
|
46125
|
-
// server/src/
|
|
46126
|
-
import fs2 from "node:fs";
|
|
46127
|
-
import path2 from "node:path";
|
|
46128
|
-
import { fileURLToPath } from "node:url";
|
|
46129
|
-
var MANIFEST = path2.join(".claude-plugin", "plugin.json");
|
|
46130
|
-
function versionAt(root) {
|
|
46131
|
-
try {
|
|
46132
|
-
const m = JSON.parse(fs2.readFileSync(path2.join(root, MANIFEST), "utf8"));
|
|
46133
|
-
return m.name === "sphica" && typeof m.version === "string" ? m.version : null;
|
|
46134
|
-
} catch {
|
|
46135
|
-
return null;
|
|
46136
|
-
}
|
|
46137
|
-
}
|
|
46138
|
-
var here = path2.dirname(fileURLToPath(import.meta.url));
|
|
46139
|
-
var ROOT = [path2.join(here, ".."), path2.join(here, "..", "..", "plugin")].find((r) => versionAt(r) !== null) ?? path2.join(here, "..");
|
|
46140
|
-
var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
46141
|
-
|
|
46142
|
-
// server/src/project.ts
|
|
46143
|
-
import { execFileSync } from "node:child_process";
|
|
46144
|
-
import fs4 from "node:fs";
|
|
46125
|
+
// server/src/overview.ts
|
|
46145
46126
|
import path4 from "node:path";
|
|
46146
46127
|
|
|
46147
46128
|
// server/src/anchors.ts
|
|
46148
|
-
import
|
|
46149
|
-
import
|
|
46129
|
+
import fs2 from "node:fs";
|
|
46130
|
+
import path2 from "node:path";
|
|
46150
46131
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
46151
|
-
var leaves = (rel) => rel === ".." || rel.startsWith(`..${
|
|
46132
|
+
var leaves = (rel) => rel === ".." || rel.startsWith(`..${path2.sep}`) || path2.isAbsolute(rel);
|
|
46152
46133
|
var literal3 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
46153
46134
|
function readText(root, rel) {
|
|
46154
|
-
const abs =
|
|
46155
|
-
if (leaves(
|
|
46135
|
+
const abs = path2.join(root, rel);
|
|
46136
|
+
if (leaves(path2.relative(root, abs)))
|
|
46156
46137
|
return;
|
|
46157
|
-
const st =
|
|
46138
|
+
const st = fs2.lstatSync(abs, { throwIfNoEntry: false });
|
|
46158
46139
|
if (!st)
|
|
46159
46140
|
return null;
|
|
46160
46141
|
if (!st.isFile() || st.size > MAX_BYTES)
|
|
46161
46142
|
return;
|
|
46162
|
-
|
|
46163
|
-
|
|
46143
|
+
try {
|
|
46144
|
+
const inside = path2.relative(fs2.realpathSync(root), fs2.realpathSync(abs));
|
|
46145
|
+
if (leaves(inside))
|
|
46146
|
+
return;
|
|
46147
|
+
const buf = fs2.readFileSync(abs);
|
|
46148
|
+
return buf.includes(0) ? undefined : buf.toString("utf8");
|
|
46149
|
+
} catch {
|
|
46164
46150
|
return;
|
|
46165
|
-
|
|
46166
|
-
return buf.includes(0) ? undefined : buf.toString("utf8");
|
|
46151
|
+
}
|
|
46167
46152
|
}
|
|
46168
46153
|
function findSymbol(text, symbol2) {
|
|
46169
46154
|
const re = new RegExp(`(?<![\\w$])${literal3(symbol2)}(?![\\w$])`);
|
|
46170
46155
|
const lines = text.split(/\r?\n/);
|
|
46171
46156
|
return { lines, i: lines.findIndex((l) => re.test(l)) };
|
|
46172
46157
|
}
|
|
46158
|
+
function fileState(root, rel) {
|
|
46159
|
+
if (!root)
|
|
46160
|
+
return "unknown";
|
|
46161
|
+
if (leaves(path2.relative(root, path2.join(root, rel))))
|
|
46162
|
+
return "unknown";
|
|
46163
|
+
let realRoot;
|
|
46164
|
+
try {
|
|
46165
|
+
realRoot = fs2.realpathSync(root);
|
|
46166
|
+
} catch {
|
|
46167
|
+
return "unknown";
|
|
46168
|
+
}
|
|
46169
|
+
let at = root;
|
|
46170
|
+
for (const part of rel.split("/")) {
|
|
46171
|
+
at = path2.join(at, part);
|
|
46172
|
+
let st;
|
|
46173
|
+
try {
|
|
46174
|
+
st = fs2.lstatSync(at, { throwIfNoEntry: false });
|
|
46175
|
+
} catch (e) {
|
|
46176
|
+
if (e.code === "ENOTDIR")
|
|
46177
|
+
return "gone";
|
|
46178
|
+
return "unknown";
|
|
46179
|
+
}
|
|
46180
|
+
if (!st)
|
|
46181
|
+
return "gone";
|
|
46182
|
+
if (st.isSymbolicLink()) {
|
|
46183
|
+
try {
|
|
46184
|
+
if (leaves(path2.relative(realRoot, fs2.realpathSync(at))))
|
|
46185
|
+
return "unknown";
|
|
46186
|
+
} catch {
|
|
46187
|
+
return "unknown";
|
|
46188
|
+
}
|
|
46189
|
+
}
|
|
46190
|
+
}
|
|
46191
|
+
return "present";
|
|
46192
|
+
}
|
|
46173
46193
|
function checkAnchor(root, a) {
|
|
46174
46194
|
if (!root)
|
|
46175
46195
|
return { state: "unknown", line: null };
|
|
@@ -46189,91 +46209,21 @@ function checkAnchor(root, a) {
|
|
|
46189
46209
|
};
|
|
46190
46210
|
}
|
|
46191
46211
|
|
|
46192
|
-
// server/src/project.ts
|
|
46193
|
-
var localFile = () => path4.join(sphicaHome(), "projects.json");
|
|
46194
|
-
var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
46195
|
-
function normalizeRemote(url2) {
|
|
46196
|
-
const raw = String(url2 ?? "").trim();
|
|
46197
|
-
if (!raw)
|
|
46198
|
-
return null;
|
|
46199
|
-
const scp = raw.match(/^(?:[^@/]+@)?([^:/]+):(?!\/)(.+?)(?:\.git)?$/);
|
|
46200
|
-
if (scp)
|
|
46201
|
-
return `${scp[1]}/${scp[2]}`;
|
|
46202
|
-
try {
|
|
46203
|
-
const u = new URL(raw);
|
|
46204
|
-
if (!u.hostname)
|
|
46205
|
-
return null;
|
|
46206
|
-
const p = u.pathname.replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
|
|
46207
|
-
return p ? `${u.hostname}/${p}` : u.hostname;
|
|
46208
|
-
} catch {
|
|
46209
|
-
return null;
|
|
46210
|
-
}
|
|
46211
|
-
}
|
|
46212
|
-
var git = (dir, ...args) => {
|
|
46213
|
-
try {
|
|
46214
|
-
return execFileSync("git", ["-C", dir, ...args], {
|
|
46215
|
-
encoding: "utf8",
|
|
46216
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
46217
|
-
timeout: 5000
|
|
46218
|
-
}).trim();
|
|
46219
|
-
} catch {
|
|
46220
|
-
return null;
|
|
46221
|
-
}
|
|
46222
|
-
};
|
|
46223
|
-
function localMap() {
|
|
46224
|
-
let raw;
|
|
46225
|
-
try {
|
|
46226
|
-
raw = fs4.readFileSync(localFile(), "utf8");
|
|
46227
|
-
} catch (e) {
|
|
46228
|
-
if (e.code === "ENOENT")
|
|
46229
|
-
return {};
|
|
46230
|
-
throw e;
|
|
46231
|
-
}
|
|
46232
|
-
let m;
|
|
46233
|
-
try {
|
|
46234
|
-
m = JSON.parse(raw);
|
|
46235
|
-
} catch {
|
|
46236
|
-
m = null;
|
|
46237
|
-
}
|
|
46238
|
-
if (!m || typeof m !== "object" || Array.isArray(m) || Object.entries(m).some(([root, name]) => !path4.isAbsolute(root) || typeof name !== "string" || !LOCAL_KEY.test(name)))
|
|
46239
|
-
throw new Error(`${localFile()} is not a valid JSON project table. Fix or delete it, then name the project again.`);
|
|
46240
|
-
return m;
|
|
46241
|
-
}
|
|
46242
|
-
function identify(dir) {
|
|
46243
|
-
const given = path4.resolve(dir);
|
|
46244
|
-
const top = git(given, "rev-parse", "--show-toplevel");
|
|
46245
|
-
const root = top || given;
|
|
46246
|
-
const remote = top ? normalizeRemote(git(root, "remote", "get-url", "origin")) : null;
|
|
46247
|
-
if (remote)
|
|
46248
|
-
return { key: `git:${remote}`, root, name: remote.split("/").slice(1).join("/") || remote };
|
|
46249
|
-
const map3 = localMap();
|
|
46250
|
-
for (let d = root;; d = path4.dirname(d)) {
|
|
46251
|
-
const local = map3[d];
|
|
46252
|
-
if (local && LOCAL_KEY.test(local))
|
|
46253
|
-
return { key: `local:${local}`, root: d, name: local };
|
|
46254
|
-
if (top || path4.dirname(d) === d)
|
|
46255
|
-
return null;
|
|
46256
|
-
}
|
|
46257
|
-
}
|
|
46258
|
-
async function projectId(db, key) {
|
|
46259
|
-
const r = await db.selectFrom("project").select("id").where("key", "=", key).executeTakeFirst();
|
|
46260
|
-
return r?.id ?? null;
|
|
46261
|
-
}
|
|
46262
|
-
|
|
46263
46212
|
// server/src/read.ts
|
|
46213
|
+
var UNSUPPORTED = "unsupported: its owner quote was retracted or forgotten, so it is not the owner's condition";
|
|
46264
46214
|
var cut = (text, start, end) => Buffer.from(text, "utf8").subarray(start, end).toString("utf8");
|
|
46265
46215
|
var speaker = (s) => s.author_kind === "owner" ? "the owner" : s.author_kind === "assistant" ? "the assistant" : `${s.author_login ?? "someone"} (${s.author_association ?? s.author_kind})`;
|
|
46266
|
-
async function readUnit(db,
|
|
46216
|
+
async function readUnit(db, projectId, ref, root, asOf) {
|
|
46267
46217
|
const byId = /^u([1-9][0-9]{0,15})$/.exec(ref);
|
|
46268
|
-
const u = await db.selectFrom("unit").selectAll().where("project_id", "=",
|
|
46269
|
-
const bare = u ?? (byId ? undefined : await db.selectFrom("unit").selectAll().where("project_id", "=",
|
|
46218
|
+
const u = await db.selectFrom("unit").selectAll().where("project_id", "=", projectId).where(byId ? "id" : "key", "=", byId ? Number(byId[1]) : ref).executeTakeFirst();
|
|
46219
|
+
const bare = u ?? (byId ? undefined : await db.selectFrom("unit").selectAll().where("project_id", "=", projectId).where("key", "in", ["trace:", "harvest:", "glean:"].map((o) => o + ref)).execute().then((rows) => rows.length === 1 ? rows[0] : undefined));
|
|
46270
46220
|
if (!bare || asOf && bare.created_at > asOf)
|
|
46271
46221
|
return null;
|
|
46272
46222
|
return describe3(db, bare, root, asOf);
|
|
46273
46223
|
}
|
|
46274
46224
|
async function describe3(db, u, root, asOf) {
|
|
46275
46225
|
const [options, evidence, adoption, anchors, links, states] = await Promise.all([
|
|
46276
|
-
db.selectFrom("unit_option").select(["id", "text", "outcome", "why"]).where("unit_id", "=", u.id).orderBy("position").execute(),
|
|
46226
|
+
db.selectFrom("unit_option").select(["id", "text", "outcome", "why", "reconsider_when"]).where("unit_id", "=", u.id).orderBy("position").execute(),
|
|
46277
46227
|
db.selectFrom("unit_evidence as e").innerJoin("source as s", "s.id", "e.source_id").where("e.unit_id", "=", u.id).where("e.added_at", "<=", asOf ?? "9999").select([
|
|
46278
46228
|
"e.option_id",
|
|
46279
46229
|
"e.role",
|
|
@@ -46343,6 +46293,10 @@ async function describe3(db, u, root, asOf) {
|
|
|
46343
46293
|
out.push("Options:");
|
|
46344
46294
|
for (const o of options) {
|
|
46345
46295
|
out.push(`- ${o.text}: ${o.outcome}${o.why ? `, because ${o.why}` : ""}`);
|
|
46296
|
+
if (o.reconsider_when) {
|
|
46297
|
+
const stands = evidence.some((e) => e.option_id === o.id && e.role === "reconsiders" && !e.retracted_at);
|
|
46298
|
+
out.push(` Reconsider when: ${inline(o.reconsider_when)}${stands ? " (the owner's words are quoted below)" : ` [${UNSUPPORTED}]`}`);
|
|
46299
|
+
}
|
|
46346
46300
|
for (const e of evidence.filter((x) => x.option_id === o.id))
|
|
46347
46301
|
out.push(quote(e));
|
|
46348
46302
|
}
|
|
@@ -46375,11 +46329,11 @@ async function describe3(db, u, root, asOf) {
|
|
|
46375
46329
|
return out.join(`
|
|
46376
46330
|
`);
|
|
46377
46331
|
}
|
|
46378
|
-
async function readSource(db,
|
|
46332
|
+
async function readSource(db, projectId, ref) {
|
|
46379
46333
|
const m = /^s([1-9][0-9]{0,15})(?:@(\d{1,9}))?$/.exec(ref);
|
|
46380
46334
|
if (!m)
|
|
46381
46335
|
return null;
|
|
46382
|
-
const s = await db.selectFrom("source").selectAll().where("project_id", "=",
|
|
46336
|
+
const s = await db.selectFrom("source").selectAll().where("project_id", "=", projectId).where("id", "=", Number(m[1])).executeTakeFirst();
|
|
46383
46337
|
if (!s)
|
|
46384
46338
|
return null;
|
|
46385
46339
|
return [
|
|
@@ -46399,6 +46353,405 @@ function part(id, text, from) {
|
|
|
46399
46353
|
return end < all.length ? [shown, `(${all.length - end} more bytes; read s${id}@${end} for the rest)`] : [shown];
|
|
46400
46354
|
}
|
|
46401
46355
|
|
|
46356
|
+
// server/src/rule-files.ts
|
|
46357
|
+
import fs3 from "node:fs";
|
|
46358
|
+
import path3 from "node:path";
|
|
46359
|
+
|
|
46360
|
+
// server/src/git.ts
|
|
46361
|
+
import { execFileSync } from "node:child_process";
|
|
46362
|
+
function cleanGit(root, args, max = 1024 * 1024) {
|
|
46363
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("GIT_")));
|
|
46364
|
+
return execFileSync("git", ["-C", root, ...args], {
|
|
46365
|
+
env,
|
|
46366
|
+
maxBuffer: max,
|
|
46367
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
46368
|
+
timeout: 1e4
|
|
46369
|
+
});
|
|
46370
|
+
}
|
|
46371
|
+
|
|
46372
|
+
// server/src/rule-files.ts
|
|
46373
|
+
var RULE_LIMITS = { files: 200, bytes: 256 * 1024, depth: 8, entries: 5000 };
|
|
46374
|
+
var NAMES = new Set(["CLAUDE.md", "AGENTS.md", "AGENTS.override.md"]);
|
|
46375
|
+
var PATHSPECS = [...NAMES, ".claude/rules/**/*.md"].map((p) => `:(glob)**/${p}`);
|
|
46376
|
+
function isRuleFile(rel) {
|
|
46377
|
+
const parts = rel.split("/");
|
|
46378
|
+
const name = parts.at(-1) ?? "";
|
|
46379
|
+
if (NAMES.has(name))
|
|
46380
|
+
return true;
|
|
46381
|
+
const i = parts.findIndex((p, k) => p === ".claude" && parts[k + 1] === "rules");
|
|
46382
|
+
return i >= 0 && parts.length > i + 2 && name.endsWith(".md");
|
|
46383
|
+
}
|
|
46384
|
+
function ruleFiles(root) {
|
|
46385
|
+
const listed = gitList(root) ?? walk(root);
|
|
46386
|
+
const out = { files: [], skipped: 0, incomplete: listed.incomplete };
|
|
46387
|
+
const realRoot = fs3.realpathSync(root);
|
|
46388
|
+
for (const [i, rel] of [...new Set(listed.paths)].sort().entries()) {
|
|
46389
|
+
if (i >= RULE_LIMITS.files) {
|
|
46390
|
+
out.skipped++;
|
|
46391
|
+
continue;
|
|
46392
|
+
}
|
|
46393
|
+
const text = readBounded(root, realRoot, rel);
|
|
46394
|
+
if (text === null)
|
|
46395
|
+
continue;
|
|
46396
|
+
if (text === undefined)
|
|
46397
|
+
out.skipped++;
|
|
46398
|
+
else
|
|
46399
|
+
out.files.push({ path: rel, text });
|
|
46400
|
+
}
|
|
46401
|
+
return out;
|
|
46402
|
+
}
|
|
46403
|
+
function readBounded(root, realRoot, rel) {
|
|
46404
|
+
const abs = path3.join(root, rel);
|
|
46405
|
+
if (leaves(path3.relative(root, abs)))
|
|
46406
|
+
return;
|
|
46407
|
+
try {
|
|
46408
|
+
const st = fs3.lstatSync(abs, { throwIfNoEntry: false });
|
|
46409
|
+
if (!st)
|
|
46410
|
+
return null;
|
|
46411
|
+
if (!st.isFile() || st.size > RULE_LIMITS.bytes)
|
|
46412
|
+
return;
|
|
46413
|
+
if (leaves(path3.relative(realRoot, fs3.realpathSync(abs))))
|
|
46414
|
+
return;
|
|
46415
|
+
const buf = fs3.readFileSync(abs);
|
|
46416
|
+
return buf.length > RULE_LIMITS.bytes || buf.includes(0) ? undefined : buf.toString("utf8");
|
|
46417
|
+
} catch {
|
|
46418
|
+
return;
|
|
46419
|
+
}
|
|
46420
|
+
}
|
|
46421
|
+
function gitList(root) {
|
|
46422
|
+
try {
|
|
46423
|
+
if (cleanGit(root, ["rev-parse", "--is-inside-work-tree"]).toString("utf8").trim() !== "true")
|
|
46424
|
+
return null;
|
|
46425
|
+
} catch {
|
|
46426
|
+
return null;
|
|
46427
|
+
}
|
|
46428
|
+
try {
|
|
46429
|
+
const out = cleanGit(root, ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", ...PATHSPECS], 16 * 1024 * 1024);
|
|
46430
|
+
return { paths: out.toString("utf8").split("\x00").filter(isRuleFile), incomplete: null };
|
|
46431
|
+
} catch {
|
|
46432
|
+
return { paths: [], incomplete: "git could not list the files" };
|
|
46433
|
+
}
|
|
46434
|
+
}
|
|
46435
|
+
function walk(root) {
|
|
46436
|
+
const paths = [];
|
|
46437
|
+
let entries = 0;
|
|
46438
|
+
let unread = 0;
|
|
46439
|
+
let tooDeep = false;
|
|
46440
|
+
let stopped = false;
|
|
46441
|
+
const visit2 = (rel, depth) => {
|
|
46442
|
+
if (stopped)
|
|
46443
|
+
return;
|
|
46444
|
+
let list;
|
|
46445
|
+
try {
|
|
46446
|
+
list = fs3.readdirSync(path3.join(root, rel), { withFileTypes: true });
|
|
46447
|
+
} catch {
|
|
46448
|
+
unread++;
|
|
46449
|
+
return;
|
|
46450
|
+
}
|
|
46451
|
+
for (const d of list) {
|
|
46452
|
+
if (++entries > RULE_LIMITS.entries) {
|
|
46453
|
+
stopped = true;
|
|
46454
|
+
return;
|
|
46455
|
+
}
|
|
46456
|
+
const child = rel ? `${rel}/${d.name}` : d.name;
|
|
46457
|
+
if ((d.isFile() || d.isSymbolicLink()) && isRuleFile(child))
|
|
46458
|
+
paths.push(child);
|
|
46459
|
+
else if (d.isDirectory() && d.name !== "node_modules" && (!d.name.startsWith(".") || d.name === ".claude")) {
|
|
46460
|
+
if (depth >= RULE_LIMITS.depth)
|
|
46461
|
+
tooDeep = true;
|
|
46462
|
+
else
|
|
46463
|
+
visit2(child, depth + 1);
|
|
46464
|
+
}
|
|
46465
|
+
}
|
|
46466
|
+
};
|
|
46467
|
+
visit2("", 1);
|
|
46468
|
+
const why = [
|
|
46469
|
+
...stopped ? [`stopped after ${RULE_LIMITS.entries} directory entries`] : [],
|
|
46470
|
+
...tooDeep ? [`did not look deeper than ${RULE_LIMITS.depth} directories`] : [],
|
|
46471
|
+
...unread ? [`could not read ${unread} director${unread === 1 ? "y" : "ies"}`] : []
|
|
46472
|
+
];
|
|
46473
|
+
return { paths, incomplete: why.length ? why.join("; ") : null };
|
|
46474
|
+
}
|
|
46475
|
+
|
|
46476
|
+
// server/src/overview.ts
|
|
46477
|
+
var OVERVIEW_LIMITS = { records: 50, key: 200, text: 300, paths: 520, heading: 120 };
|
|
46478
|
+
var PROJECT_WIDE = "Project-wide (no code location)";
|
|
46479
|
+
function pathList(paths) {
|
|
46480
|
+
const all = paths.map(inline);
|
|
46481
|
+
const out = [];
|
|
46482
|
+
let used = 0;
|
|
46483
|
+
for (const p of all) {
|
|
46484
|
+
if (used + Buffer.byteLength(p) + 2 > OVERVIEW_LIMITS.paths)
|
|
46485
|
+
break;
|
|
46486
|
+
out.push(p);
|
|
46487
|
+
used += Buffer.byteLength(p) + 2;
|
|
46488
|
+
}
|
|
46489
|
+
if (!out.length)
|
|
46490
|
+
out.push(head(all[0] ?? "", OVERVIEW_LIMITS.paths));
|
|
46491
|
+
return `${out.join(", ")}${all.length > out.length ? ` (+${all.length - out.length} more)` : ""}`;
|
|
46492
|
+
}
|
|
46493
|
+
async function liveOverview(db, projectId, after) {
|
|
46494
|
+
const live = db.selectFrom("unit").where("project_id", "=", projectId).where("lifecycle", "=", "active").where("kind", "in", ["decision", "constraint"]);
|
|
46495
|
+
const [total, rows] = await Promise.all([
|
|
46496
|
+
live.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst(),
|
|
46497
|
+
live.select(["id", "key", "kind", "stance", "text"]).where("id", ">", after ?? 0).orderBy("id").limit(OVERVIEW_LIMITS.records + 1).execute()
|
|
46498
|
+
]);
|
|
46499
|
+
const anchors = rows.length ? await db.selectFrom("unit_anchor").select(["unit_id", "path"]).where("unit_id", "in", rows.map((r) => r.id)).where("role", "=", "applies_to").where("retired_at", "is", null).orderBy("id").execute() : [];
|
|
46500
|
+
const shown = [];
|
|
46501
|
+
for (const r of rows.slice(0, OVERVIEW_LIMITS.records)) {
|
|
46502
|
+
const paths = [...new Set(anchors.filter((a) => a.unit_id === r.id).map((a) => a.path))];
|
|
46503
|
+
const first = paths[0];
|
|
46504
|
+
const dir = first === undefined ? null : path4.posix.dirname(first);
|
|
46505
|
+
const line = `- ${head(inline(r.key), OVERVIEW_LIMITS.key)} (u${r.id}, ${r.kind}${r.stance ? ` ${r.stance}` : ""}): ${head(inline(r.text), OVERVIEW_LIMITS.text)}${paths.length ? ` [${pathList(paths)}]` : ""}`;
|
|
46506
|
+
shown.push({
|
|
46507
|
+
id: r.id,
|
|
46508
|
+
group: dir === null ? PROJECT_WIDE : dir === "." ? "(repository root)" : `${dir}/`,
|
|
46509
|
+
line
|
|
46510
|
+
});
|
|
46511
|
+
}
|
|
46512
|
+
const n = Number(total?.n ?? 0);
|
|
46513
|
+
if (!shown.length)
|
|
46514
|
+
return after === null ? "No active decision or constraint is recorded for this project. status says whether sessions are still untraced." : `No active decision or constraint after id ${after}. ${n} in all.`;
|
|
46515
|
+
const groups = [...new Set(shown.map((s) => s.group))].sort((a, b) => a === PROJECT_WIDE ? 1 : b === PROJECT_WIDE ? -1 : a.localeCompare(b));
|
|
46516
|
+
const last = shown.at(-1)?.id ?? 0;
|
|
46517
|
+
const more = rows.length > shown.length;
|
|
46518
|
+
return [
|
|
46519
|
+
...groups.flatMap((g) => [
|
|
46520
|
+
`## ${head(inline(g), OVERVIEW_LIMITS.heading)}`,
|
|
46521
|
+
...shown.filter((s) => s.group === g).map((s) => s.line),
|
|
46522
|
+
""
|
|
46523
|
+
]),
|
|
46524
|
+
`${shown.length} shown of ${n} active decisions and constraints${after === null ? "" : ` (ids after ${after})`}.`,
|
|
46525
|
+
more ? `More follow: call overview again with after: ${last}. Pages are read at different times: a record that became active in between, with a lower id, is not on a later page.` : "That is the end of the list.",
|
|
46526
|
+
"Read a record by its key or u<id> before relying on it."
|
|
46527
|
+
].join(`
|
|
46528
|
+
`);
|
|
46529
|
+
}
|
|
46530
|
+
var LOOK_LIMITS = { anchors: 2000, lines: 50, line: 2200, bytes: 56 * 1024 };
|
|
46531
|
+
var MARKER = /<!--\s*sphica:\s*((?:trace|harvest|glean):[^\s>]{1,1000})\s*-->/g;
|
|
46532
|
+
async function lookOverview(db, projectId, root) {
|
|
46533
|
+
const notChecked = [];
|
|
46534
|
+
const sections = [];
|
|
46535
|
+
let used = 0;
|
|
46536
|
+
const section = (title, lines, empty) => {
|
|
46537
|
+
const shown = [];
|
|
46538
|
+
for (const line of lines.slice(0, LOOK_LIMITS.lines).map((l) => head(l, LOOK_LIMITS.line))) {
|
|
46539
|
+
if (used + bytes(line) + 1 > LOOK_LIMITS.bytes)
|
|
46540
|
+
break;
|
|
46541
|
+
used += bytes(line) + 1;
|
|
46542
|
+
shown.push(line);
|
|
46543
|
+
}
|
|
46544
|
+
sections.push([
|
|
46545
|
+
`## ${title}`,
|
|
46546
|
+
...shown.length || lines.length ? shown : [empty],
|
|
46547
|
+
...lines.length > shown.length ? [`(${lines.length - shown.length} more not shown: deal with these first, then ask again)`] : []
|
|
46548
|
+
].join(`
|
|
46549
|
+
`));
|
|
46550
|
+
};
|
|
46551
|
+
const live = db.selectFrom("unit_anchor as a").innerJoin("unit as u", "u.id", "a.unit_id").where("u.project_id", "=", projectId).where("u.lifecycle", "=", "active").where("a.retired_at", "is", null);
|
|
46552
|
+
const [anchors, total] = await Promise.all([
|
|
46553
|
+
live.select(["u.key", "u.kind", "a.path", "a.symbol", "a.line_start", "a.role"]).orderBy("a.id").limit(LOOK_LIMITS.anchors).execute(),
|
|
46554
|
+
live.select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()
|
|
46555
|
+
]);
|
|
46556
|
+
const gone = [];
|
|
46557
|
+
const lost = [];
|
|
46558
|
+
if (!root)
|
|
46559
|
+
notChecked.push("code locations: no working tree for this project here");
|
|
46560
|
+
else {
|
|
46561
|
+
let unknown3 = 0;
|
|
46562
|
+
let unscanned = 0;
|
|
46563
|
+
for (const a of anchors) {
|
|
46564
|
+
const where = `- ${inline(a.key)} (${a.kind}): ${inline(a.path)} (${a.role})`;
|
|
46565
|
+
const file2 = fileState(root, a.path);
|
|
46566
|
+
if (file2 === "gone")
|
|
46567
|
+
gone.push(where);
|
|
46568
|
+
else if (file2 === "unknown")
|
|
46569
|
+
unknown3++;
|
|
46570
|
+
else if (a.symbol) {
|
|
46571
|
+
const state = checkAnchor(root, a).state;
|
|
46572
|
+
if (state === "missing")
|
|
46573
|
+
lost.push(`- ${inline(a.key)} (${a.kind}): ${inline(a.symbol)} in ${inline(a.path)} (${a.role})`);
|
|
46574
|
+
else if (state === "unknown")
|
|
46575
|
+
unscanned++;
|
|
46576
|
+
}
|
|
46577
|
+
}
|
|
46578
|
+
if (unknown3)
|
|
46579
|
+
notChecked.push(`${unknown3} code locations that lead outside the repository or cannot be followed`);
|
|
46580
|
+
if (unscanned)
|
|
46581
|
+
notChecked.push(`${unscanned} code locations whose file could not be scanned for the symbol (too large, binary, or unreadable)`);
|
|
46582
|
+
const past = Number(total?.n ?? 0) - anchors.length;
|
|
46583
|
+
if (past > 0)
|
|
46584
|
+
notChecked.push(`${past} code locations past the first ${LOOK_LIMITS.anchors} (by age)`);
|
|
46585
|
+
}
|
|
46586
|
+
section("Files gone", gone, root ? "none" : "not checked");
|
|
46587
|
+
section("Symbol not found (the file is still there)", lost, root ? "none" : "not checked");
|
|
46588
|
+
const [options, deferred] = await Promise.all([
|
|
46589
|
+
db.selectFrom("unit_option as o").innerJoin("unit as u", "u.id", "o.unit_id").where("u.project_id", "=", projectId).where("u.lifecycle", "=", "active").where("o.reconsider_when", "is not", null).select((eb) => [
|
|
46590
|
+
"u.key",
|
|
46591
|
+
"o.text",
|
|
46592
|
+
"o.reconsider_when",
|
|
46593
|
+
eb.exists(eb.selectFrom("unit_evidence as e").innerJoin("source as s", "s.id", "e.source_id").whereRef("e.option_id", "=", "o.id").where("e.role", "=", "reconsiders").where("e.retracted_at", "is", null).where("s.author_kind", "=", "owner").select("e.id")).as("stands")
|
|
46594
|
+
]).orderBy("o.id").execute(),
|
|
46595
|
+
db.selectFrom("unit").where("project_id", "=", projectId).where("lifecycle", "=", "active").where("revisit_when", "is not", null).select(["key", "text", "revisit_when"]).orderBy("id").execute()
|
|
46596
|
+
]);
|
|
46597
|
+
section("Conditions to reconsider (judge whether one has come about; nothing here is decided)", [
|
|
46598
|
+
...options.map((o) => `- ${inline(o.key)}: rejected option ${inline(head(o.text, 120))}, reconsider when: ${inline(head(o.reconsider_when ?? "", 300))}${o.stands ? "" : ` [${UNSUPPORTED}]`}`),
|
|
46599
|
+
...deferred.map((d) => `- ${inline(d.key)}: deferred ${inline(head(d.text, 120))}, revisit when: ${inline(head(d.revisit_when ?? "", 300))}`)
|
|
46600
|
+
], "none");
|
|
46601
|
+
const marked = [];
|
|
46602
|
+
if (!root)
|
|
46603
|
+
notChecked.push("instruction files: no working tree for this project here");
|
|
46604
|
+
else {
|
|
46605
|
+
const scan = ruleFiles(root);
|
|
46606
|
+
const found = [];
|
|
46607
|
+
for (const f of scan.files)
|
|
46608
|
+
for (const [i, text] of f.text.split(/\r?\n/).entries())
|
|
46609
|
+
for (const m of text.matchAll(MARKER))
|
|
46610
|
+
found.push({ file: f.path, line: i + 1, key: m[1] ?? "" });
|
|
46611
|
+
const keys = [...new Set(found.map((f) => f.key))];
|
|
46612
|
+
const units = new Map;
|
|
46613
|
+
for (let i = 0;i < keys.length; i += 500)
|
|
46614
|
+
for (const u of await db.selectFrom("unit").select(["id", "key", "lifecycle"]).where("project_id", "=", projectId).where("key", "in", keys.slice(i, i + 500)).execute())
|
|
46615
|
+
units.set(u.key, u);
|
|
46616
|
+
const chains = new Map;
|
|
46617
|
+
for (const f of found) {
|
|
46618
|
+
const u = units.get(f.key);
|
|
46619
|
+
const where = `- ${inline(f.file)}:${f.line}: ${inline(f.key)}`;
|
|
46620
|
+
if (!u)
|
|
46621
|
+
marked.push(`${where} is not a record of this project`);
|
|
46622
|
+
else if (u.lifecycle === "withdrawn")
|
|
46623
|
+
marked.push(`${where} was withdrawn`);
|
|
46624
|
+
else if (u.lifecycle === "superseded") {
|
|
46625
|
+
let next = chains.get(u.id);
|
|
46626
|
+
if (next === undefined) {
|
|
46627
|
+
next = await successor(db, u.id);
|
|
46628
|
+
chains.set(u.id, next);
|
|
46629
|
+
}
|
|
46630
|
+
marked.push(`${where} was superseded${next ? ` by ${inline(next.key)}${next.lifecycle === "active" ? "" : `, which is ${next.lifecycle} too`}` : ""}`);
|
|
46631
|
+
}
|
|
46632
|
+
}
|
|
46633
|
+
if (scan.skipped)
|
|
46634
|
+
notChecked.push(`${scan.skipped} instruction files not read (over the caps, not regular files, or outside the repository)`);
|
|
46635
|
+
if (scan.incomplete)
|
|
46636
|
+
notChecked.push(`instruction files: the listing ${scan.incomplete}`);
|
|
46637
|
+
}
|
|
46638
|
+
section("Rule markers whose record changed", marked, root ? "none" : "not checked");
|
|
46639
|
+
return [
|
|
46640
|
+
...sections,
|
|
46641
|
+
`## Not checked
|
|
46642
|
+
${notChecked.length ? notChecked.map((n) => `- ${n}`).join(`
|
|
46643
|
+
`) : "nothing: every place above was checked"}`,
|
|
46644
|
+
"Read a record by its key before acting on it. Change a record only through /sphica:trace, with the owner's words."
|
|
46645
|
+
].join(`
|
|
46646
|
+
|
|
46647
|
+
`);
|
|
46648
|
+
}
|
|
46649
|
+
async function successor(db, id) {
|
|
46650
|
+
let end = null;
|
|
46651
|
+
let at = id;
|
|
46652
|
+
for (;; ) {
|
|
46653
|
+
const next = await db.selectFrom("unit_link as l").innerJoin("unit as u", "u.id", "l.from_unit").where("l.to_unit", "=", at).where("l.kind", "=", "supersedes").select(["u.id", "u.key", "u.lifecycle"]).executeTakeFirst();
|
|
46654
|
+
if (!next)
|
|
46655
|
+
break;
|
|
46656
|
+
end = { key: next.key, lifecycle: next.lifecycle };
|
|
46657
|
+
if (next.lifecycle !== "superseded")
|
|
46658
|
+
break;
|
|
46659
|
+
at = next.id;
|
|
46660
|
+
}
|
|
46661
|
+
return end;
|
|
46662
|
+
}
|
|
46663
|
+
|
|
46664
|
+
// server/src/plugin.ts
|
|
46665
|
+
import fs4 from "node:fs";
|
|
46666
|
+
import path5 from "node:path";
|
|
46667
|
+
import { fileURLToPath } from "node:url";
|
|
46668
|
+
var MANIFEST = path5.join(".claude-plugin", "plugin.json");
|
|
46669
|
+
function versionAt(root) {
|
|
46670
|
+
try {
|
|
46671
|
+
const m = JSON.parse(fs4.readFileSync(path5.join(root, MANIFEST), "utf8"));
|
|
46672
|
+
return m.name === "sphica" && typeof m.version === "string" ? m.version : null;
|
|
46673
|
+
} catch {
|
|
46674
|
+
return null;
|
|
46675
|
+
}
|
|
46676
|
+
}
|
|
46677
|
+
var here = path5.dirname(fileURLToPath(import.meta.url));
|
|
46678
|
+
var ROOT = [path5.join(here, ".."), path5.join(here, "..", "..", "plugin")].find((r) => versionAt(r) !== null) ?? path5.join(here, "..");
|
|
46679
|
+
var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
|
|
46680
|
+
|
|
46681
|
+
// server/src/project.ts
|
|
46682
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
46683
|
+
import fs5 from "node:fs";
|
|
46684
|
+
import path6 from "node:path";
|
|
46685
|
+
var localFile = () => path6.join(sphicaHome(), "projects.json");
|
|
46686
|
+
var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
46687
|
+
function normalizeRemote(url2) {
|
|
46688
|
+
const raw = String(url2 ?? "").trim();
|
|
46689
|
+
if (!raw)
|
|
46690
|
+
return null;
|
|
46691
|
+
const scp = raw.match(/^(?:[^@/]+@)?([^:/]+):(?!\/)(.+?)(?:\.git)?$/);
|
|
46692
|
+
if (scp)
|
|
46693
|
+
return `${scp[1]}/${scp[2]}`;
|
|
46694
|
+
try {
|
|
46695
|
+
const u = new URL(raw);
|
|
46696
|
+
if (!u.hostname)
|
|
46697
|
+
return null;
|
|
46698
|
+
const p = u.pathname.replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
|
|
46699
|
+
return p ? `${u.hostname}/${p}` : u.hostname;
|
|
46700
|
+
} catch {
|
|
46701
|
+
return null;
|
|
46702
|
+
}
|
|
46703
|
+
}
|
|
46704
|
+
var git = (dir, ...args) => {
|
|
46705
|
+
try {
|
|
46706
|
+
return execFileSync2("git", ["-C", dir, ...args], {
|
|
46707
|
+
encoding: "utf8",
|
|
46708
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
46709
|
+
timeout: 5000
|
|
46710
|
+
}).trim();
|
|
46711
|
+
} catch {
|
|
46712
|
+
return null;
|
|
46713
|
+
}
|
|
46714
|
+
};
|
|
46715
|
+
function localMap() {
|
|
46716
|
+
let raw;
|
|
46717
|
+
try {
|
|
46718
|
+
raw = fs5.readFileSync(localFile(), "utf8");
|
|
46719
|
+
} catch (e) {
|
|
46720
|
+
if (e.code === "ENOENT")
|
|
46721
|
+
return {};
|
|
46722
|
+
throw e;
|
|
46723
|
+
}
|
|
46724
|
+
let m;
|
|
46725
|
+
try {
|
|
46726
|
+
m = JSON.parse(raw);
|
|
46727
|
+
} catch {
|
|
46728
|
+
m = null;
|
|
46729
|
+
}
|
|
46730
|
+
if (!m || typeof m !== "object" || Array.isArray(m) || Object.entries(m).some(([root, name]) => !path6.isAbsolute(root) || typeof name !== "string" || !LOCAL_KEY.test(name)))
|
|
46731
|
+
throw new Error(`${localFile()} is not a valid JSON project table. Fix or delete it, then name the project again.`);
|
|
46732
|
+
return m;
|
|
46733
|
+
}
|
|
46734
|
+
function identify(dir) {
|
|
46735
|
+
const given = path6.resolve(dir);
|
|
46736
|
+
const top = git(given, "rev-parse", "--show-toplevel");
|
|
46737
|
+
const root = top || given;
|
|
46738
|
+
const remote = top ? normalizeRemote(git(root, "remote", "get-url", "origin")) : null;
|
|
46739
|
+
if (remote)
|
|
46740
|
+
return { key: `git:${remote}`, root, name: remote.split("/").slice(1).join("/") || remote };
|
|
46741
|
+
const map3 = localMap();
|
|
46742
|
+
for (let d = root;; d = path6.dirname(d)) {
|
|
46743
|
+
const local = map3[d];
|
|
46744
|
+
if (local && LOCAL_KEY.test(local))
|
|
46745
|
+
return { key: `local:${local}`, root: d, name: local };
|
|
46746
|
+
if (top || path6.dirname(d) === d)
|
|
46747
|
+
return null;
|
|
46748
|
+
}
|
|
46749
|
+
}
|
|
46750
|
+
async function projectId(db, key) {
|
|
46751
|
+
const r = await db.selectFrom("project").select("id").where("key", "=", key).executeTakeFirst();
|
|
46752
|
+
return r?.id ?? null;
|
|
46753
|
+
}
|
|
46754
|
+
|
|
46402
46755
|
// server/src/review.ts
|
|
46403
46756
|
var ESCAPES = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13, '"': 34, "\\": 92 };
|
|
46404
46757
|
function gitPath(token, prefix) {
|
|
@@ -46428,11 +46781,11 @@ function gitPath(token, prefix) {
|
|
|
46428
46781
|
}
|
|
46429
46782
|
function parseDiff(text) {
|
|
46430
46783
|
const files = [];
|
|
46431
|
-
const at = (
|
|
46432
|
-
const found = files.find((f2) => f2.path ===
|
|
46784
|
+
const at = (path7) => {
|
|
46785
|
+
const found = files.find((f2) => f2.path === path7);
|
|
46433
46786
|
if (found)
|
|
46434
46787
|
return found;
|
|
46435
|
-
const f = { path:
|
|
46788
|
+
const f = { path: path7, added: [], lines: [] };
|
|
46436
46789
|
files.push(f);
|
|
46437
46790
|
return f;
|
|
46438
46791
|
};
|
|
@@ -46579,19 +46932,37 @@ async function checkFindings(db, projectId2, files, raw) {
|
|
|
46579
46932
|
return problems;
|
|
46580
46933
|
}
|
|
46581
46934
|
|
|
46935
|
+
// server/src/trace.ts
|
|
46936
|
+
var PENDING_DAYS = 30;
|
|
46937
|
+
var pendingCutoff = (now) => new Date(now.getTime() - PENDING_DAYS * 86400000).toISOString();
|
|
46938
|
+
var untracedOwner = (eb) => eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id").where("m.author_kind", "=", "owner").where(({ not, exists, selectFrom }) => not(exists(selectFrom("source_processing as p").whereRef("p.source_id", "=", "m.id").select(sql`1`.as("x")))));
|
|
46939
|
+
var untracedSessions = (db, projectId2) => db.selectFrom("session as s").where("s.project_id", "=", projectId2).where((eb) => eb.exists(untracedOwner(eb).select(sql`1`.as("x")))).select((eb) => [
|
|
46940
|
+
"s.id",
|
|
46941
|
+
"s.host",
|
|
46942
|
+
"s.started_at",
|
|
46943
|
+
"s.branch",
|
|
46944
|
+
eb.selectFrom("source as o").whereRef("o.session_id", "=", "s.id").where("o.author_kind", "=", "owner").select((o) => o.fn.max("o.created_at").as("last")).as("last"),
|
|
46945
|
+
untracedOwner(eb).select((m) => m.fn.countAll().as("waiting")).as("waiting"),
|
|
46946
|
+
untracedOwner(eb).select((m) => m.fn.min("m.id").as("first")).as("first")
|
|
46947
|
+
]);
|
|
46948
|
+
|
|
46582
46949
|
// server/src/status.ts
|
|
46583
|
-
async function pendingCount(db, projectId2) {
|
|
46584
|
-
const
|
|
46585
|
-
|
|
46586
|
-
}
|
|
46587
|
-
|
|
46950
|
+
async function pendingCount(db, projectId2, now = new Date) {
|
|
46951
|
+
const cutoff = pendingCutoff(now);
|
|
46952
|
+
const r = await db.selectFrom(untracedSessions(db, projectId2).as("w")).select([
|
|
46953
|
+
sql`count(case when w.last >= ${cutoff} then 1 end)`.as("recent"),
|
|
46954
|
+
sql`count(case when w.last < ${cutoff} then 1 end)`.as("older")
|
|
46955
|
+
]).executeTakeFirst();
|
|
46956
|
+
return { recent: Number(r?.recent ?? 0), older: Number(r?.older ?? 0) };
|
|
46957
|
+
}
|
|
46958
|
+
async function coverage(db, projectId2, now) {
|
|
46588
46959
|
const count = async (q) => Number((await q)?.n ?? 0);
|
|
46589
46960
|
const units = (where) => count(where(unitBase()).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst());
|
|
46590
46961
|
const unitBase = () => db.selectFrom("unit").where("project_id", "=", projectId2);
|
|
46591
46962
|
const processed = (outcome) => db.selectFrom("source_processing as p").innerJoin("source as m", "m.id", "p.source_id").where("m.project_id", "=", projectId2).where("p.outcome", "=", outcome).where("m.session_id", "is not", null).select("m.session_id");
|
|
46592
|
-
const [sessions,
|
|
46963
|
+
const [sessions, pending, emptySessions, sources, active, candidates, quarantined, work] = await Promise.all([
|
|
46593
46964
|
count(db.selectFrom("session").where("project_id", "=", projectId2).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
|
|
46594
|
-
pendingCount(db, projectId2),
|
|
46965
|
+
pendingCount(db, projectId2, now),
|
|
46595
46966
|
count(db.selectFrom("session").where("id", "in", processed("no_unit")).where("id", "not in", processed("units")).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
|
|
46596
46967
|
count(db.selectFrom("source").where("project_id", "=", projectId2).select((eb) => eb.fn.countAll().as("n")).executeTakeFirst()),
|
|
46597
46968
|
units((q) => q.where("lifecycle", "=", "active")),
|
|
@@ -46599,15 +46970,21 @@ async function coverage(db, projectId2) {
|
|
|
46599
46970
|
units((q) => q.where("extraction", "=", "quarantined")),
|
|
46600
46971
|
db.selectFrom("work").where("project_id", "=", projectId2).where("status", "in", ["active", "blocked", "paused"]).orderBy("updated_at", "desc").select(["title", "current", "status"]).limit(5).execute()
|
|
46601
46972
|
]);
|
|
46602
|
-
return { sessions,
|
|
46973
|
+
return { sessions, pending, emptySessions, sources, active, candidates, quarantined, work };
|
|
46603
46974
|
}
|
|
46604
|
-
async function status(db, projectId2, name) {
|
|
46605
|
-
const c = await coverage(db, projectId2);
|
|
46975
|
+
async function status(db, projectId2, name, now = new Date) {
|
|
46976
|
+
const c = await coverage(db, projectId2, now);
|
|
46606
46977
|
const lines = [
|
|
46607
46978
|
`${name}`,
|
|
46608
46979
|
`Captured: ${plural2(c.sessions, "session")}, ${plural2(c.sources, "source")}.`,
|
|
46609
46980
|
`Extracted: ${plural2(c.active, "active record")}, ${plural2(c.candidates, "candidate")} not active yet (waiting for adoption or evidence), ${plural2(c.quarantined, "quarantined record")}.`,
|
|
46610
|
-
c.
|
|
46981
|
+
...c.pending.recent ? [
|
|
46982
|
+
`${plural2(c.pending.recent, "session")} not traced yet: their decisions exist only as captured text (run /sphica:trace pending).`
|
|
46983
|
+
] : [],
|
|
46984
|
+
...c.pending.older ? [
|
|
46985
|
+
`${c.pending.older} older ${c.pending.older === 1 ? "session" : "sessions"} (last owner message over ${PENDING_DAYS} days ago) not traced; /sphica:trace pending lists them.`
|
|
46986
|
+
] : [],
|
|
46987
|
+
...c.pending.recent || c.pending.older ? [] : ["Every captured session has been traced."],
|
|
46611
46988
|
...c.emptySessions ? [`${plural2(c.emptySessions, "session")} traced with nothing to record.`] : [],
|
|
46612
46989
|
c.work.length ? `Work in progress:
|
|
46613
46990
|
${framed(c.work.map((w) => `- ${head(inline(w.title), 200)} (${w.status}): ${head(inline(w.current), 500)}`).join(`
|
|
@@ -46775,6 +47152,25 @@ server.registerTool("read", {
|
|
|
46775
47152
|
});
|
|
46776
47153
|
var DIFF = exports_external.string().min(1).max(2000000).describe("The change under review as a unified diff (git diff output)");
|
|
46777
47154
|
var notChecked = (e) => text(`Decision lane: not checked. Sphica unavailable: ${head(reason(e), 300)}. Report the decision check as not run, not as passed.`, true);
|
|
47155
|
+
server.registerTool("overview", {
|
|
47156
|
+
title: "Live decisions, and records that need a look",
|
|
47157
|
+
description: "On request, not before every change. view live lists every active decision and constraint of the project, grouped by the directory it " + "applies to, a page at a time (pass after from the previous page). view look lists live records whose code file is gone or whose symbol " + "is not found, written conditions for reconsidering an option (for you to judge, never applied), and lines in CLAUDE.md, AGENTS.md, or " + ".claude/rules marked <!-- sphica: key --> whose record was replaced or withdrawn. Read a record by its key before relying on it.",
|
|
47158
|
+
inputSchema: {
|
|
47159
|
+
view: exports_external.enum(["live", "look"]).describe("live: every active decision and constraint; look: records that need a look"),
|
|
47160
|
+
after: exports_external.number().int().min(0).optional().describe("With live: the id the previous page said to continue after"),
|
|
47161
|
+
cwd: CWD
|
|
47162
|
+
},
|
|
47163
|
+
annotations: READ_ONLY
|
|
47164
|
+
}, async (a) => {
|
|
47165
|
+
try {
|
|
47166
|
+
const p = await projectOf(a.cwd);
|
|
47167
|
+
if (typeof p === "string")
|
|
47168
|
+
return text(p);
|
|
47169
|
+
return text(framed(a.view === "live" ? await liveOverview(db, p.id, a.after ?? null) : await lookOverview(db, p.id, p.root)));
|
|
47170
|
+
} catch (e) {
|
|
47171
|
+
return text(`Sphica unavailable: ${head(reason(e), 300)}`, true);
|
|
47172
|
+
}
|
|
47173
|
+
});
|
|
46778
47174
|
server.registerTool("review_select", {
|
|
46779
47175
|
title: "Past decisions a change touches",
|
|
46780
47176
|
description: "For a code review: the active decisions, constraints, and implementation records this diff touches (records anchored to a changed path, " + "and records with no code location that forbid or defer an option an added line names). Judge each against the diff, then check the verdicts with review_check.",
|