sphica 0.6.36 → 0.6.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/mcp-record.js +110 -62
- package/package.json +1 -1
- package/skills/glean/SKILL.md +3 -2
- package/skills/harvest/SKILL.md +2 -1
- package/skills/trace/SKILL.md +3 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "sphica",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.37",
|
|
5
5
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "iroha924",
|
package/dist/mcp-record.js
CHANGED
|
@@ -46274,6 +46274,20 @@ async function inTransaction(db, fn) {
|
|
|
46274
46274
|
}
|
|
46275
46275
|
});
|
|
46276
46276
|
}
|
|
46277
|
+
async function inRolledBack(db, fn) {
|
|
46278
|
+
return db.connection().execute(async (c) => {
|
|
46279
|
+
await sql`begin immediate`.execute(c);
|
|
46280
|
+
let out;
|
|
46281
|
+
try {
|
|
46282
|
+
out = await fn(c);
|
|
46283
|
+
} catch (e) {
|
|
46284
|
+
await sql`rollback`.execute(c).catch(() => {});
|
|
46285
|
+
throw e;
|
|
46286
|
+
}
|
|
46287
|
+
await sql`rollback`.execute(c);
|
|
46288
|
+
return out;
|
|
46289
|
+
});
|
|
46290
|
+
}
|
|
46277
46291
|
var iso = (d) => {
|
|
46278
46292
|
const t = new Date(d);
|
|
46279
46293
|
if (Number.isNaN(t.getTime()))
|
|
@@ -48691,9 +48705,7 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
48691
48705
|
...new Set(record4.units.flatMap((u) => [...u.supersedes ? [u.supersedes] : [], ...u.conflicts]))
|
|
48692
48706
|
];
|
|
48693
48707
|
const others = new Map((linked.length ? await db.selectFrom("unit as o").select(["o.id", "o.key", "o.kind", "o.lifecycle"]).where("o.project_id", "=", target.projectId).where("o.key", "in", linked).execute() : []).map((u) => [u.key, u]));
|
|
48694
|
-
const holders = new Map((others.size ? await db.selectFrom("unit_replacement as h").innerJoin("unit as n", "n.id", "h.from_unit").select(["h.to_unit", "n.key", "n.lifecycle"]).where("h.ended_at", "is", null).where("h.to_unit", "in", [...others.values()].map((o) => o.id)).execute() : []).map((h) => [h.to_unit, h]));
|
|
48695
48708
|
const units = [];
|
|
48696
|
-
const claimed = new Set;
|
|
48697
48709
|
for (const [i, u] of record4.units.entries()) {
|
|
48698
48710
|
const key = keys[i] ?? "";
|
|
48699
48711
|
const quarantine = [];
|
|
@@ -48851,23 +48863,14 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
|
|
|
48851
48863
|
let supersedes = null;
|
|
48852
48864
|
if (u.supersedes) {
|
|
48853
48865
|
const old = others.get(u.supersedes);
|
|
48854
|
-
const implemented = () => evidence.some((e) => e.role === "implements" && (["commit_message", "file_excerpt"].includes(sources.get(e.source)?.kind ?? "") || anchors.some((a) => a.role === "evidence" && (a.commit || a.observation !== null && sources.get(e.source)?.session_id === target.sessionId)))) || anchors.some((a) => a.role === "evidence" && a.commit);
|
|
48855
|
-
const takes = ["decision", "constraint"].includes(u.kind) ? adoption.some((x) => x.route !== "agent") : u.kind !== "implementation" || implemented();
|
|
48856
48866
|
if (!old)
|
|
48857
48867
|
errors3.push(`${key}: supersedes ${u.supersedes}, which is not a record of this project`);
|
|
48858
48868
|
else if (!["active", "candidate"].includes(old.lifecycle))
|
|
48859
48869
|
errors3.push(`${key}: ${u.supersedes} is already ${old.lifecycle}`);
|
|
48860
|
-
else if (claimed.has(old.id) && !quarantine.length && takes)
|
|
48861
|
-
errors3.push(`${key}: another record in this save already supersedes ${u.supersedes}`);
|
|
48862
48870
|
else if (!replaceable(u.kind, old.kind))
|
|
48863
48871
|
errors3.push(`${key}: a ${u.kind} cannot supersede ${u.supersedes}, a ${old.kind} (a record supersedes one of its own kind; a decision and a constraint can replace each other)`);
|
|
48864
|
-
else
|
|
48865
|
-
const h = holders.get(old.id);
|
|
48866
|
-
errors3.push(`${key}: ${u.supersedes} already has a successor, ${h?.key} (${h?.lifecycle}); withdraw it first, or supersede it instead`);
|
|
48867
|
-
} else
|
|
48872
|
+
else
|
|
48868
48873
|
supersedes = old.id;
|
|
48869
|
-
if (supersedes !== null && !quarantine.length && takes)
|
|
48870
|
-
claimed.add(supersedes);
|
|
48871
48874
|
}
|
|
48872
48875
|
const conflicts = u.conflicts.flatMap((k) => {
|
|
48873
48876
|
const other = others.get(k);
|
|
@@ -49001,12 +49004,20 @@ var contentHash = (u) => sha256(JSON.stringify([
|
|
|
49001
49004
|
u.revisit_when ?? null,
|
|
49002
49005
|
u.options.map((o) => o.reconsider_when === undefined ? [o.text, o.outcome, o.why ?? null] : [o.text, o.outcome, o.why ?? null, o.reconsider_when])
|
|
49003
49006
|
]));
|
|
49007
|
+
|
|
49008
|
+
class SaveRefused extends Error {
|
|
49009
|
+
}
|
|
49004
49010
|
async function settleSaved(trx, runId, saved, more = {}) {
|
|
49005
49011
|
const written = new Map(saved.written.map((w) => [w.id, w]));
|
|
49006
49012
|
const settled2 = await reconcile(trx, [...written.keys(), ...more.seeds ?? []], { runId }, {
|
|
49007
49013
|
hints: new Map(saved.written.map((w) => [w.id, w.hint])),
|
|
49008
49014
|
withdraw: more.withdraw
|
|
49009
49015
|
});
|
|
49016
|
+
const mine = new Set([...written.keys(), ...more.adopted ?? []]);
|
|
49017
|
+
const key = (id) => settled2.keys.get(id) ?? `u${id}`;
|
|
49018
|
+
for (const [id, [to, holder]] of settled2.held)
|
|
49019
|
+
if (mine.has(id))
|
|
49020
|
+
throw new SaveRefused(mine.has(holder) ? `${key(id)}: another record in this save already supersedes ${key(to)}` : `${key(id)}: ${key(to)} already has a successor, ${key(holder)} (in effect); withdraw it first, or supersede it instead`);
|
|
49010
49021
|
const now = new Map(settled2.changes.map((c) => [c.id, c.after]));
|
|
49011
49022
|
for (const w of saved.written) {
|
|
49012
49023
|
if (saved.quarantined.some((q) => q.startsWith(`${w.key} (`)))
|
|
@@ -49179,7 +49190,6 @@ ${checked.errors.map((e) => ` ${e}`).join(`
|
|
|
49179
49190
|
saved.written.push({
|
|
49180
49191
|
id,
|
|
49181
49192
|
key: p.key,
|
|
49182
|
-
adopted: p.adoption.length > 0,
|
|
49183
49193
|
hint: {
|
|
49184
49194
|
reason: p.adoption.length ? "evidence and adoption found" : "evidence found",
|
|
49185
49195
|
source: p.adoption[0]?.source ?? p.evidence[0]?.source ?? null
|
|
@@ -49477,9 +49487,6 @@ async function checkGlean(db, target, raw, facts = prepareGlean(target.root, raw
|
|
|
49477
49487
|
errors3.push(`${what}: the merge does not adopt a proposal`);
|
|
49478
49488
|
else if (got && got.s.author_kind !== "owner" && !MAINTAINERS2.has(got.s.author_association ?? ""))
|
|
49479
49489
|
errors3.push(`${what}: only the owner or a maintainer can adopt`);
|
|
49480
|
-
const held = await db.selectFrom("unit_link as l").innerJoin("unit_replacement as r", (j) => j.onRef("r.to_unit", "=", "l.to_unit").on("r.ended_at", "is", null)).innerJoin("unit as h", "h.id", "r.from_unit").innerJoin("unit as t", "t.id", "l.to_unit").where("l.from_unit", "=", u.id).where("l.kind", "=", "supersedes").where("r.from_unit", "!=", u.id).select(["h.key as holder", "t.key as replaced"]).executeTakeFirst();
|
|
49481
|
-
if (held && !parsed.data.ops.some((o) => o.op === "withdraw" && o.unit === held.holder))
|
|
49482
|
-
errors3.push(`${what}: ${held.replaced} already has a successor, ${held.holder} (in effect); withdraw it first, or supersede it instead`);
|
|
49483
49490
|
}
|
|
49484
49491
|
if (op.op === "anchor" && !repoPath(op.path))
|
|
49485
49492
|
errors3.push(`${what}: the path is not inside the repository`);
|
|
@@ -49773,16 +49780,11 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
49773
49780
|
}
|
|
49774
49781
|
const settled2 = await settleSaved(trx, runId, units, {
|
|
49775
49782
|
seeds: [...touched.keys(), ...withdraw.keys()],
|
|
49776
|
-
withdraw
|
|
49783
|
+
withdraw,
|
|
49784
|
+
adopted
|
|
49777
49785
|
});
|
|
49778
49786
|
for (const id of settled2.redundant)
|
|
49779
49787
|
changed2.push(`${settled2.keys.get(id)}: superseded by a record of this save, so not withdrawn`);
|
|
49780
|
-
for (const w of units.written)
|
|
49781
|
-
if (w.adopted)
|
|
49782
|
-
adopted.add(w.id);
|
|
49783
|
-
for (const [id, [to, holder]] of settled2.held)
|
|
49784
|
-
if (adopted.has(id))
|
|
49785
|
-
throw new Error(`${settled2.keys.get(id)}: ${settled2.keys.get(to)} already has a successor, ${settled2.keys.get(holder)} (in effect); withdraw it first, or supersede it instead`);
|
|
49786
49788
|
const written = new Set(units.written.map((w) => w.id));
|
|
49787
49789
|
for (const ch of settled2.changes) {
|
|
49788
49790
|
if (written.has(ch.id))
|
|
@@ -50243,42 +50245,43 @@ function contextOnly(id, items, c) {
|
|
|
50243
50245
|
...c.work && !c.units.length ? ["work: an automatic run updates work only beside a record of the messages it traces"] : []
|
|
50244
50246
|
];
|
|
50245
50247
|
}
|
|
50246
|
-
async function
|
|
50247
|
-
const run = await bound(db, id, projectId2);
|
|
50248
|
-
const { target, items } = await scopeOf(db, run, root);
|
|
50249
|
-
target.agent = await agentRun(db, run, call);
|
|
50250
|
-
const c = run.origin === "glean" ? await checkGlean(db, target, record4) : await checkRecord(db, target, record4);
|
|
50251
|
-
if (!("ops" in c))
|
|
50252
|
-
c.errors.push(...contextOnly(id, items, c));
|
|
50253
|
-
const units = "ops" in c ? c.units.units : c.units;
|
|
50254
|
-
const lines = [
|
|
50255
|
-
...c.errors.map((e) => `✗ ${e}`),
|
|
50256
|
-
...c.problems.map((p) => `△ ${p}`),
|
|
50257
|
-
...units.filter((u) => u.quarantine.length).map((u) => `△ ${u.key} will be quarantined: ${u.quarantine.join("; ")}`)
|
|
50258
|
-
];
|
|
50259
|
-
const summary = c.errors.length ? `✗ ${plural2(c.errors.length, "error")}; fix the record and check again` : `✓ ${plural2(units.length, "record")}${"ops" in c ? ` and ${plural2(c.ops.length, "change")}` : ""} can be saved`;
|
|
50260
|
-
return { ok: c.errors.length === 0, text: [...lines, summary].join(`
|
|
50261
|
-
`) };
|
|
50262
|
-
}
|
|
50263
|
-
async function saveText(db, id, projectId2, root, record4, probe, call) {
|
|
50248
|
+
async function prepare2(db, id, projectId2, root, record4, probe, call) {
|
|
50264
50249
|
const begun = await bound(db, id, projectId2);
|
|
50265
50250
|
if (call !== undefined && begun.begin_call_id !== null)
|
|
50266
50251
|
await sameCaller(db, begun.begin_call_id, call);
|
|
50267
|
-
const
|
|
50268
|
-
|
|
50269
|
-
|
|
50270
|
-
|
|
50271
|
-
|
|
50272
|
-
|
|
50273
|
-
|
|
50274
|
-
|
|
50275
|
-
|
|
50276
|
-
const
|
|
50277
|
-
|
|
50278
|
-
|
|
50279
|
-
|
|
50280
|
-
|
|
50281
|
-
|
|
50252
|
+
const gleanFacts = begun.origin === "glean" ? prepareGlean(root, record4, probe) : undefined;
|
|
50253
|
+
return { facts: gleanFacts ?? prepareRecord(root, record4, probe), gleanFacts };
|
|
50254
|
+
}
|
|
50255
|
+
async function validate2(trx, id, projectId2, root, record4, prepared, call) {
|
|
50256
|
+
const run = await bound(trx, id, projectId2);
|
|
50257
|
+
const scope = await scopeOf(trx, run, root);
|
|
50258
|
+
scope.target.agent = await agentRun(trx, run, call);
|
|
50259
|
+
const quarantine = (units) => units.filter((u) => u.quarantine.length).map((u) => `${u.key} will be quarantined: ${u.quarantine.join("; ")}`);
|
|
50260
|
+
if (run.origin === "glean") {
|
|
50261
|
+
const c = await checkGlean(trx, scope.target, record4, prepared.gleanFacts);
|
|
50262
|
+
return {
|
|
50263
|
+
run,
|
|
50264
|
+
errors: c.errors,
|
|
50265
|
+
problems: c.problems,
|
|
50266
|
+
quarantine: quarantine(c.units.units),
|
|
50267
|
+
units: c.units.units.length,
|
|
50268
|
+
ops: c.ops.length,
|
|
50269
|
+
write: async (w) => {
|
|
50270
|
+
const g = await saveGlean(w, scope.target, run.id, c);
|
|
50271
|
+
return { saved: g.units, changed: g.changed };
|
|
50272
|
+
}
|
|
50273
|
+
};
|
|
50274
|
+
}
|
|
50275
|
+
const checked = await checkRecord(trx, scope.target, record4, prepared.facts);
|
|
50276
|
+
checked.errors.push(...contextOnly(id, scope.items, checked));
|
|
50277
|
+
return {
|
|
50278
|
+
run,
|
|
50279
|
+
errors: checked.errors,
|
|
50280
|
+
problems: checked.problems,
|
|
50281
|
+
quarantine: quarantine(checked.units),
|
|
50282
|
+
units: checked.units.length,
|
|
50283
|
+
ops: null,
|
|
50284
|
+
write: async (w) => {
|
|
50282
50285
|
const shown = shownTo.get(id)?.sources ?? new Set;
|
|
50283
50286
|
const cited = new Set([
|
|
50284
50287
|
...checked.units.flatMap((u) => [
|
|
@@ -50289,17 +50292,62 @@ async function saveText(db, id, projectId2, root, record4, probe, call) {
|
|
|
50289
50292
|
]),
|
|
50290
50293
|
...checked.fieldDefs
|
|
50291
50294
|
].map((q) => q.source));
|
|
50292
|
-
|
|
50293
|
-
|
|
50294
|
-
|
|
50295
|
+
const saved = await saveRecord(w, scope.target, run.id, checked, scope.looked.filter((s) => shown.has(s) || cited.has(s)));
|
|
50296
|
+
return { saved, changed: [] };
|
|
50297
|
+
}
|
|
50298
|
+
};
|
|
50299
|
+
}
|
|
50300
|
+
async function checkText(db, id, projectId2, root, record4, call, probe) {
|
|
50301
|
+
const prepared = await prepare2(db, id, projectId2, root, record4, probe, call);
|
|
50302
|
+
const { v, done, refused } = await inRolledBack(db, async (trx) => {
|
|
50303
|
+
const v2 = await validate2(trx, id, projectId2, root, record4, prepared, call);
|
|
50304
|
+
if (v2.errors.length)
|
|
50305
|
+
return { v: v2, done: null, refused: null };
|
|
50306
|
+
try {
|
|
50307
|
+
const done2 = await v2.write(trx);
|
|
50308
|
+
await finishRun(trx, v2.run.id);
|
|
50309
|
+
return { v: v2, done: done2, refused: null };
|
|
50310
|
+
} catch (e) {
|
|
50311
|
+
if (!(e instanceof SaveRefused))
|
|
50312
|
+
throw e;
|
|
50313
|
+
return { v: v2, done: null, refused: e.message };
|
|
50314
|
+
}
|
|
50315
|
+
});
|
|
50316
|
+
const errors3 = [...v.errors, ...refused === null ? [] : [refused]];
|
|
50317
|
+
const lines = [
|
|
50318
|
+
...errors3.map((e) => `✗ ${e}`),
|
|
50319
|
+
...[...new Set(v.problems)].map((p) => `△ ${p}`),
|
|
50320
|
+
...done ? [
|
|
50321
|
+
...done.saved.active.map((k) => `✓ would be active: ${k}`),
|
|
50322
|
+
...done.saved.superseded.map((k) => `✓ would be superseded: ${k}`),
|
|
50323
|
+
...done.saved.candidates.map((c) => `△ would stay a candidate: ${c.key}: ${c.why}`),
|
|
50324
|
+
...done.saved.quarantined.map((q) => `△ would be quarantined: ${q}`),
|
|
50325
|
+
...[...new Set(done.saved.anchorProblems)].filter((a) => !v.problems.some((p) => p === a || p.endsWith(` ${a}`))).map((a) => `△ ${a}`),
|
|
50326
|
+
...done.changed.map((c) => `✓ would: ${c}`)
|
|
50327
|
+
] : v.quarantine.map((q) => `△ ${q}`)
|
|
50328
|
+
];
|
|
50329
|
+
const summary = errors3.length ? `✗ ${plural2(errors3.length, "error")}; fix the record and check again` : `✓ ${plural2(v.units, "record")}${v.ops === null ? "" : ` and ${plural2(v.ops, "change")}`} can be saved`;
|
|
50330
|
+
return { ok: errors3.length === 0, text: [...lines, summary].join(`
|
|
50331
|
+
`) };
|
|
50332
|
+
}
|
|
50333
|
+
async function saveText(db, id, projectId2, root, record4, probe, call) {
|
|
50334
|
+
const prepared = await prepare2(db, id, projectId2, root, record4, probe, call);
|
|
50335
|
+
const text3 = await inTransaction(db, async (trx) => {
|
|
50336
|
+
const v = await validate2(trx, id, projectId2, root, record4, prepared, call);
|
|
50337
|
+
if (v.errors.length)
|
|
50338
|
+
throw new Error(`The record is not valid:
|
|
50339
|
+
${v.errors.map((e) => ` ${e}`).join(`
|
|
50340
|
+
`)}`);
|
|
50341
|
+
const { saved, changed: changed2 } = await v.write(trx);
|
|
50342
|
+
await finishRun(trx, v.run.id);
|
|
50295
50343
|
return [
|
|
50296
50344
|
...saved.active.map((k) => `✓ ${k} active`),
|
|
50297
50345
|
...saved.superseded.map((k) => `✓ ${k} superseded`),
|
|
50298
50346
|
...saved.candidates.map((c) => `△ ${c.key} candidate: ${c.why}`),
|
|
50299
50347
|
...saved.quarantined.map((q) => `△ ${q} quarantined`),
|
|
50300
50348
|
...saved.anchorProblems.map((a) => `△ ${a}`),
|
|
50301
|
-
...
|
|
50302
|
-
...
|
|
50349
|
+
...v.ops === null ? v.problems.map((n) => `△ ${n}`) : [],
|
|
50350
|
+
...changed2.map((c) => `✓ ${c}`),
|
|
50303
50351
|
"✓ saved"
|
|
50304
50352
|
].join(`
|
|
50305
50353
|
`);
|
|
@@ -50598,7 +50646,7 @@ server.registerTool("record_context", {
|
|
|
50598
50646
|
}));
|
|
50599
50647
|
server.registerTool("record_check", {
|
|
50600
50648
|
title: "Check a record",
|
|
50601
|
-
description: "Checks a record
|
|
50649
|
+
description: "Checks a record by running the save in a transaction that rolls back: it refuses what record_save would refuse and says what would become of each record (would be active, would stay a candidate, would be superseded), writing nothing.",
|
|
50602
50650
|
inputSchema: exports_external.object({ run: RUN, record: RECORD, cwd: CWD }).strict(),
|
|
50603
50651
|
annotations: READ
|
|
50604
50652
|
}, async (a, extra) => tool(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sphica",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.37",
|
|
4
4
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/skills/glean/SKILL.md
CHANGED
|
@@ -49,8 +49,9 @@ read tools `search` and `read`. Pass the repository root as `cwd` to every tool.
|
|
|
49
49
|
the owner's message that quotes it
|
|
50
50
|
5. **Read**: `record_context` with the run: the owner's messages in this session with their refs. When a page ends with
|
|
51
51
|
`call record_context with after: "s<N>"`, call it again with that `after`
|
|
52
|
-
6. **Check**: `record_check` with the run and the record below.
|
|
53
|
-
|
|
52
|
+
6. **Check**: `record_check` with the run and the record below. It runs the save in a transaction that rolls back, so it refuses what the
|
|
53
|
+
save would refuse and lists what would change (`would be active`, `would: <change>`) without writing it. Fix errors and check again. A
|
|
54
|
+
note to ask the owner for a source means step 2 is not done
|
|
54
55
|
7. **Save**: `record_save`. **Report** what changed, copying save's lines
|
|
55
56
|
|
|
56
57
|
## The record
|
package/skills/harvest/SKILL.md
CHANGED
|
@@ -37,7 +37,8 @@ as `cwd` to every tool.
|
|
|
37
37
|
sources an earlier harvest already looked at: what they decided may already be saved, so search before recording it again. Read all of it before writing:
|
|
38
38
|
when a page ends with `call record_context with after: "s<N>"`, call it again with that `after`, until the last page
|
|
39
39
|
4. **Check**: `record_check` with the run and the record as `record`. The shape and fields are trace's ([../trace/SKILL.md](../trace/SKILL.md),
|
|
40
|
-
"The record"), with `work` left out. Keys are saved as `harvest:<number>/<key>`.
|
|
40
|
+
"The record"), with `work` left out. Keys are saved as `harvest:<number>/<key>`. As in trace, check runs the save in a transaction that
|
|
41
|
+
rolls back and says what would become of each record (`would be active`, ...). Fix and check again until there are no errors.
|
|
41
42
|
A file the pull request touched may have moved since: on a warning that a path is not in the working tree, give the commit that holds it,
|
|
42
43
|
or anchor where the code is now
|
|
43
44
|
5. **Save**: `record_save` with the same run and record
|
package/skills/trace/SKILL.md
CHANGED
|
@@ -41,7 +41,9 @@ Everything goes through Sphica's `record` MCP server (its tools are `trace_pendi
|
|
|
41
41
|
the last page (the one with the live records). Saving marks as looked at only the messages you were shown and those you quote; the
|
|
42
42
|
rest stay pending.
|
|
43
43
|
Use `search` and `read` to look at older records this session may replace
|
|
44
|
-
4. **Check**: `record_check` with the run and the record below as `record`.
|
|
44
|
+
4. **Check**: `record_check` with the run and the record below as `record`. It runs the save in a transaction that rolls back, so nothing is
|
|
45
|
+
written: it refuses what the save would refuse, and says what would become of each record (`would be active`, `would stay a candidate` with
|
|
46
|
+
the reason, `would be superseded`, `would be quarantined`). Errors refuse the save; fix and check again. Warnings say what will be
|
|
45
47
|
left out, quarantined, or kept as a candidate, and why. An anchor warning (a path not in the working tree, with near paths; a directory; a
|
|
46
48
|
symbol not in the file) means fix the anchor and check again; keep it only when you know it is right
|
|
47
49
|
5. **Save**: `record_save` with the same run and record. A run saves once
|