@skyf0xx/hedgehog 6.3.0 → 6.3.1
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/bin/cli.mjs +41 -7
- package/package.json +1 -1
- package/src/db/debt.mjs +67 -5
- package/src/db/next.mjs +25 -1
- package/src/db/notes.mjs +22 -5
- package/src/db/rebuild.mjs +19 -2
- package/src/db/schema.mjs +19 -5
- package/src/hosts/gemini/gemini-extension.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
} from '../src/db/requires.mjs';
|
|
60
60
|
import { whyPath, formatWhy } from '../src/db/why.mjs';
|
|
61
61
|
import { addFriction, listFriction } from '../src/db/friction.mjs';
|
|
62
|
-
import { addDebt, listDebt } from '../src/db/debt.mjs';
|
|
62
|
+
import { addDebt, listDebt, resolveDebt } from '../src/db/debt.mjs';
|
|
63
63
|
import { addDecision, listDecisions } from '../src/db/decision.mjs';
|
|
64
64
|
import {
|
|
65
65
|
shouldPromptForStar,
|
|
@@ -663,7 +663,8 @@ ${bold('Usage')}
|
|
|
663
663
|
npx @skyf0xx/hedgehog friction add "<note>" log a friction note [--task <task-id>]
|
|
664
664
|
npx @skyf0xx/hedgehog friction list list logged friction, oldest first
|
|
665
665
|
npx @skyf0xx/hedgehog debt add <task-id> "<note>" declare debt that lands in dependent tasks' packets
|
|
666
|
-
npx @skyf0xx/hedgehog debt list [<task-id>]
|
|
666
|
+
npx @skyf0xx/hedgehog debt list [<task-id>] [--all] list open debt, oldest first (--all includes resolved)
|
|
667
|
+
npx @skyf0xx/hedgehog debt resolve <debt-id> --reason "<why>" mark a debt note resolved
|
|
667
668
|
npx @skyf0xx/hedgehog decision add <task-id> "<note>" declare a decision that lands in dependent tasks' packets
|
|
668
669
|
npx @skyf0xx/hedgehog decision list [<task-id>] list declared decisions, oldest first
|
|
669
670
|
npx @skyf0xx/hedgehog db migrate bring the graph's schema up to the latest version
|
|
@@ -3844,28 +3845,61 @@ async function debtCommand(args) {
|
|
|
3844
3845
|
}
|
|
3845
3846
|
|
|
3846
3847
|
if (sub === 'list') {
|
|
3847
|
-
const
|
|
3848
|
+
const includeResolved = args.includes('--all') || args.includes('--resolved');
|
|
3849
|
+
const taskId = args.slice(1).find((a) => !a.startsWith('--'));
|
|
3848
3850
|
const db = openDb();
|
|
3849
3851
|
let entries;
|
|
3850
3852
|
try {
|
|
3851
|
-
entries = listDebt(db, taskId);
|
|
3853
|
+
entries = listDebt(db, taskId, { includeResolved });
|
|
3852
3854
|
} finally {
|
|
3853
3855
|
db.close();
|
|
3854
3856
|
}
|
|
3855
3857
|
|
|
3856
3858
|
if (entries.length === 0) {
|
|
3857
|
-
console.log(`${dim('No debt declared.')}\n`);
|
|
3859
|
+
console.log(`${dim(includeResolved ? 'No debt declared.' : 'No open debt.')}\n`);
|
|
3858
3860
|
return;
|
|
3859
3861
|
}
|
|
3860
3862
|
for (const entry of entries) {
|
|
3861
3863
|
console.log(`#${entry.id} ${dim(entry.loggedAt)} ${bold(entry.taskId)}`);
|
|
3862
|
-
console.log(` ${entry.note}
|
|
3864
|
+
console.log(` ${entry.note}`);
|
|
3865
|
+
if (entry.resolvedAt) {
|
|
3866
|
+
console.log(` ${green('resolved')} ${dim(entry.resolvedAt)} — ${entry.resolvedReason}`);
|
|
3867
|
+
}
|
|
3868
|
+
console.log('');
|
|
3869
|
+
}
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
if (sub === 'resolve') {
|
|
3874
|
+
const debtId = args[1];
|
|
3875
|
+
const reasonIdx = args.indexOf('--reason');
|
|
3876
|
+
const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : undefined;
|
|
3877
|
+
|
|
3878
|
+
if (!debtId || debtId.startsWith('--') || !reason) {
|
|
3879
|
+
console.error(`${red('Usage:')} hedgehog debt resolve <debt-id> --reason "<why>"\n`);
|
|
3880
|
+
process.exitCode = 1;
|
|
3881
|
+
return;
|
|
3863
3882
|
}
|
|
3883
|
+
|
|
3884
|
+
const db = openDb();
|
|
3885
|
+
let result;
|
|
3886
|
+
try {
|
|
3887
|
+
result = await resolveDebt(db, { debtId: Number(debtId), reason });
|
|
3888
|
+
} catch (err) {
|
|
3889
|
+
console.error(`${red('Failed to resolve debt:')} ${err.message}\n`);
|
|
3890
|
+
process.exitCode = 1;
|
|
3891
|
+
return;
|
|
3892
|
+
} finally {
|
|
3893
|
+
db.close();
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
console.log(` ${green('resolved')} #${result.id} ${bold(result.taskId)}`);
|
|
3897
|
+
console.log(` ${dim(result.note)}`);
|
|
3864
3898
|
return;
|
|
3865
3899
|
}
|
|
3866
3900
|
|
|
3867
3901
|
console.error(
|
|
3868
|
-
`${red('Unknown debt subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog debt add <task-id> "<note>"\n or: hedgehog debt list [<task-id>]\n`,
|
|
3902
|
+
`${red('Unknown debt subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog debt add <task-id> "<note>"\n or: hedgehog debt list [<task-id>] [--all]\n or: hedgehog debt resolve <debt-id> --reason "<why>"\n`,
|
|
3869
3903
|
);
|
|
3870
3904
|
process.exitCode = 1;
|
|
3871
3905
|
}
|
package/package.json
CHANGED
package/src/db/debt.mjs
CHANGED
|
@@ -58,14 +58,25 @@ export async function addDebt(db, { taskId, note }, notesDir = undefined) {
|
|
|
58
58
|
return { id: Number(result.lastInsertRowid), taskId, note };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Every debt row, oldest first, optionally narrowed to one task.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
// Every debt row, oldest first, optionally narrowed to one task. Resolved
|
|
62
|
+
// rows are excluded by default — `debt list` is meant to answer "what's
|
|
63
|
+
// still open", the same way `reconcile list` never re-surfaces a task once
|
|
64
|
+
// it has closed — and included when `includeResolved` is set (`--all`).
|
|
65
|
+
export function listDebt(db, taskId, { includeResolved = false } = {}) {
|
|
66
|
+
const conditions = [];
|
|
67
|
+
const params = [];
|
|
68
|
+
if (taskId) {
|
|
69
|
+
conditions.push('task_id = ?');
|
|
70
|
+
params.push(taskId);
|
|
71
|
+
}
|
|
72
|
+
if (!includeResolved) conditions.push('resolved_at IS NULL');
|
|
73
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
65
74
|
try {
|
|
66
75
|
return db
|
|
67
76
|
.prepare(
|
|
68
|
-
`SELECT id, task_id AS taskId, note, logged_at AS loggedAt
|
|
77
|
+
`SELECT id, task_id AS taskId, note, logged_at AS loggedAt,
|
|
78
|
+
resolved_at AS resolvedAt, resolved_reason AS resolvedReason
|
|
79
|
+
FROM debt ${where} ORDER BY id ASC`,
|
|
69
80
|
)
|
|
70
81
|
.all(...params);
|
|
71
82
|
} catch {
|
|
@@ -73,3 +84,54 @@ export function listDebt(db, taskId) {
|
|
|
73
84
|
return [];
|
|
74
85
|
}
|
|
75
86
|
}
|
|
87
|
+
|
|
88
|
+
// Count of open (unresolved) debt across the whole graph — the one-line
|
|
89
|
+
// figure `hedgehog next`/`hedgehog claim` surface so debt doesn't
|
|
90
|
+
// accumulate silently with nothing prompting a look at it.
|
|
91
|
+
export function openDebtCount(db) {
|
|
92
|
+
try {
|
|
93
|
+
const row = db.prepare('SELECT COUNT(*) AS n FROM debt WHERE resolved_at IS NULL').get();
|
|
94
|
+
return row.n;
|
|
95
|
+
} catch {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Resolves one debt row by id: marks it resolved in the DB and writes the
|
|
101
|
+
// committed record behind it — same ordering as addDebt (file before row)
|
|
102
|
+
// and the same reason: if the write fails, nothing has been resolved on a
|
|
103
|
+
// fact that would not survive the next rebuild.
|
|
104
|
+
//
|
|
105
|
+
// The committed note is keyed by the debt row's own (task_id, note,
|
|
106
|
+
// logged_at) rather than its DB `id`, because that id is a fresh
|
|
107
|
+
// autoincrement every time rebuild.mjs#replayNotes re-inserts debt rows —
|
|
108
|
+
// it is not stable across a rebuild, so a resolution referencing it would
|
|
109
|
+
// point at nothing once replayed. The triple already committed for the
|
|
110
|
+
// debt note itself is the one part of its identity that *is* stable.
|
|
111
|
+
export async function resolveDebt(db, { debtId, reason }, notesDir = undefined) {
|
|
112
|
+
applySchema(db);
|
|
113
|
+
|
|
114
|
+
if (!debtId) throw new Error('debt resolve requires a debt id');
|
|
115
|
+
if (!reason) throw new Error('debt resolve requires a --reason');
|
|
116
|
+
|
|
117
|
+
const row = db
|
|
118
|
+
.prepare('SELECT id, task_id AS taskId, note, logged_at AS loggedAt, resolved_at AS resolvedAt FROM debt WHERE id = ?')
|
|
119
|
+
.get(debtId);
|
|
120
|
+
if (!row) throw new Error(`no such debt: #${debtId}`);
|
|
121
|
+
if (row.resolvedAt) throw new Error(`debt #${debtId} is already resolved`);
|
|
122
|
+
|
|
123
|
+
const resolvedAt = new Date().toISOString();
|
|
124
|
+
await appendNote(
|
|
125
|
+
row.taskId,
|
|
126
|
+
{ kind: 'debt-resolve', resolves: row.loggedAt, reason, loggedAt: resolvedAt },
|
|
127
|
+
notesDir,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
db.prepare('UPDATE debt SET resolved_at = ?, resolved_reason = ? WHERE id = ?').run(
|
|
131
|
+
resolvedAt,
|
|
132
|
+
reason,
|
|
133
|
+
debtId,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return { id: row.id, taskId: row.taskId, note: row.note, resolvedAt, resolvedReason: reason };
|
|
137
|
+
}
|
package/src/db/next.mjs
CHANGED
|
@@ -105,7 +105,9 @@ function loadInheritedDebt(db, taskId) {
|
|
|
105
105
|
try {
|
|
106
106
|
return db
|
|
107
107
|
.prepare(
|
|
108
|
-
`SELECT task_id AS taskId, note FROM debt
|
|
108
|
+
`SELECT task_id AS taskId, note FROM debt
|
|
109
|
+
WHERE task_id IN (${placeholders}) AND resolved_at IS NULL
|
|
110
|
+
ORDER BY id ASC`,
|
|
109
111
|
)
|
|
110
112
|
.all(...upstream);
|
|
111
113
|
} catch {
|
|
@@ -113,6 +115,20 @@ function loadInheritedDebt(db, taskId) {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
// Total open (unresolved) debt across the whole graph — not just this
|
|
119
|
+
// task's ancestors — so a packet can say plainly that debt exists even
|
|
120
|
+
// where none of it happens to be inherited here. Tolerates a `debt` table
|
|
121
|
+
// or `resolved_at` column that doesn't exist yet, the same way
|
|
122
|
+
// loadInheritedDebt does.
|
|
123
|
+
function loadOpenDebtCount(db) {
|
|
124
|
+
try {
|
|
125
|
+
const row = db.prepare('SELECT COUNT(*) AS n FROM debt WHERE resolved_at IS NULL').get();
|
|
126
|
+
return row.n;
|
|
127
|
+
} catch {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
// Decisions declared by anything this task inherits from (see
|
|
117
133
|
// decision.mjs) — same upstream walk as loadInheritedDebt, same tolerance
|
|
118
134
|
// for a `decisions` table that doesn't exist yet on an older graph.
|
|
@@ -181,6 +197,7 @@ function assemblePacket(db, task) {
|
|
|
181
197
|
const incompleteDeps = incompleteDependencies(db, task.id);
|
|
182
198
|
const inheritedDebt = loadInheritedDebt(db, task.id);
|
|
183
199
|
const inheritedDecisions = loadInheritedDecisions(db, task.id);
|
|
200
|
+
const openDebtCount = loadOpenDebtCount(db);
|
|
184
201
|
|
|
185
202
|
return {
|
|
186
203
|
task,
|
|
@@ -190,6 +207,7 @@ function assemblePacket(db, task) {
|
|
|
190
207
|
incompleteDeps,
|
|
191
208
|
inheritedDebt,
|
|
192
209
|
inheritedDecisions,
|
|
210
|
+
openDebtCount,
|
|
193
211
|
};
|
|
194
212
|
}
|
|
195
213
|
|
|
@@ -484,6 +502,7 @@ export function formatPacket(packet, statusLine, coreId = null, exists = null) {
|
|
|
484
502
|
incompleteDeps = [],
|
|
485
503
|
inheritedDebt = [],
|
|
486
504
|
inheritedDecisions = [],
|
|
505
|
+
openDebtCount = 0,
|
|
487
506
|
} = packet;
|
|
488
507
|
const scopeGlobs = JSON.parse(task.scope_globs);
|
|
489
508
|
const firstArrival = firstArrivalPackages(task, exists);
|
|
@@ -521,6 +540,11 @@ export function formatPacket(packet, statusLine, coreId = null, exists = null) {
|
|
|
521
540
|
lines.push(` ! ${entry.taskId} ${entry.note}`);
|
|
522
541
|
}
|
|
523
542
|
}
|
|
543
|
+
if (openDebtCount > 0) {
|
|
544
|
+
lines.push(
|
|
545
|
+
` ${openDebtCount} open debt note(s) across the whole graph — \`hedgehog debt list --all\` to see them all`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
524
548
|
lines.push('');
|
|
525
549
|
lines.push('INHERITED DECISIONS');
|
|
526
550
|
if (inheritedDecisions.length === 0) {
|
package/src/db/notes.mjs
CHANGED
|
@@ -45,12 +45,21 @@ function validateNotesFile(record, path) {
|
|
|
45
45
|
if (entry === null || typeof entry !== 'object') {
|
|
46
46
|
throw new Error(`${path}: notes file "${task}" has a non-object entry in notes`);
|
|
47
47
|
}
|
|
48
|
-
if (entry.kind !== 'debt' && entry.kind !== 'decision') {
|
|
48
|
+
if (entry.kind !== 'debt' && entry.kind !== 'decision' && entry.kind !== 'debt-resolve') {
|
|
49
49
|
throw new Error(
|
|
50
|
-
`${path}: notes file "${task}" has an entry with kind "${entry.kind}" — expected "debt" or "
|
|
50
|
+
`${path}: notes file "${task}" has an entry with kind "${entry.kind}" — expected "debt", "decision", or "debt-resolve"`,
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
|
-
if (
|
|
53
|
+
if (entry.kind === 'debt-resolve') {
|
|
54
|
+
if (!entry.resolves || typeof entry.resolves !== 'string') {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${path}: notes file "${task}" has a "debt-resolve" entry with no "resolves" (the logged_at of the debt note it closes)`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (!entry.reason || typeof entry.reason !== 'string') {
|
|
60
|
+
throw new Error(`${path}: notes file "${task}" has a "debt-resolve" entry with no "reason"`);
|
|
61
|
+
}
|
|
62
|
+
} else if (!entry.note || typeof entry.note !== 'string') {
|
|
54
63
|
throw new Error(`${path}: notes file "${task}" has an entry with no "note" (string)`);
|
|
55
64
|
}
|
|
56
65
|
if (!entry.logged_at || typeof entry.logged_at !== 'string') {
|
|
@@ -92,7 +101,7 @@ export async function loadNotes(notesDir = NOTES_DIR) {
|
|
|
92
101
|
// never leaves a half-written file for loadNotes to trip on —
|
|
93
102
|
// reconcile.mjs#writeReconciledFile's pattern, applied to a file that
|
|
94
103
|
// grows instead of one written once.
|
|
95
|
-
export async function appendNote(taskId, { kind, note, loggedAt }, notesDir = NOTES_DIR) {
|
|
104
|
+
export async function appendNote(taskId, { kind, note, loggedAt, resolves, reason }, notesDir = NOTES_DIR) {
|
|
96
105
|
const path = notesFilePath(taskId, notesDir);
|
|
97
106
|
|
|
98
107
|
let existing = [];
|
|
@@ -103,9 +112,17 @@ export async function appendNote(taskId, { kind, note, loggedAt }, notesDir = NO
|
|
|
103
112
|
if (!err || err.code !== 'ENOENT') throw err;
|
|
104
113
|
}
|
|
105
114
|
|
|
115
|
+
// `debt-resolve` entries carry `resolves`/`reason` instead of `note` —
|
|
116
|
+
// omit `note` entirely rather than writing it as undefined/null, so a
|
|
117
|
+
// resolve entry's shape matches what validateNotesFile expects back.
|
|
118
|
+
const entry =
|
|
119
|
+
kind === 'debt-resolve'
|
|
120
|
+
? { kind, resolves, reason, logged_at: loggedAt }
|
|
121
|
+
: { kind, note, logged_at: loggedAt };
|
|
122
|
+
|
|
106
123
|
const record = {
|
|
107
124
|
task: taskId.toUpperCase(),
|
|
108
|
-
notes: [...existing,
|
|
125
|
+
notes: [...existing, entry],
|
|
109
126
|
};
|
|
110
127
|
|
|
111
128
|
await mkdir(notesDir, { recursive: true });
|
package/src/db/rebuild.mjs
CHANGED
|
@@ -110,23 +110,40 @@ function replayNotes(db, notesByTask) {
|
|
|
110
110
|
const insertDecision = db.prepare(
|
|
111
111
|
'INSERT INTO decisions (task_id, note, logged_at) VALUES (?, ?, ?)',
|
|
112
112
|
);
|
|
113
|
+
const resolveDebtRow = db.prepare(
|
|
114
|
+
'UPDATE debt SET resolved_at = ?, resolved_reason = ? WHERE task_id = ? AND logged_at = ? AND resolved_at IS NULL',
|
|
115
|
+
);
|
|
113
116
|
|
|
114
117
|
const orphaned = [];
|
|
118
|
+
// Two passes: every `debt`/`decision` entry inserted first, then every
|
|
119
|
+
// `debt-resolve` entry applied — a resolve entry can appear anywhere
|
|
120
|
+
// after its debt entry in the same file, but the row it references must
|
|
121
|
+
// already exist for the UPDATE to find it.
|
|
115
122
|
for (const [taskId, notes] of notesByTask) {
|
|
116
123
|
if (taskExists.get(taskId) === undefined) {
|
|
117
124
|
for (const entry of notes) {
|
|
118
|
-
orphaned.push({ kind: entry.kind, taskId, note: entry.note });
|
|
125
|
+
if (entry.kind !== 'debt-resolve') orphaned.push({ kind: entry.kind, taskId, note: entry.note });
|
|
119
126
|
}
|
|
120
127
|
continue;
|
|
121
128
|
}
|
|
122
129
|
for (const entry of notes) {
|
|
123
130
|
if (entry.kind === 'debt') {
|
|
124
131
|
insertDebt.run(taskId, entry.note, entry.logged_at);
|
|
125
|
-
} else {
|
|
132
|
+
} else if (entry.kind === 'decision') {
|
|
126
133
|
insertDecision.run(taskId, entry.note, entry.logged_at);
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
}
|
|
137
|
+
for (const [taskId, notes] of notesByTask) {
|
|
138
|
+
if (taskExists.get(taskId) === undefined) continue;
|
|
139
|
+
for (const entry of notes) {
|
|
140
|
+
if (entry.kind !== 'debt-resolve') continue;
|
|
141
|
+
const result = resolveDebtRow.run(entry.logged_at, entry.reason, taskId, entry.resolves);
|
|
142
|
+
if (result.changes === 0) {
|
|
143
|
+
orphaned.push({ kind: entry.kind, taskId, note: `resolve for ${entry.resolves}` });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
130
147
|
return orphaned;
|
|
131
148
|
}
|
|
132
149
|
|
package/src/db/schema.mjs
CHANGED
|
@@ -103,10 +103,12 @@ CREATE TABLE IF NOT EXISTS verifications (
|
|
|
103
103
|
-- every task that (transitively) depends on task_id, so the limitation
|
|
104
104
|
-- travels down the chain the same way the dependency does.
|
|
105
105
|
CREATE TABLE IF NOT EXISTS debt (
|
|
106
|
-
id
|
|
107
|
-
task_id
|
|
108
|
-
note
|
|
109
|
-
logged_at
|
|
106
|
+
id INTEGER PRIMARY KEY,
|
|
107
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
108
|
+
note TEXT NOT NULL,
|
|
109
|
+
logged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
110
|
+
resolved_at TEXT,
|
|
111
|
+
resolved_reason TEXT
|
|
110
112
|
);
|
|
111
113
|
|
|
112
114
|
-- Declared decision: a note one task leaves for the tasks that inherit
|
|
@@ -176,7 +178,7 @@ export function ensureTaskColumns(db) {
|
|
|
176
178
|
// hand-set past what MIGRATIONS actually covers, since runMigrations
|
|
177
179
|
// trusts this number to mean "every migration through this version has
|
|
178
180
|
// run."
|
|
179
|
-
export const CURRENT_SCHEMA_VERSION =
|
|
181
|
+
export const CURRENT_SCHEMA_VERSION = 3;
|
|
180
182
|
|
|
181
183
|
// Forward migrations, applied in order to bring a graph's user_version up
|
|
182
184
|
// to CURRENT_SCHEMA_VERSION. Unlike the CREATE TABLE IF NOT EXISTS /
|
|
@@ -209,6 +211,18 @@ const MIGRATIONS = [
|
|
|
209
211
|
`);
|
|
210
212
|
},
|
|
211
213
|
},
|
|
214
|
+
{
|
|
215
|
+
version: 3,
|
|
216
|
+
// A `debt` row with no `resolved_at` reads back as unresolved, so an
|
|
217
|
+
// existing graph's rows default to open the moment this column exists.
|
|
218
|
+
migrate: (db) => {
|
|
219
|
+
const existing = new Set(db.prepare('PRAGMA table_info(debt)').all().map((row) => row.name));
|
|
220
|
+
if (!existing.has('resolved_at')) db.exec('ALTER TABLE debt ADD COLUMN resolved_at TEXT');
|
|
221
|
+
if (!existing.has('resolved_reason')) {
|
|
222
|
+
db.exec('ALTER TABLE debt ADD COLUMN resolved_reason TEXT');
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
},
|
|
212
226
|
];
|
|
213
227
|
|
|
214
228
|
// Brings a graph's `PRAGMA user_version` up to CURRENT_SCHEMA_VERSION,
|