limn-review 0.1.146 → 0.1.148
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/ai/effort.js +3 -1
- package/dist/ai/providers/claude.js +77 -0
- package/dist/api/plugins/rate-limit.js +14 -2
- package/dist/config.js +10 -4
- package/dist/data-dir.js +22 -1
- package/dist/db/erase-account.js +26 -0
- package/dist/db/export-account.js +42 -0
- package/dist/db/instance-id.js +38 -0
- package/dist/db/migrations/0139_instance_meta.sql +8 -0
- package/dist/db/migrations/0140_reference_doc_wiki.sql +62 -0
- package/dist/db/migrations/0141_tracker_ticket_comments.sql +20 -0
- package/dist/db/migrations/0142_ticket_review_comments_seen.sql +4 -0
- package/dist/db/migrations/0143_reference_doc_figures.sql +25 -0
- package/dist/db/migrations/0144_workspace_tracker_notion.sql +3 -0
- package/dist/db/migrations/meta/_journal.json +42 -0
- package/dist/db/queries.js +3 -0
- package/dist/db/retention.js +3 -0
- package/dist/db/schema.sqlite.js +156 -1
- package/dist/db/ticket-comments-prune.js +19 -0
- package/dist/reference-docs/chat-quotes.js +2 -2
- package/dist/reference-docs/context.js +6 -1
- package/dist/reference-docs/convert.js +37 -7
- package/dist/reference-docs/diagram-outline.js +236 -0
- package/dist/reference-docs/figure-cache.js +34 -0
- package/dist/reference-docs/figure-describer.js +91 -0
- package/dist/reference-docs/figure-fetch.js +66 -0
- package/dist/reference-docs/figures.js +317 -0
- package/dist/reference-docs/library.js +71 -21
- package/dist/reference-docs/manager.js +201 -63
- package/dist/reference-docs/owner.js +144 -0
- package/dist/reference-docs/paths.js +14 -2
- package/dist/reference-docs/prompts.js +32 -0
- package/dist/reference-docs/review-refs.js +10 -2
- package/dist/reference-docs/routes.js +17 -1
- package/dist/reference-docs/startup.js +16 -5
- package/dist/reference-docs/store.js +106 -1
- package/dist/reference-docs/wiki/build.js +179 -0
- package/dist/reference-docs/wiki/confluence-storage.js +454 -0
- package/dist/reference-docs/wiki/confluence.js +429 -0
- package/dist/reference-docs/wiki/credentials.js +166 -0
- package/dist/reference-docs/wiki/fake-wiki.js +99 -0
- package/dist/reference-docs/wiki/notion-blocks.js +270 -0
- package/dist/reference-docs/wiki/notion.js +450 -0
- package/dist/reference-docs/wiki/pages.js +103 -0
- package/dist/reference-docs/wiki/refresh.js +103 -0
- package/dist/reference-docs/wiki/register.js +11 -0
- package/dist/reference-docs/wiki/routes.js +199 -0
- package/dist/reference-docs/wiki/source.js +91 -0
- package/dist/reference-docs/wiki/sync.js +206 -0
- package/dist/review/agentic.js +3 -0
- package/dist/review/story-docs/agent.js +12 -6
- package/dist/review/story-docs/fingerprint.js +18 -1
- package/dist/review/story-docs/inputs.js +12 -2
- package/dist/review/story-docs/manager.js +67 -31
- package/dist/review/story-docs/persist.js +1 -0
- package/dist/review/story-docs/prompts.js +92 -20
- package/dist/review/story-docs/routes.js +16 -2
- package/dist/review/story-docs/shortlist.js +120 -6
- package/dist/review/ticket-review/manager.js +9 -2
- package/dist/review/ticket-review/persist.js +2 -0
- package/dist/review/ticket-review/prepare.js +13 -1
- package/dist/review/ticket-review/prompts.js +48 -3
- package/dist/review/ticket-review/routes.js +9 -1
- package/dist/shared/claude-review.js +28 -2
- package/dist/shared/index.js +3 -0
- package/dist/shared/notion-tracker.js +169 -0
- package/dist/shared/reference-docs-quote.js +26 -0
- package/dist/shared/reference-docs-wiki.js +126 -0
- package/dist/shared/reference-docs.js +13 -0
- package/dist/shared/story-docs-alignment.js +20 -9
- package/dist/shared/ticket-comments.js +33 -0
- package/dist/shared/ticket-refs.js +10 -0
- package/dist/shared/types.js +4 -3
- package/dist/tracker/calls.js +19 -4
- package/dist/tracker/clip.js +17 -0
- package/dist/tracker/comments.js +155 -0
- package/dist/tracker/github/reader.js +36 -3
- package/dist/tracker/jira/client.js +63 -1
- package/dist/tracker/jira/fetch.js +9 -3
- package/dist/tracker/linear/client.js +40 -2
- package/dist/tracker/links.js +9 -3
- package/dist/tracker/manual-links.js +41 -3
- package/dist/tracker/notion/adapter.js +33 -0
- package/dist/tracker/notion/blocks.js +201 -0
- package/dist/tracker/notion/client.js +164 -0
- package/dist/tracker/notion/reader.js +300 -0
- package/dist/tracker/notion/schema.js +180 -0
- package/dist/tracker/peers.js +18 -0
- package/dist/tracker/registry.js +4 -1
- package/dist/tracker/routes.js +64 -7
- package/dist/tracker/settings.js +31 -1
- package/dist/tracker/sprint/notion.js +265 -0
- package/dist/tracker/sprint/read.js +13 -5
- package/dist/tracker/store.js +9 -0
- package/dist/tracker/stories.js +2 -13
- package/dist/tracker/ticket-source.js +4 -0
- package/dist/tracker/worker.js +10 -0
- package/package.json +1 -1
- package/public/assets/{AiFixTab-DUxlYxj1.js → AiFixTab-C4qSF6Rh.js} +3 -3
- package/public/assets/ClaudeReviewTab-iR4WZm87.js +20 -0
- package/public/assets/{PrBotBehaviourTab-Cvxgm35A.js → PrBotBehaviourTab-D7Lnwfm8.js} +1 -1
- package/public/assets/index-C-ZWx5Hz.css +1 -0
- package/public/assets/index-ZWr-Rhuj.js +82 -0
- package/public/index.html +2 -2
- package/public/assets/ClaudeReviewTab-Dvb8i10G.js +0 -20
- package/public/assets/index-CTwtlPvo.css +0 -1
- package/public/assets/index-WwIuB9lr.js +0 -82
package/dist/ai/effort.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
// exactly the same runs as the Settings control (REVIEW_EFFORT IS the control, locked), so "Set by
|
|
12
12
|
// REVIEW_EFFORT" under it is the whole story. Every other agent kind (CI review, ticket review,
|
|
13
13
|
// diagrams, Check threads, chats, Resolve with Claude) runs at its provider's own fixed depth
|
|
14
|
-
// (`defaultRunEffort`), which reads no environment
|
|
14
|
+
// (`defaultRunEffort`), which reads no environment — except Check against docs, whose read is pinned to
|
|
15
|
+
// 'low' on both providers (review/story-docs/prompts.ts STORY_DOCS_EFFORT: the sections' text is in the
|
|
16
|
+
// prompt, so it is reading, not search). Still a fixed depth; this setting does not reach it.
|
|
15
17
|
//
|
|
16
18
|
// A worktree run sends the level. A diff-only review runs ONE STEP LOWER, but never below what Default
|
|
17
19
|
// runs it at — unless the chosen level is itself below that, when it runs at the chosen level
|
|
@@ -273,6 +273,82 @@ async function agentComplete(model, opts, credential) {
|
|
|
273
273
|
}
|
|
274
274
|
return { text: trimmed, usage: { inputTokens, outputTokens } };
|
|
275
275
|
}
|
|
276
|
+
// ---- describeImage (vision; docs/REFERENCE-DOCS.md § Figures) ------------------------------------
|
|
277
|
+
//
|
|
278
|
+
// The cheap completion's twin with ONE image beside the prompt: the same ladder, the same request shape
|
|
279
|
+
// (low effort + thinking headroom on the adaptive model), no tools, one turn. On the login the image
|
|
280
|
+
// rides a streamed user message (the Agent SDK's only way to send content blocks); on the key it is a
|
|
281
|
+
// plain Messages API call.
|
|
282
|
+
async function describeImage(req) {
|
|
283
|
+
const model = req.model ?? CLAUDE_MODEL_FOR_ROLE.fast;
|
|
284
|
+
const credential = resolveAiCredential();
|
|
285
|
+
const route = cheapRoute(credential);
|
|
286
|
+
if (route === 'none')
|
|
287
|
+
throw new Error(NO_CLAUDE_AUTH_MESSAGE);
|
|
288
|
+
const content = [
|
|
289
|
+
{ type: 'image', source: { type: 'base64', media_type: req.image.mime, data: req.image.base64 } },
|
|
290
|
+
{ type: 'text', text: req.prompt },
|
|
291
|
+
];
|
|
292
|
+
const shape = cheapRequestShape(model, req.maxTokens ?? 400);
|
|
293
|
+
if (route === 'api' && credential.route === 'api_key') {
|
|
294
|
+
const { default: Anthropic } = await loadAnthropicSdk();
|
|
295
|
+
const client = new Anthropic({ apiKey: credential.apiKey });
|
|
296
|
+
const resp = await client.messages.create({
|
|
297
|
+
model,
|
|
298
|
+
max_tokens: shape.maxTokens,
|
|
299
|
+
...(shape.effort ? { output_config: { effort: shape.effort } } : {}),
|
|
300
|
+
system: req.system,
|
|
301
|
+
messages: [{ role: 'user', content }],
|
|
302
|
+
});
|
|
303
|
+
const blocks = resp.content;
|
|
304
|
+
const text = (Array.isArray(blocks) ? blocks : [])
|
|
305
|
+
.map((b) => {
|
|
306
|
+
const block = b;
|
|
307
|
+
return block?.type === 'text' && typeof block.text === 'string' ? block.text : '';
|
|
308
|
+
})
|
|
309
|
+
.join('');
|
|
310
|
+
return { text: text.trim(), usage: { inputTokens: resp.usage.input_tokens, outputTokens: resp.usage.output_tokens }, model };
|
|
311
|
+
}
|
|
312
|
+
const { query } = await loadAgentSdk();
|
|
313
|
+
async function* oneMessage() {
|
|
314
|
+
yield { type: 'user', message: { role: 'user', content }, parent_tool_use_id: null, session_id: '' };
|
|
315
|
+
}
|
|
316
|
+
const q = query({
|
|
317
|
+
prompt: oneMessage(),
|
|
318
|
+
options: {
|
|
319
|
+
model,
|
|
320
|
+
...(shape.effort ? { effort: shape.effort } : {}),
|
|
321
|
+
systemPrompt: req.system,
|
|
322
|
+
allowedTools: [],
|
|
323
|
+
maxTurns: 1,
|
|
324
|
+
permissionMode: 'bypassPermissions',
|
|
325
|
+
settingSources: [],
|
|
326
|
+
...ISOLATED_MCP,
|
|
327
|
+
hooks: { PreToolUse: [ownToolsOnlyHook([])] },
|
|
328
|
+
...claudeExecutableOptions(),
|
|
329
|
+
env: agentSdkEnv(process.env, credential),
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
let text = '';
|
|
333
|
+
let inputTokens = 0;
|
|
334
|
+
let outputTokens = 0;
|
|
335
|
+
for await (const message of q) {
|
|
336
|
+
if (message.type === 'assistant') {
|
|
337
|
+
const inner = (message.message ?? {});
|
|
338
|
+
for (const b of inner.content ?? [])
|
|
339
|
+
if (b?.type === 'text' && typeof b.text === 'string')
|
|
340
|
+
text += b.text;
|
|
341
|
+
}
|
|
342
|
+
else if (message.type === 'result') {
|
|
343
|
+
const usage = (message.usage ?? {});
|
|
344
|
+
inputTokens = usage.input_tokens ?? inputTokens;
|
|
345
|
+
outputTokens = usage.output_tokens ?? outputTokens;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!text.trim())
|
|
349
|
+
throw new Error('The image description came back empty.');
|
|
350
|
+
return { text: text.trim(), usage: { inputTokens, outputTokens }, model };
|
|
351
|
+
}
|
|
276
352
|
// ---- detect ----------------------------------------------------------------------------------
|
|
277
353
|
async function detect() {
|
|
278
354
|
if (!config.aiEnabled) {
|
|
@@ -346,6 +422,7 @@ export const claudeProvider = {
|
|
|
346
422
|
resolveModel,
|
|
347
423
|
runAgent,
|
|
348
424
|
complete,
|
|
425
|
+
describeImage,
|
|
349
426
|
detect,
|
|
350
427
|
credentialRoute: () => resolveAiCredential().route,
|
|
351
428
|
usage: {
|
|
@@ -116,11 +116,13 @@ function tierFor(method, path) {
|
|
|
116
116
|
// the customer's Jira with the saved token.
|
|
117
117
|
// GET /api/workspaces/:id/tracker/linear-check the Linear connection check — one call to
|
|
118
118
|
// Linear with the saved key.
|
|
119
|
-
//
|
|
119
|
+
// GET /api/workspaces/:id/tracker/notion-schema the Notion mapping read — the saved database's
|
|
120
|
+
// schema (+ ≤ 3 related databases) with the saved token.
|
|
121
|
+
// All four take the 60/min `search` bucket, the "one upstream call per request" tier. The plain
|
|
120
122
|
// `GET …/tracker` is DB-only and stays on `read` with the rest of the family.
|
|
121
123
|
if (method === 'PUT' && /^\/api\/workspaces\/\d+\/tracker$/.test(path))
|
|
122
124
|
return [TIERS.search, TIERS.read];
|
|
123
|
-
if (!mutating && /^\/api\/workspaces\/\d+\/tracker\/(jira-fields|linear-check)$/.test(path)) {
|
|
125
|
+
if (!mutating && /^\/api\/workspaces\/\d+\/tracker\/(jira-fields|linear-check|notion-schema)$/.test(path)) {
|
|
124
126
|
return [TIERS.search, TIERS.read];
|
|
125
127
|
}
|
|
126
128
|
// SONARCLOUD (sonar/routes.ts): "Test connection" calls SonarCloud with the saved token (one call
|
|
@@ -128,6 +130,10 @@ function tierFor(method, path) {
|
|
|
128
130
|
// `…/sonar` are DB-only and fall to `read` on the next line, decided rather than inherited.
|
|
129
131
|
if (method === 'POST' && /^\/api\/workspaces\/\d+\/sonar\/check$/.test(path))
|
|
130
132
|
return [TIERS.search, TIERS.read];
|
|
133
|
+
// WIKI CREDENTIALS (reference-docs/wiki/routes.ts): "Test connection" is one read of the customer's
|
|
134
|
+
// Confluence with the saved token → `search`. GET/PUT `…/wiki-credentials` are DB-only → `read` below.
|
|
135
|
+
if (method === 'POST' && /^\/api\/workspaces\/\d+\/wiki-credentials\/check$/.test(path))
|
|
136
|
+
return [TIERS.search, TIERS.read];
|
|
131
137
|
// RUN AUTO REVIEWS NOW / RETRY (review/claude-review/auto-settings.ts): one press can queue up to a
|
|
132
138
|
// day's cap of agentic model runs on the reader's own Claude or Codex — `ai` + `aiHourly`, decided
|
|
133
139
|
// ABOVE the family's `read` line, by exact shape. The GET/PUT `…/auto-review` stay `read`.
|
|
@@ -420,6 +426,12 @@ function tierFor(method, path) {
|
|
|
420
426
|
}
|
|
421
427
|
if (method === 'POST' && /^\/api\/docs\/\d+\/rebuild$/.test(path))
|
|
422
428
|
return [TIERS.ai, TIERS.aiHourly];
|
|
429
|
+
// WIKI SOURCES (reference-docs/wiki/routes.ts): adding a Confluence page and "Refresh now" read the
|
|
430
|
+
// CUSTOMER'S Confluence quota with the saved token (a page, its tree, a CQL search) and may run the
|
|
431
|
+
// build's model → search + ai + ai_hourly. (The credentials routes sit with the workspace family above.)
|
|
432
|
+
if (method === 'POST' && (path === '/api/docs/wiki' || /^\/api\/docs\/\d+\/refresh$/.test(path))) {
|
|
433
|
+
return [TIERS.search, TIERS.ai, TIERS.aiHourly];
|
|
434
|
+
}
|
|
423
435
|
if (path === '/api/docs' || path.startsWith('/api/docs/'))
|
|
424
436
|
return [TIERS.read];
|
|
425
437
|
if (/^\/api\/workspaces\/\d+\/docs-settings$/.test(path))
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
-
import { migrateLegacyDataDir, resolveDataDir } from './data-dir.js';
|
|
3
|
+
import { defaultDocsDir, migrateLegacyDataDir, resolveDataDir } from './data-dir.js';
|
|
4
4
|
import { SPRINT_PATHS_BUDGET_USD_DEFAULT, SPRINT_PATHS_DAILY_CAP_DEFAULT, SPRINT_PATHS_MAX_TURNS_DEFAULT, } from './shared/index.js';
|
|
5
5
|
// apps/backend as the base for relative paths regardless of cwd.
|
|
6
6
|
const backendRoot = resolve(import.meta.dirname, '..');
|
|
@@ -53,6 +53,7 @@ if (!process.env.VITEST) {
|
|
|
53
53
|
// GitHub rate limit and contend on the SQLite write lock. DATABASE_URL / --db
|
|
54
54
|
// override this; a relative override resolves under apps/backend.
|
|
55
55
|
const rawDbUrl = process.env.DATABASE_URL ?? './data/pierre-review.sqlite';
|
|
56
|
+
const dbPath = isAbsolute(rawDbUrl) ? rawDbUrl : resolve(backendRoot, rawDbUrl);
|
|
56
57
|
// ---- Phase 2 real-time sync: adaptive polling (see docs/REALTIME-SYNC.md) ----
|
|
57
58
|
// DEFAULT ON. This is the PRIMARY sync strategy: cadence by activity bucket, plus a
|
|
58
59
|
// conditional REST probe whose 304 costs no rate limit, so it's both fresher on active repos
|
|
@@ -136,7 +137,7 @@ export const config = {
|
|
|
136
137
|
persistBodies: process.env.PERSIST_BODIES === 'true',
|
|
137
138
|
port: intFromEnv('PORT', 4000),
|
|
138
139
|
host: process.env.HOST ?? '127.0.0.1',
|
|
139
|
-
dbPath
|
|
140
|
+
dbPath,
|
|
140
141
|
backfillDays: intFromEnv('BACKFILL_DAYS', 90),
|
|
141
142
|
// First sync runs in two phases: a fast "foreground" window (matching the
|
|
142
143
|
// default timeline range) so the board is usable in seconds, then the rest of
|
|
@@ -419,8 +420,13 @@ export const config = {
|
|
|
419
420
|
sequenceDiagramDailyCap: intFromEnv('SEQUENCE_DIAGRAM_DAILY_CAP', 30),
|
|
420
421
|
// ---- Reference docs (the per-workspace library — reference-docs/, docs/REFERENCE-DOCS.md) ----
|
|
421
422
|
// Where the markdown tree lives: `<docsDir>/<accountId>/<workspaceId>/library/` (+ `sources/`).
|
|
422
|
-
// LIMN_DOCS_DIR overrides
|
|
423
|
-
|
|
423
|
+
// LIMN_DOCS_DIR overrides. Default: ONE ROOT PER DATABASE (data-dir.ts `defaultDocsDir`) —
|
|
424
|
+
// `<dataDir>/docs` for the data directory's own DB, `<dataDir>/docs/db-<hash>` for any other.
|
|
425
|
+
docsDir: process.env.LIMN_DOCS_DIR?.trim() || defaultDocsDir(dbPath, resolveDataDir()),
|
|
426
|
+
// The root every database shared before the split: a DB with another root moves its own
|
|
427
|
+
// workspace folders out of it ONCE (reference-docs/owner.ts `migrateLegacyDocs`). Never when
|
|
428
|
+
// LIMN_DOCS_DIR is set — the user chose where the library lives.
|
|
429
|
+
legacyDocsDir: process.env.LIMN_DOCS_DIR?.trim() ? null : join(resolveDataDir(), 'docs'),
|
|
424
430
|
// One Sonnet 5 build per document: its OWN spend and turn caps (a long PDF is read in 20-page
|
|
425
431
|
// slices and written out section by section). Env only — no route sets them.
|
|
426
432
|
docsBuildBudgetUsd: floatFromEnv('DOCS_BUILD_BUDGET_USD', 6),
|
package/dist/data-dir.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// user's history stranded. Instead the next boot simply tries the move again.
|
|
31
31
|
//
|
|
32
32
|
// No imports outside `node:*`, so cli.ts can load it before config.ts reads the environment.
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
33
34
|
import { existsSync, renameSync } from 'node:fs';
|
|
34
35
|
import { homedir } from 'node:os';
|
|
35
36
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
@@ -52,7 +53,27 @@ export function resolveDataDir(env = process.env, home = homedir()) {
|
|
|
52
53
|
return target;
|
|
53
54
|
}
|
|
54
55
|
/** The database file the CLI and the server open by default (the name predates the rename). */
|
|
55
|
-
const DB_FILE = 'pierre-review.sqlite';
|
|
56
|
+
export const DB_FILE = 'pierre-review.sqlite';
|
|
57
|
+
/**
|
|
58
|
+
* THE REFERENCE-DOCS ROOT FOR ONE DATABASE (when LIMN_DOCS_DIR is not set). ⚠ Two databases never
|
|
59
|
+
* share one: each one's boot janitor deletes every docs folder its own rows do not account for, so a
|
|
60
|
+
* shared root let a second backend (a worktree's `pnpm dev`, the installed CLI) wipe the first one's
|
|
61
|
+
* library (docs/REFERENCE-DOCS.md § On disk).
|
|
62
|
+
*
|
|
63
|
+
* - The data directory's own database (`<dataDir>/pierre-review.sqlite`, the installed CLI's
|
|
64
|
+
* default) keeps `<dataDir>/docs` — where every installed library already is; nothing moves.
|
|
65
|
+
* - Any other database (`pnpm dev`'s `apps/backend/data/pierre-review.sqlite`, a `--db`, a
|
|
66
|
+
* DATABASE_URL) gets `<dataDir>/docs/db-<12 hex of sha256(absolute DB path)>`. The `db-` prefix
|
|
67
|
+
* keeps it non-numeric, so the default root's janitor — which only walks `<accountId>` folders,
|
|
68
|
+
* in this version and every earlier one — never enters it.
|
|
69
|
+
*/
|
|
70
|
+
export function defaultDocsDir(dbPath, dataDir) {
|
|
71
|
+
const abs = resolve(dbPath);
|
|
72
|
+
const root = join(dataDir, 'docs');
|
|
73
|
+
if (abs === join(resolve(dataDir), DB_FILE))
|
|
74
|
+
return root;
|
|
75
|
+
return join(root, `db-${createHash('sha256').update(abs).digest('hex').slice(0, 12)}`);
|
|
76
|
+
}
|
|
56
77
|
function isInside(dir, path) {
|
|
57
78
|
const rel = relative(dir, path);
|
|
58
79
|
return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel));
|
package/dist/db/erase-account.js
CHANGED
|
@@ -162,6 +162,11 @@ export async function eraseAccountData(accountId) {
|
|
|
162
162
|
// hostname AND the sealed API token. No FKs on the first two, so the explicit deletes are the
|
|
163
163
|
// whole guarantee; workspace_trackers cascades from workspaces too, and is explicit anyway.
|
|
164
164
|
await tx.delete(schema.trackerTickets).where(eq(schema.trackerTickets.accountId, accountId)).execute();
|
|
165
|
+
// The tickets' comments (migration 0141): people's words from the tracker. No FKs.
|
|
166
|
+
await tx
|
|
167
|
+
.delete(schema.trackerTicketComments)
|
|
168
|
+
.where(eq(schema.trackerTicketComments.accountId, accountId))
|
|
169
|
+
.execute();
|
|
165
170
|
await tx.delete(schema.jiraAcFields).where(eq(schema.jiraAcFields.accountId, accountId)).execute();
|
|
166
171
|
await tx.delete(schema.workspaceTrackers).where(eq(schema.workspaceTrackers.accountId, accountId)).execute();
|
|
167
172
|
// SonarCloud (migration 0114 / pg 0101): the workspace rows hold the SEALED token; the snapshots
|
|
@@ -178,6 +183,12 @@ export async function eraseAccountData(accountId) {
|
|
|
178
183
|
// person typed, answers they paid for). Both cascade from workspaces too, and are explicit anyway.
|
|
179
184
|
// The FILES (the markdown tree and the stored uploads) are removed after the transaction below.
|
|
180
185
|
await tx.delete(schema.referenceDocChatMessages).where(eq(schema.referenceDocChatMessages.accountId, accountId)).execute();
|
|
186
|
+
// Wiki sources (migration 0140): the page rows (before their documents — composite FK) and the
|
|
187
|
+
// sealed wiki credential. Both cascade too, and are explicit anyway.
|
|
188
|
+
await tx.delete(schema.referenceDocWikiPages).where(eq(schema.referenceDocWikiPages.accountId, accountId)).execute();
|
|
189
|
+
await tx.delete(schema.workspaceWikiCredentials).where(eq(schema.workspaceWikiCredentials.accountId, accountId)).execute();
|
|
190
|
+
// Figure descriptions (migration 0143): the account's image-description cache.
|
|
191
|
+
await tx.delete(schema.referenceDocFigureDescriptions).where(eq(schema.referenceDocFigureDescriptions.accountId, accountId)).execute();
|
|
181
192
|
await tx.delete(schema.referenceDocs).where(eq(schema.referenceDocs.accountId, accountId)).execute();
|
|
182
193
|
// Story docs checks (migration 0121 / pg 0108): cascade from workspaces too, and are explicit anyway.
|
|
183
194
|
await tx.delete(schema.storyDocsAlignments).where(eq(schema.storyDocsAlignments.accountId, accountId)).execute();
|
|
@@ -328,6 +339,8 @@ export function accountScopedTables() {
|
|
|
328
339
|
// The issue tracker (migration 0088 / pg 0075). All three carry their own accountId and are
|
|
329
340
|
// erased explicitly above; `workspaceTrackers` holds a credential.
|
|
330
341
|
{ name: 'trackerTickets', col: schema.trackerTickets.accountId, table: schema.trackerTickets },
|
|
342
|
+
// Ticket comments (migration 0141), erased explicitly above.
|
|
343
|
+
{ name: 'trackerTicketComments', col: schema.trackerTicketComments.accountId, table: schema.trackerTicketComments },
|
|
331
344
|
{ name: 'jiraAcFields', col: schema.jiraAcFields.accountId, table: schema.jiraAcFields },
|
|
332
345
|
{ name: 'workspaceTrackers', col: schema.workspaceTrackers.accountId, table: schema.workspaceTrackers },
|
|
333
346
|
// SonarCloud (migration 0114 / pg 0101), erased explicitly above; `workspaceSonar` holds a credential.
|
|
@@ -344,6 +357,19 @@ export function accountScopedTables() {
|
|
|
344
357
|
col: schema.referenceDocChatMessages.accountId,
|
|
345
358
|
table: schema.referenceDocChatMessages,
|
|
346
359
|
},
|
|
360
|
+
// Wiki sources (migration 0140), erased explicitly above; `workspaceWikiCredentials` holds a credential.
|
|
361
|
+
{ name: 'referenceDocWikiPages', col: schema.referenceDocWikiPages.accountId, table: schema.referenceDocWikiPages },
|
|
362
|
+
// Figure descriptions (migration 0143), erased explicitly above.
|
|
363
|
+
{
|
|
364
|
+
name: 'referenceDocFigureDescriptions',
|
|
365
|
+
col: schema.referenceDocFigureDescriptions.accountId,
|
|
366
|
+
table: schema.referenceDocFigureDescriptions,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: 'workspaceWikiCredentials',
|
|
370
|
+
col: schema.workspaceWikiCredentials.accountId,
|
|
371
|
+
table: schema.workspaceWikiCredentials,
|
|
372
|
+
},
|
|
347
373
|
// Story docs checks (migration 0121 / pg 0108), erased explicitly above.
|
|
348
374
|
{ name: 'storyDocsAlignments', col: schema.storyDocsAlignments.accountId, table: schema.storyDocsAlignments },
|
|
349
375
|
// Chronology's settings history (migration 0096 / pg 0083), erased explicitly above.
|
|
@@ -84,6 +84,8 @@ export async function exportAccountData(accountId) {
|
|
|
84
84
|
matchScope: schema.workspaceTrackers.matchScope,
|
|
85
85
|
email: schema.workspaceTrackers.authEmail,
|
|
86
86
|
hasToken: schema.workspaceTrackers.authToken,
|
|
87
|
+
// The Notion property mapping (not a secret).
|
|
88
|
+
notionConfig: schema.workspaceTrackers.notionConfig,
|
|
87
89
|
updatedAt: schema.workspaceTrackers.updatedAt,
|
|
88
90
|
})
|
|
89
91
|
.from(schema.workspaceTrackers)
|
|
@@ -116,12 +118,48 @@ export async function exportAccountData(accountId) {
|
|
|
116
118
|
summary: schema.referenceDocs.summary,
|
|
117
119
|
topics: schema.referenceDocs.topics,
|
|
118
120
|
sections: schema.referenceDocs.sections,
|
|
121
|
+
wiki: schema.referenceDocs.wiki,
|
|
119
122
|
createdAt: schema.referenceDocs.createdAt,
|
|
120
123
|
builtAt: schema.referenceDocs.builtAt,
|
|
121
124
|
})
|
|
122
125
|
.from(schema.referenceDocs)
|
|
123
126
|
.where(eq(schema.referenceDocs.accountId, accountId))
|
|
124
127
|
.execute();
|
|
128
|
+
// Wiki sources (migration 0140): the pages of each wiki document, and the wiki credential WITHOUT its token.
|
|
129
|
+
const wikiPageRows = await db
|
|
130
|
+
.select({
|
|
131
|
+
docId: schema.referenceDocWikiPages.docId,
|
|
132
|
+
pageId: schema.referenceDocWikiPages.pageId,
|
|
133
|
+
title: schema.referenceDocWikiPages.title,
|
|
134
|
+
version: schema.referenceDocWikiPages.version,
|
|
135
|
+
lastEditedAt: schema.referenceDocWikiPages.lastEditedAt,
|
|
136
|
+
lastFetchedAt: schema.referenceDocWikiPages.lastFetchedAt,
|
|
137
|
+
})
|
|
138
|
+
.from(schema.referenceDocWikiPages)
|
|
139
|
+
.where(eq(schema.referenceDocWikiPages.accountId, accountId))
|
|
140
|
+
.execute();
|
|
141
|
+
const wikiCredentialRows = await db
|
|
142
|
+
.select({
|
|
143
|
+
workspaceId: schema.workspaceWikiCredentials.workspaceId,
|
|
144
|
+
provider: schema.workspaceWikiCredentials.provider,
|
|
145
|
+
siteUrl: schema.workspaceWikiCredentials.siteUrl,
|
|
146
|
+
email: schema.workspaceWikiCredentials.authEmail,
|
|
147
|
+
hasToken: schema.workspaceWikiCredentials.authToken,
|
|
148
|
+
updatedAt: schema.workspaceWikiCredentials.updatedAt,
|
|
149
|
+
})
|
|
150
|
+
.from(schema.workspaceWikiCredentials)
|
|
151
|
+
.where(eq(schema.workspaceWikiCredentials.accountId, accountId))
|
|
152
|
+
.execute();
|
|
153
|
+
const figureRows = await db
|
|
154
|
+
.select({
|
|
155
|
+
imageHash: schema.referenceDocFigureDescriptions.imageHash,
|
|
156
|
+
description: schema.referenceDocFigureDescriptions.description,
|
|
157
|
+
model: schema.referenceDocFigureDescriptions.model,
|
|
158
|
+
createdAt: schema.referenceDocFigureDescriptions.createdAt,
|
|
159
|
+
})
|
|
160
|
+
.from(schema.referenceDocFigureDescriptions)
|
|
161
|
+
.where(eq(schema.referenceDocFigureDescriptions.accountId, accountId))
|
|
162
|
+
.execute();
|
|
125
163
|
const docChatRows = await db
|
|
126
164
|
.select({
|
|
127
165
|
workspaceId: schema.referenceDocChatMessages.workspaceId,
|
|
@@ -275,6 +313,10 @@ export async function exportAccountData(accountId) {
|
|
|
275
313
|
workspaceTrackers: trackerRows.map((t) => ({ ...t, hasToken: t.hasToken != null && t.hasToken !== '' })),
|
|
276
314
|
workspaceSonar: sonarRows.map((t) => ({ ...t, hasToken: t.hasToken != null && t.hasToken !== '' })),
|
|
277
315
|
referenceDocs: referenceDocRows,
|
|
316
|
+
referenceDocWikiPages: wikiPageRows,
|
|
317
|
+
// ⚠ PRESENCE ONLY — never the stored token, in any form.
|
|
318
|
+
workspaceWikiCredentials: wikiCredentialRows.map((t) => ({ ...t, hasToken: t.hasToken != null && t.hasToken !== '' })),
|
|
319
|
+
referenceDocFigureDescriptions: figureRows,
|
|
278
320
|
repoRelations: repoRelationRows,
|
|
279
321
|
sprintCodeJudgements: capped('sprintCodeJudgements', codeJudgementRows),
|
|
280
322
|
referenceDocChatMessages: capped('referenceDocChatMessages', docChatRows),
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { db, schema } from './client.js';
|
|
4
|
+
// THIS DATABASE'S ID (migration 0139, `instance_meta`). A random id written ONCE, the first time
|
|
5
|
+
// anything asks, and read back from then on. It names the database FILE, not a path: the file can be
|
|
6
|
+
// moved (the ~/.pierre-review → ~/.limn rename) and keeps its id; a second database on the same
|
|
7
|
+
// machine (a worktree's dev server, the installed CLI) always has a different one. The reference-docs
|
|
8
|
+
// janitor writes it into each workspace docs folder as `.owner` and never deletes a folder holding
|
|
9
|
+
// another id (reference-docs/owner.ts).
|
|
10
|
+
const KEY = 'instance_id';
|
|
11
|
+
let cached = null;
|
|
12
|
+
async function readOrCreate() {
|
|
13
|
+
const t = schema.instanceMeta;
|
|
14
|
+
const read = async () => {
|
|
15
|
+
const rows = await db.select({ value: t.value }).from(t).where(eq(t.key, KEY)).execute();
|
|
16
|
+
return rows[0]?.value ?? null;
|
|
17
|
+
};
|
|
18
|
+
const existing = await read();
|
|
19
|
+
if (existing)
|
|
20
|
+
return existing;
|
|
21
|
+
// Two first callers race to the insert; the loser's row is dropped and both read the winner's.
|
|
22
|
+
await db.insert(t).values({ key: KEY, value: randomUUID() }).onConflictDoNothing().execute();
|
|
23
|
+
const id = await read();
|
|
24
|
+
if (!id)
|
|
25
|
+
throw new Error('instance_meta: instance_id could not be written');
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
/** This database's random id (created on first call, cached for the process). */
|
|
29
|
+
export function getInstanceId() {
|
|
30
|
+
if (!cached) {
|
|
31
|
+
cached = readOrCreate().catch((err) => {
|
|
32
|
+
cached = null;
|
|
33
|
+
throw err;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=instance-id.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- THIS DATABASE'S OWN FACTS (key → value), not an account's. One row so far: `instance_id`, a random
|
|
2
|
+
-- id written once (db/instance-id.ts) that names THIS database file wherever it is moved. The
|
|
3
|
+
-- reference-docs janitor writes it into every workspace docs folder it creates (`.owner`) and never
|
|
4
|
+
-- deletes a folder another database's id is written in (docs/REFERENCE-DOCS.md § On disk).
|
|
5
|
+
CREATE TABLE `instance_meta` (
|
|
6
|
+
`key` text PRIMARY KEY NOT NULL,
|
|
7
|
+
`value` text NOT NULL
|
|
8
|
+
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- WIKI SOURCES FOR THE REFERENCE LIBRARY (2026-10-28; docs/REFERENCE-DOCS.md § Wiki sources). CORE,
|
|
2
|
+
-- free, LOCAL ONLY (agentic). A Confluence page tree becomes ONE document; a refresh re-reads only the
|
|
3
|
+
-- changed pages and re-summarises only the changed sections. Self-contained and additive.
|
|
4
|
+
--
|
|
5
|
+
-- reference_docs.wiki_json NEW, nullable JSON (ReferenceDocWikiState): the tree's root,
|
|
6
|
+
-- depth and refresh state. NULL for every other source.
|
|
7
|
+
-- reference_docs.summary_cache_json NEW, nullable JSON (ReferenceDocSummaryCache): section hash →
|
|
8
|
+
-- summary, so an unchanged section is never summarised again.
|
|
9
|
+
-- reference_docs (id, account_id) NEW unique index: the composite FK target below.
|
|
10
|
+
-- workspaces.docs_used_at NEW, nullable: the library was last used — the refresh's
|
|
11
|
+
-- demand gate (7 days).
|
|
12
|
+
-- reference_doc_wiki_pages NEW: one row per page of a wiki document (version, hash, last
|
|
13
|
+
-- fetch, last error). Composite FK onto the document; cascades.
|
|
14
|
+
-- workspace_wiki_credentials NEW: a sealed wiki credential per (workspace, provider).
|
|
15
|
+
-- Composite FK onto the workspace; cascades.
|
|
16
|
+
-- Both new tables carry account_id and join accountScopedTables() (erased, exported without the token).
|
|
17
|
+
ALTER TABLE `reference_docs` ADD COLUMN `wiki_json` text;
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
ALTER TABLE `reference_docs` ADD COLUMN `summary_cache_json` text;
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
ALTER TABLE `workspaces` ADD COLUMN `docs_used_at` integer;
|
|
22
|
+
--> statement-breakpoint
|
|
23
|
+
CREATE UNIQUE INDEX `reference_docs_id_account` ON `reference_docs` (`id`,`account_id`);
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
CREATE TABLE `reference_doc_wiki_pages` (
|
|
26
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
27
|
+
`account_id` integer NOT NULL,
|
|
28
|
+
`doc_id` integer NOT NULL,
|
|
29
|
+
`page_id` text NOT NULL,
|
|
30
|
+
`parent_page_id` text,
|
|
31
|
+
`depth` integer DEFAULT 0 NOT NULL,
|
|
32
|
+
`position` integer DEFAULT 0 NOT NULL,
|
|
33
|
+
`title` text NOT NULL,
|
|
34
|
+
`version` integer,
|
|
35
|
+
`last_edited_at` integer,
|
|
36
|
+
`content_hash` text,
|
|
37
|
+
`last_fetched_at` integer,
|
|
38
|
+
`last_error` text,
|
|
39
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
40
|
+
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
41
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
42
|
+
CONSTRAINT `reference_doc_wiki_pages_doc_account_fk` FOREIGN KEY (`doc_id`,`account_id`) REFERENCES `reference_docs`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
|
|
43
|
+
);
|
|
44
|
+
--> statement-breakpoint
|
|
45
|
+
CREATE UNIQUE INDEX `reference_doc_wiki_pages_doc_page` ON `reference_doc_wiki_pages` (`doc_id`,`page_id`);
|
|
46
|
+
--> statement-breakpoint
|
|
47
|
+
CREATE INDEX `reference_doc_wiki_pages_account_doc` ON `reference_doc_wiki_pages` (`account_id`,`doc_id`);
|
|
48
|
+
--> statement-breakpoint
|
|
49
|
+
CREATE TABLE `workspace_wiki_credentials` (
|
|
50
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
51
|
+
`account_id` integer NOT NULL,
|
|
52
|
+
`workspace_id` integer NOT NULL,
|
|
53
|
+
`provider` text NOT NULL,
|
|
54
|
+
`site_url` text,
|
|
55
|
+
`auth_email` text,
|
|
56
|
+
`auth_token` text,
|
|
57
|
+
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
58
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
59
|
+
CONSTRAINT `workspace_wiki_credentials_workspace_account_fk` FOREIGN KEY (`workspace_id`,`account_id`) REFERENCES `workspaces`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
|
|
60
|
+
);
|
|
61
|
+
--> statement-breakpoint
|
|
62
|
+
CREATE UNIQUE INDEX `workspace_wiki_credentials_workspace_provider` ON `workspace_wiki_credentials` (`account_id`,`workspace_id`,`provider`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- TICKET COMMENTS (docs/TRACKERS.md § Ticket comments). The discussion under a tracker ticket, one row
|
|
2
|
+
-- per (account, provider, site, key, comment) — per TICKET, never per PR. The worker keeps the newest 20
|
|
3
|
+
-- per ticket from the issue read it already makes; bots are stored with `is_bot` and never shown.
|
|
4
|
+
CREATE TABLE `tracker_ticket_comments` (
|
|
5
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
6
|
+
`account_id` integer NOT NULL,
|
|
7
|
+
`provider` text NOT NULL,
|
|
8
|
+
`api_root` text NOT NULL,
|
|
9
|
+
`issue_key` text NOT NULL,
|
|
10
|
+
`comment_id` text NOT NULL,
|
|
11
|
+
`author_name` text DEFAULT '' NOT NULL,
|
|
12
|
+
`author_id` text,
|
|
13
|
+
`is_bot` integer DEFAULT false NOT NULL,
|
|
14
|
+
`body` text NOT NULL,
|
|
15
|
+
`created_at` integer NOT NULL,
|
|
16
|
+
`updated_at` integer,
|
|
17
|
+
`fetched_at` integer NOT NULL
|
|
18
|
+
);
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
CREATE UNIQUE INDEX `tracker_ticket_comments_ticket_comment` ON `tracker_ticket_comments` (`account_id`,`provider`,`api_root`,`issue_key`,`comment_id`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- What a ticket review run was given of the ticket's comments: {ids, count, hash} of the people's
|
|
2
|
+
-- comments (docs/TRACKERS.md § Ticket comments). NOT part of the run's fingerprint: a new comment never
|
|
3
|
+
-- makes a run stale; the state route counts the comments the run did not see.
|
|
4
|
+
ALTER TABLE `ticket_reviews` ADD `comments_seen` text;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- FIGURES AS TEXT + NOTION SOURCES (2026-10-28; docs/REFERENCE-DOCS.md § Figures, § Wiki sources →
|
|
2
|
+
-- Notion). CORE, free, LOCAL ONLY (agentic). Self-contained and additive. Notion needs no schema: its
|
|
3
|
+
-- token is a `workspace_wiki_credentials` row with provider 'notion' (0140).
|
|
4
|
+
--
|
|
5
|
+
-- reference_docs.figures_json NEW, nullable JSON (ReferenceDocFigures): the latest build's
|
|
6
|
+
-- figures — described / outlined / kept / pending — and the one
|
|
7
|
+
-- sentence when they could not be described here.
|
|
8
|
+
-- reference_doc_figure_descriptions NEW: the AI image-description CACHE, one row per (account,
|
|
9
|
+
-- image sha256), so an unchanged image is never described again.
|
|
10
|
+
-- The table carries account_id, joins accountScopedTables(), the erasure and the export.
|
|
11
|
+
ALTER TABLE `reference_docs` ADD COLUMN `figures_json` text;
|
|
12
|
+
--> statement-breakpoint
|
|
13
|
+
CREATE TABLE `reference_doc_figure_descriptions` (
|
|
14
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
15
|
+
`account_id` integer NOT NULL,
|
|
16
|
+
`image_hash` text NOT NULL,
|
|
17
|
+
`description` text NOT NULL,
|
|
18
|
+
`provider` text,
|
|
19
|
+
`model` text,
|
|
20
|
+
`cost_usd` real,
|
|
21
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
22
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade
|
|
23
|
+
);
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
CREATE UNIQUE INDEX `reference_doc_figure_descriptions_account_hash` ON `reference_doc_figure_descriptions` (`account_id`,`image_hash`);
|
|
@@ -974,6 +974,48 @@
|
|
|
974
974
|
"when": 1792137625016,
|
|
975
975
|
"tag": "0138_team_claim_lines",
|
|
976
976
|
"breakpoints": true
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"idx": 139,
|
|
980
|
+
"version": "6",
|
|
981
|
+
"when": 1792137626016,
|
|
982
|
+
"tag": "0139_instance_meta",
|
|
983
|
+
"breakpoints": true
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
"idx": 140,
|
|
987
|
+
"version": "6",
|
|
988
|
+
"when": 1792137627016,
|
|
989
|
+
"tag": "0140_reference_doc_wiki",
|
|
990
|
+
"breakpoints": true
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
"idx": 141,
|
|
994
|
+
"version": "6",
|
|
995
|
+
"when": 1792137629016,
|
|
996
|
+
"tag": "0141_tracker_ticket_comments",
|
|
997
|
+
"breakpoints": true
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
"idx": 142,
|
|
1001
|
+
"version": "6",
|
|
1002
|
+
"when": 1792137630016,
|
|
1003
|
+
"tag": "0142_ticket_review_comments_seen",
|
|
1004
|
+
"breakpoints": true
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
"idx": 143,
|
|
1008
|
+
"version": "6",
|
|
1009
|
+
"when": 1792137632016,
|
|
1010
|
+
"tag": "0143_reference_doc_figures",
|
|
1011
|
+
"breakpoints": true
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
"idx": 144,
|
|
1015
|
+
"version": "6",
|
|
1016
|
+
"when": 1792137640016,
|
|
1017
|
+
"tag": "0144_workspace_tracker_notion",
|
|
1018
|
+
"breakpoints": true
|
|
977
1019
|
}
|
|
978
1020
|
]
|
|
979
1021
|
}
|
package/dist/db/queries.js
CHANGED
|
@@ -54,6 +54,7 @@ import { forwardKindOf, isConflicting, isRedCiStatus, RED_CI_STATUSES, } from '.
|
|
|
54
54
|
import { authorAutomationFor, dependencyPrState, dependencyStateDetail, MERGE_QUEUE_CARD_DETAIL, securityFixDetail, unionAdvisoryIds, } from './dependency-cards.js';
|
|
55
55
|
import { deriveSecurityAlerts } from './security-alerts.js';
|
|
56
56
|
import { pruneTicketReviewsForPrs } from './ticket-review-prune.js';
|
|
57
|
+
import { pruneOrphanTicketComments } from './ticket-comments-prune.js';
|
|
57
58
|
import { pruneCiReviewsForPrs } from './ci-review-prune.js';
|
|
58
59
|
import { pruneSequenceDiagramsForPrs } from './sequence-diagram-prune.js';
|
|
59
60
|
import { failingChecksFields, prFailingChecks, trunkFailingChecks, trunkFailingFor, } from './failing-checks.js';
|
|
@@ -7872,6 +7873,8 @@ export async function deleteRepo(id, accountId) {
|
|
|
7872
7873
|
// Stored tracker tickets (core since migration 0088 / pg 0075; the plugin used to prune them
|
|
7873
7874
|
// per PR). No FKs, so nothing would cascade.
|
|
7874
7875
|
await tx.delete(schema.trackerTickets).where(inArray(schema.trackerTickets.prId, prIds)).execute();
|
|
7876
|
+
// Their tickets' comments (migration 0141), once no PR of this account names the ticket.
|
|
7877
|
+
await pruneOrphanTicketComments(tx, [accountId]);
|
|
7875
7878
|
// SonarCloud snapshots (migration 0114 / pg 0101). No FKs, so nothing would cascade.
|
|
7876
7879
|
await tx.delete(schema.sonarSnapshots).where(inArray(schema.sonarSnapshots.prId, prIds)).execute();
|
|
7877
7880
|
// The team-claim ledger (migration 0138). No FKs, so nothing would cascade. This machine's OPEN
|
package/dist/db/retention.js
CHANGED
|
@@ -2,6 +2,7 @@ import { inArray, lt } from 'drizzle-orm';
|
|
|
2
2
|
import { db, runTransaction, schema } from './client.js';
|
|
3
3
|
import { config } from '../config.js';
|
|
4
4
|
import { pruneTicketReviewsForPrs } from './ticket-review-prune.js';
|
|
5
|
+
import { pruneOrphanTicketComments } from './ticket-comments-prune.js';
|
|
5
6
|
import { pruneCiReviewsForPrs } from './ci-review-prune.js';
|
|
6
7
|
import { pruneSequenceDiagramsForPrs } from './sequence-diagram-prune.js';
|
|
7
8
|
import { deleteSettledClaimLinesForPrs } from './team-claim-lines-delete.js';
|
|
@@ -99,6 +100,8 @@ async function deletePrSubtree(tx, prIds) {
|
|
|
99
100
|
// Stored tracker tickets (core since migration 0088 / pg 0075; the plugin's `pruneProByPrIds`
|
|
100
101
|
// used to take them). No FKs — explicit, pr-keyed.
|
|
101
102
|
await tx.delete(schema.trackerTickets).where(inArray(schema.trackerTickets.prId, prIds)).execute();
|
|
103
|
+
// Their tickets' comments (migration 0141), once no PR names the ticket (db/ticket-comments-prune.ts).
|
|
104
|
+
await pruneOrphanTicketComments(tx, null);
|
|
102
105
|
// SonarCloud snapshots (migration 0114 / pg 0101). No FKs — explicit, pr-keyed.
|
|
103
106
|
await tx.delete(schema.sonarSnapshots).where(inArray(schema.sonarSnapshots.prId, prIds)).execute();
|
|
104
107
|
// The team-claim ledger (migration 0138). No FKs — explicit, pr-keyed. This machine's OPEN rows
|