sphica 0.6.28 → 0.6.30

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/dist/mcp.js CHANGED
@@ -45649,6 +45649,29 @@ function parseObject(obj, jsonPath, options) {
45649
45649
  return target;
45650
45650
  }
45651
45651
 
45652
+ // server/src/authority.ts
45653
+ var AUTHORITY = {
45654
+ owner: "the owner's decision",
45655
+ agent: "decided by an AI",
45656
+ none: "adopted by no one"
45657
+ };
45658
+ var AI_DECIDED = "A record marked decided by an AI was decided by an AI in an earlier session, not by the owner (unmarked delivered decisions are the owner's): with a concrete reason you may depart from it, saying in your reply which record and why. It never relaxes the owner's rules, public contracts, or approval gates.";
45659
+ async function authorityOf(db, ids, asOf) {
45660
+ const out = new Map(ids.map((id) => [id, "none"]));
45661
+ if (!ids.length)
45662
+ return out;
45663
+ const at = asOf ?? "9999";
45664
+ const rows = await db.selectFrom("unit_adoption").select(["unit_id", "route"]).where("unit_id", "in", ids).where("added_at", "<=", at).where((eb) => eb.or([eb("retracted_at", "is", null), eb("retracted_at", ">", at)])).execute();
45665
+ for (const r of rows) {
45666
+ if (r.route === "agent") {
45667
+ if (out.get(r.unit_id) === "none")
45668
+ out.set(r.unit_id, "agent");
45669
+ } else
45670
+ out.set(r.unit_id, "owner");
45671
+ }
45672
+ return out;
45673
+ }
45674
+
45652
45675
  // server/src/kysely-node-sqlite.ts
45653
45676
  var plain2 = (row) => {
45654
45677
  const out = {};
@@ -45711,7 +45734,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
45711
45734
 
45712
45735
  // server/src/sqlite.ts
45713
45736
  var SCHEMA_GENERATION = 2;
45714
- var SCHEMA_REVISION = 9;
45737
+ var SCHEMA_REVISION = 10;
45715
45738
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
45716
45739
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
45717
45740
  function requireRuntime() {
@@ -45831,7 +45854,8 @@ var EVIDENCE_ROLES = [
45831
45854
  "rejects",
45832
45855
  "explains",
45833
45856
  "implements",
45834
- "reconsiders"
45857
+ "reconsiders",
45858
+ "decides"
45835
45859
  ];
45836
45860
  var WORK_STATUSES = ["active", "blocked", "paused", "done", "abandoned"];
45837
45861
  var FIELD_TYPES = ["text", "enum", "integer", "date"];
@@ -45973,6 +45997,126 @@ function renamesSince(root, commit) {
45973
45997
  }
45974
45998
  }
45975
45999
 
46000
+ // server/src/rule-files.ts
46001
+ import fs4 from "node:fs";
46002
+ import path4 from "node:path";
46003
+ var RULE_LIMITS = { files: 200, bytes: 256 * 1024, depth: 8, entries: 5000 };
46004
+ var RULE_NAMES = new Set(["CLAUDE.md", "AGENTS.md", "AGENTS.override.md"]);
46005
+ var PATHSPECS = [...RULE_NAMES, ".claude/rules/**/*.md"].map((p) => `:(glob)**/${p}`);
46006
+ function isRuleFile(rel) {
46007
+ const parts = rel.split("/");
46008
+ const name = parts.at(-1) ?? "";
46009
+ if (RULE_NAMES.has(name))
46010
+ return true;
46011
+ const i = parts.findIndex((p, k) => p === ".claude" && parts[k + 1] === "rules");
46012
+ return i >= 0 && parts.length > i + 2 && name.endsWith(".md");
46013
+ }
46014
+ function ruleFiles(root) {
46015
+ const listed = gitList(root) ?? walk(root);
46016
+ const out = { files: [], skipped: 0, incomplete: listed.incomplete };
46017
+ const realRoot = fs4.realpathSync(root);
46018
+ for (const [i, rel] of [...new Set(listed.paths)].sort().entries()) {
46019
+ if (i >= RULE_LIMITS.files) {
46020
+ out.skipped++;
46021
+ continue;
46022
+ }
46023
+ const text = readBounded(root, realRoot, rel);
46024
+ if (text === null)
46025
+ continue;
46026
+ if (text === undefined)
46027
+ out.skipped++;
46028
+ else
46029
+ out.files.push({ path: rel, text });
46030
+ }
46031
+ return out;
46032
+ }
46033
+ function readBounded(root, realRoot, rel) {
46034
+ const abs = path4.join(root, rel);
46035
+ if (leaves(path4.relative(root, abs)))
46036
+ return;
46037
+ try {
46038
+ const st = fs4.lstatSync(abs, { throwIfNoEntry: false });
46039
+ if (!st)
46040
+ return null;
46041
+ if (!st.isFile() || st.size > RULE_LIMITS.bytes)
46042
+ return;
46043
+ if (leaves(path4.relative(realRoot, fs4.realpathSync(abs))))
46044
+ return;
46045
+ const buf = fs4.readFileSync(abs);
46046
+ return buf.length > RULE_LIMITS.bytes || buf.includes(0) ? undefined : buf.toString("utf8");
46047
+ } catch {
46048
+ return;
46049
+ }
46050
+ }
46051
+ function gitList(root) {
46052
+ try {
46053
+ if (cleanGit(root, ["rev-parse", "--is-inside-work-tree"]).toString("utf8").trim() !== "true")
46054
+ return null;
46055
+ } catch {
46056
+ return null;
46057
+ }
46058
+ try {
46059
+ const out = cleanGit(root, ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", ...PATHSPECS], 16 * 1024 * 1024);
46060
+ return { paths: out.toString("utf8").split("\x00").filter(isRuleFile), incomplete: null };
46061
+ } catch {
46062
+ return { paths: [], incomplete: "git could not list the files" };
46063
+ }
46064
+ }
46065
+ function walk(root) {
46066
+ const paths = [];
46067
+ let entries = 0;
46068
+ let unread = 0;
46069
+ let tooDeep = false;
46070
+ let stopped = false;
46071
+ const visit2 = (rel, depth) => {
46072
+ if (stopped)
46073
+ return;
46074
+ let list;
46075
+ try {
46076
+ list = fs4.readdirSync(path4.join(root, rel), { withFileTypes: true });
46077
+ } catch {
46078
+ unread++;
46079
+ return;
46080
+ }
46081
+ for (const d of list) {
46082
+ if (++entries > RULE_LIMITS.entries) {
46083
+ stopped = true;
46084
+ return;
46085
+ }
46086
+ const child = rel ? `${rel}/${d.name}` : d.name;
46087
+ if ((d.isFile() || d.isSymbolicLink()) && isRuleFile(child))
46088
+ paths.push(child);
46089
+ else if (d.isDirectory() && d.name !== "node_modules" && (!d.name.startsWith(".") || d.name === ".claude")) {
46090
+ if (depth >= RULE_LIMITS.depth)
46091
+ tooDeep = true;
46092
+ else
46093
+ visit2(child, depth + 1);
46094
+ }
46095
+ }
46096
+ };
46097
+ visit2("", 1);
46098
+ const why = [
46099
+ ...stopped ? [`stopped after ${RULE_LIMITS.entries} directory entries`] : [],
46100
+ ...tooDeep ? [`did not look deeper than ${RULE_LIMITS.depth} directories`] : [],
46101
+ ...unread ? [`could not read ${unread} director${unread === 1 ? "y" : "ies"}`] : []
46102
+ ];
46103
+ return { paths, incomplete: why.length ? why.join("; ") : null };
46104
+ }
46105
+ var INSTRUCTION_DIRS = new Set([
46106
+ ".claude",
46107
+ ".agents",
46108
+ ".codex",
46109
+ ".cursor",
46110
+ "skills",
46111
+ ".claude-plugin",
46112
+ ".codex-plugin"
46113
+ ]);
46114
+ var instructionFile = (relative) => {
46115
+ const parts = relative.toLowerCase().split(/[\\/]/);
46116
+ const names = [...RULE_NAMES, "SKILL.md"].map((n) => n.toLowerCase());
46117
+ return parts.some((p) => INSTRUCTION_DIRS.has(p)) || names.includes(parts.at(-1) ?? "") || parts.slice(-2).join("/") === ".github/copilot-instructions.md";
46118
+ };
46119
+
45976
46120
  // server/src/record.ts
45977
46121
  var KEY = /^[a-z0-9][a-z0-9._/-]{0,63}$/;
45978
46122
  var FIELD_NAME = /^[a-z][a-z0-9_]{0,39}$/;
@@ -46062,8 +46206,8 @@ var strong = (matched, of) => matched * 2 > of;
46062
46206
  var PATH_REFUSED = "path must be relative to the repository root, such as src/x.ts: absolute paths, .., backslashes, and empty parts are refused";
46063
46207
  async function searchUnits(db, projectId, q) {
46064
46208
  const wanted = queryTerms(q.question);
46065
- const path4 = q.path === undefined ? undefined : repoPath(q.path);
46066
- if (path4 === null)
46209
+ const path5 = q.path === undefined ? undefined : repoPath(q.path);
46210
+ if (path5 === null)
46067
46211
  return { hits: [], weaker: 0, terms: wanted, stopped: false, read: 0, refused: PATH_REFUSED };
46068
46212
  const match = ftsQuery(q.question);
46069
46213
  if (!match)
@@ -46073,8 +46217,8 @@ async function searchUnits(db, projectId, q) {
46073
46217
  query = query.where("u.kind", "in", q.kinds);
46074
46218
  if (q.lifecycles?.length)
46075
46219
  query = query.where("u.lifecycle", "in", q.lifecycles);
46076
- if (path4)
46077
- query = query.where(({ exists, selectFrom }) => exists(selectFrom("unit_anchor as a").select("a.id").whereRef("a.unit_id", "=", "u.id").where("a.path", "=", path4).where("a.retired_at", "is", null)));
46220
+ if (path5)
46221
+ query = query.where(({ exists, selectFrom }) => exists(selectFrom("unit_anchor as a").select("a.id").whereRef("a.unit_id", "=", "u.id").where("a.path", "=", path5).where("a.retired_at", "is", null)));
46078
46222
  const ranked = await query.select(["u.id", "f.rank"]).orderBy("f.rank").orderBy("u.id").limit(UNIT_SCAN_MAX + 1).execute();
46079
46223
  const stopped = ranked.length > UNIT_SCAN_MAX;
46080
46224
  let weaker = 0;
@@ -46115,8 +46259,10 @@ async function searchUnits(db, projectId, q) {
46115
46259
  });
46116
46260
  }
46117
46261
  hits.sort((a, b) => (LIFE_ORDER[a.lifecycle] ?? 9) - (LIFE_ORDER[b.lifecycle] ?? 9) || b.matched.length - a.matched.length || a.rank - b.rank);
46262
+ const top = hits.slice(0, q.limit);
46263
+ const whose = await authorityOf(db, top.filter((h) => ["decision", "constraint"].includes(h.kind)).map((h) => h.id));
46118
46264
  return {
46119
- hits: hits.slice(0, q.limit).map(({ rank: _rank, ...h }) => h),
46265
+ hits: top.map(({ rank: _rank, ...h }) => ({ ...h, authority: whose.get(h.id) })),
46120
46266
  weaker,
46121
46267
  terms: wanted,
46122
46268
  stopped,
@@ -46125,31 +46271,16 @@ async function searchUnits(db, projectId, q) {
46125
46271
  }
46126
46272
  async function liveSuccessors(db, id) {
46127
46273
  const seen = new Set([id]);
46128
- const found = [];
46129
- const replaced = new Set;
46130
- for (let frontier = [id];frontier.length; ) {
46131
- const next = await db.selectFrom("unit_link as l").innerJoin("unit as n", "n.id", "l.from_unit").where("l.to_unit", "in", frontier).where("l.kind", "=", "supersedes").where("n.extraction", "=", "supported").where("n.lifecycle", "<>", "candidate").select([
46132
- "l.to_unit",
46133
- "n.id",
46134
- "n.key",
46135
- "n.kind",
46136
- "n.stance",
46137
- "n.lifecycle",
46138
- "n.text",
46139
- "n.why",
46140
- "n.revisit_when"
46141
- ]).execute();
46142
- frontier = [];
46143
- for (const { to_unit, ...n } of next) {
46144
- replaced.add(to_unit);
46145
- if (seen.has(n.id))
46146
- continue;
46147
- seen.add(n.id);
46148
- found.push(n);
46149
- frontier.push(n.id);
46150
- }
46274
+ let end;
46275
+ for (let at = id;; ) {
46276
+ const next = await db.selectFrom("unit_replacement as h").innerJoin("unit as n", "n.id", "h.from_unit").where("h.to_unit", "=", at).where("h.ended_at", "is", null).select(["n.id", "n.key", "n.kind", "n.stance", "n.lifecycle", "n.text", "n.why", "n.revisit_when"]).executeTakeFirst();
46277
+ if (!next || seen.has(next.id))
46278
+ break;
46279
+ seen.add(next.id);
46280
+ end = next;
46281
+ at = next.id;
46151
46282
  }
46152
- return found.filter((n) => !replaced.has(n.id));
46283
+ return end ? [end] : [];
46153
46284
  }
46154
46285
  async function judgeUnits(db, rows, wanted, hits) {
46155
46286
  const ids = rows.map((r) => r.id);
@@ -46260,6 +46391,21 @@ async function searchSources(db, projectId, question, limit, owner) {
46260
46391
  stopped = true;
46261
46392
  return { hits, weaker, terms: wanted, stopped, read };
46262
46393
  }
46394
+ var hitText = (h) => [
46395
+ `## ${h.key} (u${h.id}): ${h.kind}${h.stance ? ` ${h.stance}` : ""}, ${h.lifecycle}${h.authority ? `, ${AUTHORITY[h.authority]}` : ""}`,
46396
+ head(h.text, 600),
46397
+ ...h.why ? [`Why: ${head(h.why, 400)}`] : [],
46398
+ ...h.revisit_when ? [`Revisit when: ${head(h.revisit_when, 200)}`] : [],
46399
+ ...h.options.length ? [
46400
+ `Options: ${h.options.map((o) => `${o.text} (${o.outcome}${o.why ? `: ${head(o.why, 160)}` : ""})`).join(" / ")}`
46401
+ ] : [],
46402
+ ...h.anchors.length ? [`Code: ${h.anchors.map((a) => `${a.path}${a.symbol ? ` ${a.symbol}` : ""} (${a.role})`).join(", ")}`] : [],
46403
+ h.successorOf ? `Replaces ${h.successorOf}, which matched` : `Matched: ${h.matched.join(", ")}${h.aliasOnly ? " (search aliases only)" : ""}`
46404
+ ].join(`
46405
+ `);
46406
+ var hitsText = (hits) => [...hits.map(hitText), ...hits.some((h) => h.authority === "agent") ? [AI_DECIDED] : []].join(`
46407
+
46408
+ `);
46263
46409
 
46264
46410
  // server/src/asked.ts
46265
46411
  var DECIDED_KINDS = new Set(["decision", "constraint"]);
@@ -46448,7 +46594,15 @@ async function describe3(db, u, root, asOf, renames) {
46448
46594
  "s.text"
46449
46595
  ]).orderBy("a.id").execute(),
46450
46596
  db.selectFrom("unit_anchor").select(["path", "symbol", "role", "commit_sha", "line_start", "retired_at"]).where("unit_id", "=", u.id).where("added_at", "<=", asOf ?? "9999").orderBy("id").execute(),
46451
- db.selectFrom("unit_link as l").innerJoin("unit as a", "a.id", "l.from_unit").innerJoin("unit as b", "b.id", "l.to_unit").where((eb) => eb.or([eb("l.from_unit", "=", u.id), eb("l.to_unit", "=", u.id)])).where("l.added_at", "<=", asOf ?? "9999").select(["l.kind", "l.resolved_at", "a.id as from_id", "a.key as from_key", "b.key as to_key"]).execute(),
46597
+ db.selectFrom("unit_link as l").innerJoin("unit as a", "a.id", "l.from_unit").innerJoin("unit as b", "b.id", "l.to_unit").where((eb) => eb.or([eb("l.from_unit", "=", u.id), eb("l.to_unit", "=", u.id)])).where("l.added_at", "<=", asOf ?? "9999").select((eb) => [
46598
+ "l.kind",
46599
+ "l.resolved_at",
46600
+ "a.id as from_id",
46601
+ "b.id as to_id",
46602
+ "a.key as from_key",
46603
+ "b.key as to_key",
46604
+ eb.selectFrom("unit_state as t").whereRef("t.unit_id", "=", "a.id").where("t.at", "<=", asOf ?? "9999").select("t.to_state").orderBy("t.id", "desc").limit(1).as("from_lifecycle")
46605
+ ]).execute(),
46452
46606
  db.selectFrom("unit_state").select(["from_state", "to_state", "at", "reason"]).where("unit_id", "=", u.id).orderBy("id").execute(),
46453
46607
  db.selectFrom("unit_field as f").innerJoin("field_def as d", "d.id", "f.field_def_id").innerJoin("source as s", "s.id", "f.source_id").where("f.unit_id", "=", u.id).where("f.added_at", "<=", asOf ?? "9999").select([
46454
46608
  "d.name",
@@ -46482,10 +46636,13 @@ async function describe3(db, u, root, asOf, renames) {
46482
46636
  for (const l of links)
46483
46637
  l.resolved_at = later(l.resolved_at);
46484
46638
  }
46639
+ const whose = ["decision", "constraint"].includes(u.kind) ? (await authorityOf(db, [u.id], asOf)).get(u.id) : undefined;
46485
46640
  const out = [
46486
- `${u.key} (u${u.id}, revision ${u.revision}): ${u.kind}${u.stance ? ` ${u.stance}` : ""}, ${lifecycle}${u.extraction === "quarantined" ? `, quarantined: ${u.extraction_reason}` : ""}${u.unsourced ? ", unsourced: no source was given, so it is never used as fact" : ""}`,
46641
+ `${u.key} (u${u.id}, revision ${u.revision}): ${u.kind}${u.stance ? ` ${u.stance}` : ""}, ${lifecycle}${whose ? `, ${AUTHORITY[whose]}` : ""}${u.extraction === "quarantined" ? `, quarantined: ${u.extraction_reason}` : ""}${u.unsourced ? ", unsourced: no source was given, so it is never used as fact" : ""}`,
46487
46642
  u.text
46488
46643
  ];
46644
+ if (whose === "agent")
46645
+ out.push(AI_DECIDED);
46489
46646
  if (u.why)
46490
46647
  out.push(`Why: ${u.why}`);
46491
46648
  if (u.scope_note)
@@ -46528,11 +46685,14 @@ async function describe3(db, u, root, asOf, renames) {
46528
46685
  out.push(` - ${where} (${a.role}${a.commit_sha ? `, commit ${a.commit_sha.slice(0, 12)}` : ""}): ${c.state}${c.line ? ` at line ${c.line}` : ""}${c.state === "missing" ? ` — needs review: the code it points at is gone${movedTo(root, a, renames)}` : ""}`);
46529
46686
  }
46530
46687
  }
46531
- for (const l of links) {
46532
- if (l.kind === "supersedes")
46533
- out.push(l.from_id === u.id ? `Supersedes ${l.to_key}` : `Superseded by ${l.from_key}`);
46534
- else
46535
- out.push(`Conflicts with ${l.from_id === u.id ? l.to_key : l.from_key}${l.resolved_at ? " (resolved)" : " (unresolved)"}`);
46688
+ out.push(...await replacements(db, u.id, links, asOf));
46689
+ const conflicting = links.filter((l) => l.kind === "conflicts");
46690
+ const sides = await authorityOf(db, conflicting.map((l) => l.from_id === u.id ? l.to_id : l.from_id), asOf);
46691
+ const mine = (await authorityOf(db, [u.id], asOf)).get(u.id);
46692
+ for (const l of conflicting) {
46693
+ const other = l.from_id === u.id ? l.to_id : l.from_id;
46694
+ const held = mine !== "owner" || sides.get(other) === "owner";
46695
+ out.push(`Conflicts with ${l.from_id === u.id ? l.to_key : l.from_key}${l.resolved_at ? " (resolved)" : held ? " (unresolved: held back from automatic delivery until resolved, not withdrawn)" : " (unresolved: still delivered, since only the owner's words hold the owner's decision back)"}`);
46536
46696
  }
46537
46697
  const aliases = await db.selectFrom("unit_alias").select("terms").where("unit_id", "=", u.id).where("content_hash", "=", u.content_hash).where("added_at", "<=", asOf ?? "9999").orderBy("id", "desc").executeTakeFirst();
46538
46698
  const terms2 = aliases ? JSON.parse(aliases.terms) : [];
@@ -46542,6 +46702,46 @@ async function describe3(db, u, root, asOf, renames) {
46542
46702
  return out.join(`
46543
46703
  `);
46544
46704
  }
46705
+ async function replacements(db, id, links, asOf) {
46706
+ const intents = links.filter((l) => l.kind === "supersedes");
46707
+ if (!intents.length)
46708
+ return [];
46709
+ const own2 = intents.find((l) => l.from_id === id);
46710
+ const rows = await db.selectFrom("unit_replacement as h").innerJoin("unit as a", "a.id", "h.from_unit").where((eb) => eb.or([
46711
+ eb("h.from_unit", "=", id),
46712
+ eb("h.to_unit", "=", id),
46713
+ ...own2 ? [eb("h.to_unit", "=", own2.to_id)] : []
46714
+ ])).where("h.started_at", "<=", asOf ?? "9999").select(["h.from_unit", "h.to_unit", "h.started_at", "h.ended_at", "h.end_reason", "a.key as from_key"]).orderBy("h.id").execute();
46715
+ const gaps = await db.selectFrom("unit_replacement_gap").select(["from_unit", "to_unit"]).where((eb) => eb.or([eb("from_unit", "=", id), eb("to_unit", "=", id)])).execute();
46716
+ const gap = (from, to) => gaps.some((g) => g.from_unit === from && g.to_unit === to);
46717
+ const unrecorded = "its history before the update to revision 10 was not recorded";
46718
+ const open2 = (r) => r.ended_at === null || asOf !== undefined && r.ended_at > asOf;
46719
+ const period = (r) => `from ${r.started_at} to ${r.ended_at}: ${inline(r.end_reason ?? "")}`;
46720
+ const out = [];
46721
+ if (own2) {
46722
+ const mine = rows.filter((r) => r.from_unit === id && r.to_unit === own2.to_id);
46723
+ const now = mine.find(open2);
46724
+ const holder = rows.find((r) => r.to_unit === own2.to_id && r.from_unit !== id && open2(r));
46725
+ out.push(`Supersedes ${own2.to_key} (${now ? `in effect since ${now.started_at}` : holder ? `not in effect: ${holder.from_key} is in effect as its successor` : "not in effect"})`);
46726
+ for (const r of mine)
46727
+ if (!open2(r))
46728
+ out.push(`Replaced ${own2.to_key} ${period(r)}`);
46729
+ if (gap(id, own2.to_id))
46730
+ out.push(`Supersedes ${own2.to_key}: ${unrecorded}`);
46731
+ }
46732
+ const into = rows.filter((r) => r.to_unit === id);
46733
+ for (const r of into)
46734
+ out.push(open2(r) ? `Superseded by ${r.from_key} (since ${r.started_at})` : `Was superseded by ${r.from_key} ${period(r)}`);
46735
+ for (const l of intents) {
46736
+ if (l.to_id !== id)
46737
+ continue;
46738
+ if (gap(l.from_id, id))
46739
+ out.push(`Superseded by ${l.from_key} at some time: ${unrecorded}`);
46740
+ else if (!into.some((r) => r.from_unit === l.from_id))
46741
+ out.push(`Replacement proposed by ${l.from_key} (${l.from_lifecycle ?? "candidate"})`);
46742
+ }
46743
+ return out;
46744
+ }
46545
46745
  function movedTo(root, a, renames) {
46546
46746
  if (!root || !a.commit_sha || fileState(root, a.path) !== "gone")
46547
46747
  return "";
@@ -46582,112 +46782,6 @@ function part(id, text2, from) {
46582
46782
  return end < all.length ? [shown, `(${all.length - end} more bytes; read s${id}@${end} for the rest)`] : [shown];
46583
46783
  }
46584
46784
 
46585
- // server/src/rule-files.ts
46586
- import fs4 from "node:fs";
46587
- import path4 from "node:path";
46588
- var RULE_LIMITS = { files: 200, bytes: 256 * 1024, depth: 8, entries: 5000 };
46589
- var RULE_NAMES = new Set(["CLAUDE.md", "AGENTS.md", "AGENTS.override.md"]);
46590
- var PATHSPECS = [...RULE_NAMES, ".claude/rules/**/*.md"].map((p) => `:(glob)**/${p}`);
46591
- function isRuleFile(rel) {
46592
- const parts = rel.split("/");
46593
- const name = parts.at(-1) ?? "";
46594
- if (RULE_NAMES.has(name))
46595
- return true;
46596
- const i = parts.findIndex((p, k) => p === ".claude" && parts[k + 1] === "rules");
46597
- return i >= 0 && parts.length > i + 2 && name.endsWith(".md");
46598
- }
46599
- function ruleFiles(root) {
46600
- const listed = gitList(root) ?? walk(root);
46601
- const out = { files: [], skipped: 0, incomplete: listed.incomplete };
46602
- const realRoot = fs4.realpathSync(root);
46603
- for (const [i, rel] of [...new Set(listed.paths)].sort().entries()) {
46604
- if (i >= RULE_LIMITS.files) {
46605
- out.skipped++;
46606
- continue;
46607
- }
46608
- const text2 = readBounded(root, realRoot, rel);
46609
- if (text2 === null)
46610
- continue;
46611
- if (text2 === undefined)
46612
- out.skipped++;
46613
- else
46614
- out.files.push({ path: rel, text: text2 });
46615
- }
46616
- return out;
46617
- }
46618
- function readBounded(root, realRoot, rel) {
46619
- const abs = path4.join(root, rel);
46620
- if (leaves(path4.relative(root, abs)))
46621
- return;
46622
- try {
46623
- const st = fs4.lstatSync(abs, { throwIfNoEntry: false });
46624
- if (!st)
46625
- return null;
46626
- if (!st.isFile() || st.size > RULE_LIMITS.bytes)
46627
- return;
46628
- if (leaves(path4.relative(realRoot, fs4.realpathSync(abs))))
46629
- return;
46630
- const buf = fs4.readFileSync(abs);
46631
- return buf.length > RULE_LIMITS.bytes || buf.includes(0) ? undefined : buf.toString("utf8");
46632
- } catch {
46633
- return;
46634
- }
46635
- }
46636
- function gitList(root) {
46637
- try {
46638
- if (cleanGit(root, ["rev-parse", "--is-inside-work-tree"]).toString("utf8").trim() !== "true")
46639
- return null;
46640
- } catch {
46641
- return null;
46642
- }
46643
- try {
46644
- const out = cleanGit(root, ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", ...PATHSPECS], 16 * 1024 * 1024);
46645
- return { paths: out.toString("utf8").split("\x00").filter(isRuleFile), incomplete: null };
46646
- } catch {
46647
- return { paths: [], incomplete: "git could not list the files" };
46648
- }
46649
- }
46650
- function walk(root) {
46651
- const paths = [];
46652
- let entries = 0;
46653
- let unread = 0;
46654
- let tooDeep = false;
46655
- let stopped = false;
46656
- const visit2 = (rel, depth) => {
46657
- if (stopped)
46658
- return;
46659
- let list;
46660
- try {
46661
- list = fs4.readdirSync(path4.join(root, rel), { withFileTypes: true });
46662
- } catch {
46663
- unread++;
46664
- return;
46665
- }
46666
- for (const d of list) {
46667
- if (++entries > RULE_LIMITS.entries) {
46668
- stopped = true;
46669
- return;
46670
- }
46671
- const child = rel ? `${rel}/${d.name}` : d.name;
46672
- if ((d.isFile() || d.isSymbolicLink()) && isRuleFile(child))
46673
- paths.push(child);
46674
- else if (d.isDirectory() && d.name !== "node_modules" && (!d.name.startsWith(".") || d.name === ".claude")) {
46675
- if (depth >= RULE_LIMITS.depth)
46676
- tooDeep = true;
46677
- else
46678
- visit2(child, depth + 1);
46679
- }
46680
- }
46681
- };
46682
- visit2("", 1);
46683
- const why = [
46684
- ...stopped ? [`stopped after ${RULE_LIMITS.entries} directory entries`] : [],
46685
- ...tooDeep ? [`did not look deeper than ${RULE_LIMITS.depth} directories`] : [],
46686
- ...unread ? [`could not read ${unread} director${unread === 1 ? "y" : "ies"}`] : []
46687
- ];
46688
- return { paths, incomplete: why.length ? why.join("; ") : null };
46689
- }
46690
-
46691
46785
  // server/src/export.ts
46692
46786
  var EXPORT_LIMITS = { records: 50, depth: 20, bytes: 60 * 1024 };
46693
46787
  async function chosen(db, projectId, ref) {
@@ -46740,9 +46834,11 @@ async function lines(db, u) {
46740
46834
  const kept = (t) => plain(t).split(`
46741
46835
  `).join(`
46742
46836
  `);
46837
+ const whose = ["decision", "constraint"].includes(u.kind) ? (await authorityOf(db, [u.id])).get(u.id) : undefined;
46743
46838
  const out = [
46744
46839
  `key: ${inline(u.key)} (u${u.id})`,
46745
46840
  `kind: ${u.kind}${u.stance ? ` ${u.stance}` : ""}`,
46841
+ ...whose ? [`authority: ${AUTHORITY[whose]}`] : [],
46746
46842
  `text: ${kept(u.text)}`
46747
46843
  ];
46748
46844
  if (u.why)
@@ -46784,7 +46880,7 @@ async function replaced(db, from) {
46784
46880
  const out = [];
46785
46881
  let frontier = [from];
46786
46882
  for (let depth = 0;frontier.length; depth++) {
46787
- const older = await db.selectFrom("unit_link as l").innerJoin("unit as u", "u.id", "l.to_unit").where("l.from_unit", "in", frontier.map((u) => u.id)).where("l.kind", "=", "supersedes").selectAll("u").select("l.from_unit").orderBy("l.from_unit").orderBy("u.id").execute();
46883
+ const older = await db.selectFrom("unit_replacement as h").innerJoin("unit as u", "u.id", "h.to_unit").where("h.from_unit", "in", frontier.map((u) => u.id)).where("h.ended_at", "is", null).selectAll("u").select("h.from_unit").orderBy("h.from_unit").orderBy("u.id").execute();
46788
46884
  if (!older.some((u) => !seen.has(u.id)))
46789
46885
  break;
46790
46886
  if (depth === EXPORT_LIMITS.depth)
@@ -46859,12 +46955,6 @@ ${problems.join(`
46859
46955
  return tooBig;
46860
46956
  return { document };
46861
46957
  }
46862
- var INSTRUCTION_DIRS = new Set([".claude", ".agents", ".codex", ".cursor"]);
46863
- var instructionFile = (relative) => {
46864
- const parts = relative.toLowerCase().split(/[\\/]/);
46865
- const names = [...RULE_NAMES].map((n) => n.toLowerCase());
46866
- return parts.some((p) => INSTRUCTION_DIRS.has(p)) || names.includes(parts.at(-1) ?? "") || parts.slice(-2).join("/") === ".github/copilot-instructions.md";
46867
- };
46868
46958
  function exportPath(root, given) {
46869
46959
  if (inline(given) !== given || given.includes("\x00"))
46870
46960
  return { error: "The path has characters that cannot be shown as typed; use plain characters." };
@@ -47017,12 +47107,13 @@ async function liveOverview(db, projectId, after) {
47017
47107
  live.select(["id", "key", "kind", "stance", "text"]).where("id", ">", after ?? 0).orderBy("id").limit(OVERVIEW_LIMITS.records + 1).execute()
47018
47108
  ]);
47019
47109
  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() : [];
47110
+ const whose = await authorityOf(db, rows.map((r) => r.id));
47020
47111
  const shown = [];
47021
47112
  for (const r of rows.slice(0, OVERVIEW_LIMITS.records)) {
47022
47113
  const paths = [...new Set(anchors.filter((a) => a.unit_id === r.id).map((a) => a.path))];
47023
47114
  const first = paths[0];
47024
47115
  const dir = first === undefined ? null : path6.posix.dirname(first);
47025
- 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)}]` : ""}`;
47116
+ const line = `- ${head(inline(r.key), OVERVIEW_LIMITS.key)} (u${r.id}, ${r.kind}${r.stance ? ` ${r.stance}` : ""}${whose.get(r.id) === "agent" ? ", decided by an AI" : ""}): ${head(inline(r.text), OVERVIEW_LIMITS.text)}${paths.length ? ` [${pathList(paths)}]` : ""}`;
47026
47117
  shown.push({
47027
47118
  id: r.id,
47028
47119
  group: dir === null ? PROJECT_WIDE : dir === "." ? "(repository root)" : `${dir}/`,
@@ -47043,7 +47134,8 @@ async function liveOverview(db, projectId, after) {
47043
47134
  ]),
47044
47135
  `${shown.length} shown of ${n} active decisions and constraints${after === null ? "" : ` (ids after ${after})`}.`,
47045
47136
  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.",
47046
- "Read a record by its key or u<id> before relying on it."
47137
+ "Read a record by its key or u<id> before relying on it.",
47138
+ ...shown.some((x) => whose.get(x.id) === "agent") ? [AI_DECIDED] : []
47047
47139
  ].join(`
47048
47140
  `);
47049
47141
  }
@@ -47170,7 +47262,7 @@ async function successor(db, id) {
47170
47262
  let end = null;
47171
47263
  let at = id;
47172
47264
  for (;; ) {
47173
- 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();
47265
+ const next = await db.selectFrom("unit_replacement as h").innerJoin("unit as u", "u.id", "h.from_unit").where("h.to_unit", "=", at).where("h.ended_at", "is", null).select(["u.id", "u.key", "u.lifecycle"]).executeTakeFirst();
47174
47266
  if (!next)
47175
47267
  break;
47176
47268
  end = { key: next.key, lifecycle: next.lifecycle };
@@ -47414,6 +47506,16 @@ async function selectForReview(db, projectId2, files) {
47414
47506
  }
47415
47507
  return [...out.values()];
47416
47508
  }
47509
+ var AI_DEPARTURE = "A record marked decided by an AI was decided by an AI in an earlier session, not by the owner: a change that departs from it is a violation only when the change gives no reason for departing.";
47510
+ async function selectedText(db, hits) {
47511
+ const whose = await authorityOf(db, hits.map((u) => u.id));
47512
+ const ai = hits.some((u) => whose.get(u.id) === "agent");
47513
+ return [
47514
+ ...hits.map((u) => `- ${u.key} (${u.kind}${u.stance ? ` ${u.stance}` : ""}${whose.get(u.id) === "agent" ? ", decided by an AI" : ""}): ${head(inline(u.text), 300)} [${u.because}]`),
47515
+ ...ai ? [AI_DEPARTURE] : []
47516
+ ].join(`
47517
+ `);
47518
+ }
47417
47519
 
47418
47520
  // server/src/review-findings.ts
47419
47521
  var Finding = exports_external.object({
@@ -47464,21 +47566,28 @@ async function checkFindings(db, projectId2, files, raw) {
47464
47566
  // server/src/trace.ts
47465
47567
  var PENDING_DAYS = 14;
47466
47568
  var pendingCutoff = (now) => new Date(now.getTime() - PENDING_DAYS * 86400000).toISOString();
47467
- 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")))));
47468
- 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) => [
47469
- "s.id",
47470
- "s.host",
47471
- "s.started_at",
47472
- "s.branch",
47473
- 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"),
47474
- untracedOwner(eb).select((m) => m.fn.countAll().as("waiting")).as("waiting"),
47475
- untracedOwner(eb).select((m) => m.fn.min("m.id").as("first")).as("first")
47476
- ]);
47569
+ var untraced = (eb, auto) => {
47570
+ const q = eb.selectFrom("source as m").whereRef("m.session_id", "=", "s.id");
47571
+ return (auto ? q : q.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")))));
47572
+ };
47573
+ var untracedSessions = (db, projectId2, o = {}) => {
47574
+ const auto = o.auto ?? false;
47575
+ const skip = o.skip;
47576
+ return db.selectFrom("session as s").where("s.project_id", "=", projectId2).$if(Boolean(skip), (q) => q.where((eb) => eb.not(eb.and([eb("s.host", "=", skip?.host ?? ""), eb("s.external_id", "=", skip?.session ?? "")])))).where((eb) => eb.exists(untraced(eb, auto).select(sql`1`.as("x")))).select((eb) => [
47577
+ "s.id",
47578
+ "s.host",
47579
+ "s.started_at",
47580
+ "s.branch",
47581
+ eb.selectFrom("source as o").whereRef("o.session_id", "=", "s.id").where("o.author_kind", "=", "owner").select((o2) => o2.fn.max("o.created_at").as("last")).as("last"),
47582
+ untraced(eb, auto).select((m) => m.fn.countAll().as("waiting")).as("waiting"),
47583
+ untraced(eb, auto).select((m) => m.fn.min("m.id").as("first")).as("first")
47584
+ ]);
47585
+ };
47477
47586
 
47478
47587
  // server/src/status.ts
47479
- async function pendingCount(db, projectId2, now = new Date) {
47588
+ async function pendingCount(db, projectId2, now = new Date, o = {}) {
47480
47589
  const cutoff = pendingCutoff(now);
47481
- const r = await db.selectFrom(untracedSessions(db, projectId2).as("w")).select([
47590
+ const r = await db.selectFrom(untracedSessions(db, projectId2, o).as("w")).select([
47482
47591
  sql`count(case when w.last >= ${cutoff} then 1 end)`.as("recent"),
47483
47592
  sql`count(case when w.last < ${cutoff} then 1 end)`.as("older")
47484
47593
  ]).executeTakeFirst();
@@ -47544,18 +47653,6 @@ var among = (r) => r.stopped ? `among the first ${r.read} candidates by rank ` :
47544
47653
  var stoppedAfter = (r) => r.stopped ? `
47545
47654
 
47546
47655
  Stopped after ${r.read} candidates by rank; more may match.` : "";
47547
- var hitText = (h) => [
47548
- `## ${h.key} (u${h.id}): ${h.kind}${h.stance ? ` ${h.stance}` : ""}, ${h.lifecycle}`,
47549
- head(h.text, 600),
47550
- ...h.why ? [`Why: ${head(h.why, 400)}`] : [],
47551
- ...h.revisit_when ? [`Revisit when: ${head(h.revisit_when, 200)}`] : [],
47552
- ...h.options.length ? [
47553
- `Options: ${h.options.map((o) => `${o.text} (${o.outcome}${o.why ? `: ${head(o.why, 160)}` : ""})`).join(" / ")}`
47554
- ] : [],
47555
- ...h.anchors.length ? [`Code: ${h.anchors.map((a) => `${a.path}${a.symbol ? ` ${a.symbol}` : ""} (${a.role})`).join(", ")}`] : [],
47556
- h.successorOf ? `Replaces ${h.successorOf}, which matched` : `Matched: ${h.matched.join(", ")}${h.aliasOnly ? " (search aliases only)" : ""}`
47557
- ].join(`
47558
- `);
47559
47656
  var server = new McpServer({ name: "sphica", version: VERSION ?? "unknown" }, {
47560
47657
  capabilities: { experimental: { "codex/sandbox-state-meta": {} } },
47561
47658
  instructions: [
@@ -47646,9 +47743,7 @@ Matched: ${h.matched.join(", ")}`).join(`
47646
47743
  return text2(`Nothing was searched: ${r.refused}`, true);
47647
47744
  if (!r.hits.length)
47648
47745
  return text2(`No record ${among(r)}holds most of: ${r.terms.join(", ") || "(no searchable words)"}. ${r.weaker} weaker matches left out. ` + (r.stopped ? "Search with more specific words. " : "") + "Search again with other words or the other language, or search sources; status says whether sessions are still untraced.");
47649
- return text2(framed(`${r.hits.map(hitText).join(`
47650
-
47651
- `)}${stoppedAfter(r)}
47746
+ return text2(framed(`${hitsText(r.hits)}${stoppedAfter(r)}
47652
47747
 
47653
47748
  Read a record by its key before relying on it.`));
47654
47749
  } catch (e) {
@@ -47757,8 +47852,7 @@ server.registerTool("review_select", {
47757
47852
  if (!hits.length)
47758
47853
  return text2(`Decision lane: checked. No active record applies to the ${files.length} changed files.`);
47759
47854
  return text2(`Decision lane: checked. ${hits.length} records apply; read each before judging it.
47760
- ${framed(hits.map((u) => `- ${u.key} (${u.kind}${u.stance ? ` ${u.stance}` : ""}): ${head(inline(u.text), 300)} [${u.because}]`).join(`
47761
- `))}`);
47855
+ ${framed(await selectedText(db, hits))}`);
47762
47856
  } catch (e) {
47763
47857
  return notChecked(e);
47764
47858
  }