limn-review 0.1.166 → 0.1.168
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/api/plugins/rate-limit.js +18 -2
- package/dist/api/routes/me.js +48 -2
- package/dist/api/routes/repos.js +7 -3
- package/dist/auth/account.js +19 -1
- package/dist/coding/story-build/docs.js +5 -4
- package/dist/db/erase-account.js +9 -0
- package/dist/db/export-account.js +20 -0
- package/dist/db/feature-gates.js +80 -0
- package/dist/db/migrations/0152_reference_doc_chunks.sql +45 -0
- package/dist/db/migrations/0153_feature_visibility.sql +16 -0
- package/dist/db/migrations/0154_first_landing.sql +15 -0
- package/dist/db/migrations/0155_repo_docs_offers.sql +36 -0
- package/dist/db/migrations/0156_ci_history_pending.sql +9 -0
- package/dist/db/migrations/0157_sprint_source_probe.sql +17 -0
- package/dist/db/migrations/0158_repo_docs_unreadable.sql +12 -0
- package/dist/db/migrations/meta/_journal.json +49 -0
- package/dist/db/my-turn-floor.js +18 -0
- package/dist/db/pending-first-landing.js +174 -0
- package/dist/db/pending-tabs.js +8 -0
- package/dist/db/queries.js +20 -2
- package/dist/db/schema.sqlite.js +115 -0
- package/dist/index.js +7 -0
- package/dist/reference-docs/chat.js +38 -2
- package/dist/reference-docs/chunks.js +162 -0
- package/dist/reference-docs/manager.js +4 -0
- package/dist/reference-docs/prompts.js +7 -1
- package/dist/reference-docs/repo-offers-routes.js +129 -0
- package/dist/reference-docs/repo-offers-store.js +283 -0
- package/dist/reference-docs/repo-offers.js +235 -0
- package/dist/reference-docs/retrieve.js +110 -0
- package/dist/reference-docs/routes.js +19 -3
- package/dist/reference-docs/startup.js +9 -1
- package/dist/reference-docs/wiki/github.js +1 -1
- package/dist/reference-docs/wiki/routes.js +53 -37
- package/dist/review/agentic.js +4 -0
- package/dist/review/story-docs/candidates.js +34 -0
- package/dist/review/story-docs/manager.js +4 -3
- package/dist/review/story-docs/shortlist.js +4 -3
- package/dist/shared/features.js +51 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/pending-rules.js +13 -0
- package/dist/shared/reference-docs-offers.js +63 -0
- package/dist/shared/sprint-stories.js +7 -0
- package/dist/sync/ci-history-pending.js +49 -0
- package/dist/sync/sync-manager.js +678 -297
- package/dist/sync/sync-repo.js +3 -2
- package/dist/sync/upsert.js +28 -1
- package/dist/sync/walk-listeners.js +30 -0
- package/dist/tracker/sprint/github.js +290 -9
- package/dist/tracker/sprint/probe.js +204 -0
- package/dist/tracker/worker.js +5 -0
- package/package.json +1 -1
- package/public/assets/{AiFixTab-RRZCvJa-.js → AiFixTab-CLg4ncIF.js} +1 -1
- package/public/assets/{ClaudeReviewTab-Dorz2I8S.js → ClaudeReviewTab-BNfmEfZY.js} +1 -1
- package/public/assets/{PrBotBehaviourTab-COf3vJXP.js → PrBotBehaviourTab-5acbInfj.js} +1 -1
- package/public/assets/index-CViygrci.css +1 -0
- package/public/assets/index-DVFWpfMV.js +89 -0
- package/public/index.html +2 -2
- package/public/assets/index-BezBYXmy.js +0 -89
- package/public/assets/index-CeGG8m25.css +0 -1
|
@@ -177,6 +177,15 @@ function tierFor(method, path) {
|
|
|
177
177
|
// EXACT `===` for the same near-miss.
|
|
178
178
|
if (path === '/api/me/my-turn-settings')
|
|
179
179
|
return [TIERS.read];
|
|
180
|
+
// PUT /api/me/feature-settings — Settings → Features (which sidebar destinations show). One
|
|
181
|
+
// validated UPDATE plus DB-only reads; no GitHub, no model. Recorded, EXACT `===`.
|
|
182
|
+
if (path === '/api/me/feature-settings')
|
|
183
|
+
return [TIERS.read];
|
|
184
|
+
// POST /api/me/review-suggestion/dismiss — Pending's one-time AI review card, dismissed for good.
|
|
185
|
+
// One UPDATE of one account column; no GitHub, no model (the card's Run goes through the review
|
|
186
|
+
// start route, which keeps its own tier). Recorded, EXACT `===`.
|
|
187
|
+
if (path === '/api/me/review-suggestion/dismiss')
|
|
188
|
+
return [TIERS.read];
|
|
180
189
|
// PUT / DELETE /api/my-turn/dismissals/:kind/:id — dismiss or bring back one My Turn entry. One
|
|
181
190
|
// ownership read and one upsert/delete on a tiny table; no GitHub call, no model call. Recorded
|
|
182
191
|
// rather than inherited, and spelled with the `/`-terminated prefix: `/api/my-turn` itself (the
|
|
@@ -445,9 +454,16 @@ function tierFor(method, path) {
|
|
|
445
454
|
// → search + ai + ai_hourly. Spelled EXACTLY here, above the family's `read` line: these GitHub reads
|
|
446
455
|
// never reach the `hitsGithub` alternation below (that is the GitHub WRITE bucket, and this family
|
|
447
456
|
// returns first).
|
|
448
|
-
|
|
457
|
+
// The wizard's "From your repositories" (`POST /api/docs/offers/check`, reference-docs/repo-offers-routes.ts)
|
|
458
|
+
// looks at up to DOCS_FROM_REPOS_SCAN_MAX repositories' trees on the reader's GitHub quota (a stored scan
|
|
459
|
+
// is reused) and runs no model → search + read, like the preflights. EXACT `===`.
|
|
460
|
+
if (method === 'POST' &&
|
|
461
|
+
(path === '/api/docs/wiki/preflight' || path === '/api/docs/github/preflight' || path === '/api/docs/offers/check'))
|
|
449
462
|
return [TIERS.search, TIERS.read];
|
|
450
|
-
|
|
463
|
+
// THE DOCS OFFERS' ADD (reference-docs/repo-offers-routes.ts): each chosen folder is read on GitHub and
|
|
464
|
+
// its build queued, exactly as `POST /api/docs/github` → search + ai + ai_hourly. Its "Not now"
|
|
465
|
+
// (`/api/docs/offers/dismiss`) is DB only and falls to the family's `read` line.
|
|
466
|
+
if (method === 'POST' && (path === '/api/docs/github' || path === '/api/docs/offers/add'))
|
|
451
467
|
return [TIERS.search, TIERS.ai, TIERS.aiHourly];
|
|
452
468
|
if (path === '/api/docs' || path.startsWith('/api/docs/'))
|
|
453
469
|
return [TIERS.read];
|
package/dist/api/routes/me.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { MY_TURN_TOGGLES, validateMyTurnSettings } from '../../shared/index.js';
|
|
1
|
+
import { FEATURE_IDS, MY_TURN_TOGGLES, resolveFeatures, validateMyTurnSettings } from '../../shared/index.js';
|
|
2
2
|
import { config } from '../../config.js';
|
|
3
|
-
import { accountToLocalUser, setBlastRadiusConfig, setLargePrCodeLocThreshold, setMyTurnSettings, } from '../../auth/account.js';
|
|
3
|
+
import { accountToLocalUser, setBlastRadiusConfig, setFeatureSettings, setLargePrCodeLocThreshold, setMyTurnSettings, } from '../../auth/account.js';
|
|
4
4
|
import { resolveLargePrThreshold } from '../../db/code-loc.js';
|
|
5
5
|
import { exportAccountData } from '../../db/export-account.js';
|
|
6
6
|
import { accountIdOf } from '../plugins/auth.js';
|
|
@@ -17,6 +17,29 @@ import { aiProviderSettingStatus, ALL_RUN_KINDS, CODEX_NOT_AVAILABLE_MESSAGE, CO
|
|
|
17
17
|
import { getMyTurn } from '../../db/queries.js';
|
|
18
18
|
import { clearDailyBriefCountsFor } from '../../db/daily-brief.js';
|
|
19
19
|
import { dismissMyTurn, restoreMyTurn } from '../../db/my-turn-dismissals.js';
|
|
20
|
+
import { featureFacts } from '../../db/feature-gates.js';
|
|
21
|
+
import { dismissReviewSuggestion } from '../../db/pending-first-landing.js';
|
|
22
|
+
// SETTINGS → FEATURES: the switches as a whole object (a PUT replaces it). Booleans only, the known
|
|
23
|
+
// keys only; `null` resets every switch to its default.
|
|
24
|
+
const featureSettingsSchema = {
|
|
25
|
+
body: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
required: ['settings'],
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
properties: {
|
|
30
|
+
settings: {
|
|
31
|
+
type: ['object', 'null'],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
properties: Object.fromEntries(FEATURE_IDS.map((k) => [k, { type: 'boolean' }])),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
/** Which sidebar destinations show (shared features.ts). DB reads only. */
|
|
39
|
+
async function featuresFor(accountId, account) {
|
|
40
|
+
const facts = await featureFacts(accountId, account?.featuresExistingInstall === true);
|
|
41
|
+
return resolveFeatures(account?.featureSettings ?? null, facts);
|
|
42
|
+
}
|
|
20
43
|
// The LARGE-PR FLAG's threshold. `null` is a first-class value meaning "reset to the product
|
|
21
44
|
// default", so the type is the union — NOT an optional key, which would make "clear it" and
|
|
22
45
|
// "don't change it" the same request. The bounds are validation, not taste: `minimum: 1` because a
|
|
@@ -272,6 +295,8 @@ export async function meRoutes(app) {
|
|
|
272
295
|
// The MY TURN settings, RAW and nullable (the stored overrides) — the `blastRadius` rule: the
|
|
273
296
|
// SPA resolves them through the ONE `resolveMyTurnSettings`, so nothing is duplicated here.
|
|
274
297
|
myTurnSettings: req.account?.myTurnSettings ?? null,
|
|
298
|
+
// SETTINGS → FEATURES, resolved (visibility only; a deep link still opens a hidden one).
|
|
299
|
+
features: await featuresFor(accountId, req.account),
|
|
275
300
|
// Orgs currently SAML-blocked for this account (empty in the normal case).
|
|
276
301
|
authNotices: getAuthNotices(accountId),
|
|
277
302
|
};
|
|
@@ -357,6 +382,27 @@ export async function meRoutes(app) {
|
|
|
357
382
|
const body = { status: 'ok', myTurnSettings: stored };
|
|
358
383
|
return body;
|
|
359
384
|
});
|
|
385
|
+
// SETTINGS → FEATURES — the fourth ONE-PER-ACCOUNT setting: which sidebar destinations show.
|
|
386
|
+
// A PUT of the WHOLE switches object (null resets all); echoes what was STORED and the resolved
|
|
387
|
+
// result, so the sidebar and Settings agree without waiting for /api/me. Visibility only: no
|
|
388
|
+
// sync, read or scoring changes. Rate tier `read` (pinned in rate-limit.test.ts): one validated
|
|
389
|
+
// UPDATE plus the DB-only facts, no GitHub and no model.
|
|
390
|
+
app.put('/api/me/feature-settings', { schema: featureSettingsSchema }, async (req) => {
|
|
391
|
+
const accountId = accountIdOf(req);
|
|
392
|
+
const { settings } = req.body;
|
|
393
|
+
const stored = await setFeatureSettings(accountId, settings);
|
|
394
|
+
const features = await featuresFor(accountId, { ...req.account, featureSettings: stored });
|
|
395
|
+
const body = { status: 'ok', featureSettings: stored, features };
|
|
396
|
+
return body;
|
|
397
|
+
});
|
|
398
|
+
// PENDING'S ONE-TIME AI REVIEW SUGGESTION — dismissed for good (db/pending-first-landing.ts). One
|
|
399
|
+
// UPDATE of one account column, stamped once (a second press keeps the first time). Rate tier
|
|
400
|
+
// `read` (pinned in rate-limit.test.ts): no GitHub, no model.
|
|
401
|
+
app.post('/api/me/review-suggestion/dismiss', async (req) => {
|
|
402
|
+
await dismissReviewSuggestion(accountIdOf(req));
|
|
403
|
+
const body = { status: 'ok' };
|
|
404
|
+
return body;
|
|
405
|
+
});
|
|
360
406
|
// Access + portability: the whole account as one JSON document (db/export-account.ts). Erasure
|
|
361
407
|
// is deleting the local database file.
|
|
362
408
|
app.get('/api/me/export', async (req, reply) => {
|
package/dist/api/routes/repos.js
CHANGED
|
@@ -22,6 +22,7 @@ const createRepoSchema = {
|
|
|
22
22
|
properties: {
|
|
23
23
|
owner: { type: 'string', minLength: 1 },
|
|
24
24
|
name: { type: 'string', minLength: 1 },
|
|
25
|
+
firstRun: { type: 'boolean' },
|
|
25
26
|
},
|
|
26
27
|
},
|
|
27
28
|
};
|
|
@@ -316,7 +317,7 @@ export async function repoRoutes(app) {
|
|
|
316
317
|
return body;
|
|
317
318
|
});
|
|
318
319
|
app.post('/api/repos', { schema: createRepoSchema }, async (req, reply) => {
|
|
319
|
-
const { owner, name } = req.body;
|
|
320
|
+
const { owner, name, firstRun } = req.body;
|
|
320
321
|
const accountId = accountIdOf(req);
|
|
321
322
|
let resp;
|
|
322
323
|
try {
|
|
@@ -350,7 +351,10 @@ export async function repoRoutes(app) {
|
|
|
350
351
|
}
|
|
351
352
|
const canonOwner = resp.repository.owner.login;
|
|
352
353
|
const canonName = resp.repository.name;
|
|
353
|
-
const repoId = await upsertRepo(canonOwner, canonName, resp.repository.id, null, accountId
|
|
354
|
+
const repoId = await upsertRepo(canonOwner, canonName, resp.repository.id, null, accountId, undefined, undefined,
|
|
355
|
+
// The reader's add: decides the first-onboarding mark (My Turn's 7-day lookback, Pending's
|
|
356
|
+
// summary window) inside the same transaction as the insert.
|
|
357
|
+
{ firstRun: firstRun === true });
|
|
354
358
|
// No second visibility axis to set: `upsertRepo` gives the repo its workspace membership
|
|
355
359
|
// in the same transaction, and a repo in a workspace is fully live — Feed, Activity, My
|
|
356
360
|
// Turn and Bots all cover it. (The old `setRepoInboxWatch(accountId, repoId, true)` call
|
|
@@ -385,7 +389,7 @@ export async function repoRoutes(app) {
|
|
|
385
389
|
// when its turn came (the walk's upsertRepo re-creates the row). Cancelling a queued
|
|
386
390
|
// repo is synchronous: it dequeues + clears the progress row, so the delete can go.
|
|
387
391
|
if (isSyncQueued(id))
|
|
388
|
-
requestSyncCancel(id);
|
|
392
|
+
requestSyncCancel(id, { dropFollowOns: true });
|
|
389
393
|
// A sync in flight would re-create the repo (and its rows) right after we
|
|
390
394
|
// delete them, since the sync's upserts are still running. Refuse until it
|
|
391
395
|
// settles — the cron tick / initial backfill is short.
|
package/dist/auth/account.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { eq } from 'drizzle-orm';
|
|
3
|
-
import { compactMyTurnSettings } from '../shared/index.js';
|
|
3
|
+
import { compactFeatureSettings, compactMyTurnSettings } from '../shared/index.js';
|
|
4
4
|
import { db, schema } from '../db/client.js';
|
|
5
5
|
import { getGithubTokenAsync } from '../github/auth.js';
|
|
6
6
|
const STALE_MS = 24 * 60 * 60 * 1000; // re-fetch the local identity once a day
|
|
@@ -35,6 +35,8 @@ function rowToAccount(row) {
|
|
|
35
35
|
largePrCodeLocThreshold: row.largePrCodeLocThreshold ?? null,
|
|
36
36
|
blastRadiusConfig: row.blastRadiusConfig ?? null,
|
|
37
37
|
myTurnSettings: row.myTurnSettings ?? null,
|
|
38
|
+
featureSettings: row.featureSettings ?? null,
|
|
39
|
+
featuresExistingInstall: row.featuresExistingInstall,
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
// Module cache of the local account, set by ensureLocalAccount() at startup so
|
|
@@ -294,6 +296,22 @@ export async function setMyTurnSettings(accountId, settings) {
|
|
|
294
296
|
await refreshLocalAccountCache(accountId);
|
|
295
297
|
return value;
|
|
296
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* SETTINGS → FEATURES: store the reader's switches (overrides only, compacted; null resets every
|
|
301
|
+
* switch to its default). Returns what was stored.
|
|
302
|
+
*/
|
|
303
|
+
export async function setFeatureSettings(accountId, settings) {
|
|
304
|
+
const { accounts } = schema;
|
|
305
|
+
const value = compactFeatureSettings(settings);
|
|
306
|
+
await db
|
|
307
|
+
.update(accounts)
|
|
308
|
+
.set({ featureSettings: value })
|
|
309
|
+
.where(eq(accounts.id, accountId))
|
|
310
|
+
.execute();
|
|
311
|
+
// ⚠ The settings rule above: without the refresh, /api/me would hand the old value back.
|
|
312
|
+
await refreshLocalAccountCache(accountId);
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
297
315
|
/**
|
|
298
316
|
* Resolve an account's owner to a row in `users` (by login) if they've appeared
|
|
299
317
|
* in any synced repo. Returns null when "you" haven't authored/acted anywhere
|
|
@@ -13,14 +13,14 @@ import { pickReviewNonce } from '../../review/claude-review/prompts.js';
|
|
|
13
13
|
import { buildBuildDocsPrompt, buildDocsAbouts, buildDocsSystemPrompt, buildDocsUntrustedTexts, } from '../../review/story-docs/build-prompts.js';
|
|
14
14
|
import { validateBuildDocs } from '../../review/story-docs/build-validate.js';
|
|
15
15
|
import { inlineSections, storyDocsRoots, storyDocsTurnCap } from '../../review/story-docs/prompts.js';
|
|
16
|
-
import {
|
|
16
|
+
import { runShortlist, wholeLibraryShortlist } from '../../review/story-docs/shortlist.js';
|
|
17
17
|
import { storyBuildLimits } from './limits.js';
|
|
18
18
|
import { getStoryBuildRow, listStoryBuildRepoRows, updateStoryBuild } from './persist.js';
|
|
19
19
|
// DOCS CONSIDERATION — WORK PACKAGE 3 (docs/STORY-BUILD.md § Docs consideration). THE STORY WINS.
|
|
20
20
|
//
|
|
21
21
|
// runBuildDocs the story-docs pipeline's `build` mode over ONE session: the frozen story (seed), the
|
|
22
22
|
// gap fold and the CURRENT plan revision → the shortlist (the whole library when it fits,
|
|
23
|
-
// else
|
|
23
|
+
// else full-text search → Haiku) → ONE low-effort read over the inlined sections,
|
|
24
24
|
// answering `submit_build_docs` (grounded / superseded / suggestion) → every reference
|
|
25
25
|
// checked against the shortlisted documents (an unreferenced item DROPPED) → stored on the
|
|
26
26
|
// SESSION's row (`docs_json`; never `story_docs_alignments`, so the per-story check's
|
|
@@ -137,11 +137,12 @@ export const runBuildDocs = async (ctx, { accountId, buildId, force, onProgress,
|
|
|
137
137
|
const budgetLeft = storyBuildBudgetLeftUsd(row, await listStoryBuildRepoRows(ctx, accountId, buildId));
|
|
138
138
|
if (budgetLeft < STORY_BUILD_MIN_BUDGET_LEFT_USD)
|
|
139
139
|
return finish({ docsStatus: 'failed', docsError: BUILD_DOCS_TEXT.budget });
|
|
140
|
-
const [{ loadLibrarySnapshot }, { readIndex }, { libraryDir }, { shortlistDocsOf }] = await Promise.all([
|
|
140
|
+
const [{ loadLibrarySnapshot }, { readIndex }, { libraryDir }, { shortlistDocsOf }, { storyDocsCandidates }] = await Promise.all([
|
|
141
141
|
import('../../reference-docs/context.js'),
|
|
142
142
|
import('../../reference-docs/library.js'),
|
|
143
143
|
import('../../reference-docs/paths.js'),
|
|
144
144
|
import('../../review/story-docs/manager.js'),
|
|
145
|
+
import('../../review/story-docs/candidates.js'),
|
|
145
146
|
]);
|
|
146
147
|
const [index, lib] = await Promise.all([readIndex(accountId, row.workspaceId), loadLibrarySnapshot(accountId, row.workspaceId)]);
|
|
147
148
|
// 1. THE SHORTLIST — every section when the library fits, else the prefilter's candidates → Haiku.
|
|
@@ -163,7 +164,7 @@ export const runBuildDocs = async (ctx, { accountId, buildId, force, onProgress,
|
|
|
163
164
|
criteria: queryCriteria,
|
|
164
165
|
index,
|
|
165
166
|
docs,
|
|
166
|
-
candidates:
|
|
167
|
+
candidates: await storyDocsCandidates({ accountId, workspaceId: row.workspaceId }, { story: storyText, criteria: queryCriteria }, docs),
|
|
167
168
|
},
|
|
168
169
|
lib,
|
|
169
170
|
...(seams.complete ? { complete: seams.complete } : {}),
|
package/dist/db/erase-account.js
CHANGED
|
@@ -189,9 +189,14 @@ export async function eraseAccountData(accountId) {
|
|
|
189
189
|
// Wiki sources (migration 0140): the page rows (before their documents — composite FK) and the
|
|
190
190
|
// sealed wiki credential. Both cascade too, and are explicit anyway.
|
|
191
191
|
await tx.delete(schema.referenceDocWikiPages).where(eq(schema.referenceDocWikiPages.accountId, accountId)).execute();
|
|
192
|
+
// Full-text passages (migration 0152): derived from the files, before their documents (composite FK).
|
|
193
|
+
// The FTS5 index follows by trigger. They cascade too, and are explicit anyway.
|
|
194
|
+
await tx.delete(schema.referenceDocChunks).where(eq(schema.referenceDocChunks.accountId, accountId)).execute();
|
|
192
195
|
await tx.delete(schema.workspaceWikiCredentials).where(eq(schema.workspaceWikiCredentials.accountId, accountId)).execute();
|
|
193
196
|
// Figure descriptions (migration 0143): the account's image-description cache.
|
|
194
197
|
await tx.delete(schema.referenceDocFigureDescriptions).where(eq(schema.referenceDocFigureDescriptions.accountId, accountId)).execute();
|
|
198
|
+
// Docs offers (migration 0155): cascade from workspaces and repos too, and are explicit anyway.
|
|
199
|
+
await tx.delete(schema.referenceDocRepoOffers).where(eq(schema.referenceDocRepoOffers.accountId, accountId)).execute();
|
|
195
200
|
await tx.delete(schema.referenceDocs).where(eq(schema.referenceDocs.accountId, accountId)).execute();
|
|
196
201
|
// Story docs checks (migration 0121 / pg 0108): cascade from workspaces too, and are explicit anyway.
|
|
197
202
|
await tx.delete(schema.storyDocsAlignments).where(eq(schema.storyDocsAlignments.accountId, accountId)).execute();
|
|
@@ -375,6 +380,10 @@ export function accountScopedTables() {
|
|
|
375
380
|
},
|
|
376
381
|
// Wiki sources (migration 0140), erased explicitly above; `workspaceWikiCredentials` holds a credential.
|
|
377
382
|
{ name: 'referenceDocWikiPages', col: schema.referenceDocWikiPages.accountId, table: schema.referenceDocWikiPages },
|
|
383
|
+
// Full-text passages (migration 0152), erased explicitly above. Derived, so not exported.
|
|
384
|
+
{ name: 'referenceDocChunks', col: schema.referenceDocChunks.accountId, table: schema.referenceDocChunks },
|
|
385
|
+
// Docs offers (migration 0155), erased explicitly above.
|
|
386
|
+
{ name: 'referenceDocRepoOffers', col: schema.referenceDocRepoOffers.accountId, table: schema.referenceDocRepoOffers },
|
|
378
387
|
// Figure descriptions (migration 0143), erased explicitly above.
|
|
379
388
|
{
|
|
380
389
|
name: 'referenceDocFigureDescriptions',
|
|
@@ -53,6 +53,9 @@ export async function exportAccountData(accountId) {
|
|
|
53
53
|
largePrCodeLocThreshold: accounts.largePrCodeLocThreshold,
|
|
54
54
|
blastRadiusConfig: accounts.blastRadiusConfig,
|
|
55
55
|
myTurnSettings: accounts.myTurnSettings,
|
|
56
|
+
featureSettings: accounts.featureSettings,
|
|
57
|
+
featuresExistingInstall: accounts.featuresExistingInstall,
|
|
58
|
+
reviewSuggestionDismissedAt: accounts.reviewSuggestionDismissedAt,
|
|
56
59
|
aiBudgetSettings: accounts.aiBudgetSettings,
|
|
57
60
|
aiBudgetReading: accounts.aiBudgetReading,
|
|
58
61
|
lastLoginAt: accounts.lastLoginAt,
|
|
@@ -162,6 +165,22 @@ export async function exportAccountData(accountId) {
|
|
|
162
165
|
.from(schema.referenceDocFigureDescriptions)
|
|
163
166
|
.where(eq(schema.referenceDocFigureDescriptions.accountId, accountId))
|
|
164
167
|
.execute();
|
|
168
|
+
const repoOfferRows = await db
|
|
169
|
+
.select({
|
|
170
|
+
workspaceId: schema.referenceDocRepoOffers.workspaceId,
|
|
171
|
+
repoId: schema.referenceDocRepoOffers.repoId,
|
|
172
|
+
path: schema.referenceDocRepoOffers.path,
|
|
173
|
+
docCount: schema.referenceDocRepoOffers.docCount,
|
|
174
|
+
wordEstimate: schema.referenceDocRepoOffers.wordEstimate,
|
|
175
|
+
tooLarge: schema.referenceDocRepoOffers.tooLarge,
|
|
176
|
+
state: schema.referenceDocRepoOffers.state,
|
|
177
|
+
docId: schema.referenceDocRepoOffers.docId,
|
|
178
|
+
detectedAt: schema.referenceDocRepoOffers.detectedAt,
|
|
179
|
+
decidedAt: schema.referenceDocRepoOffers.decidedAt,
|
|
180
|
+
})
|
|
181
|
+
.from(schema.referenceDocRepoOffers)
|
|
182
|
+
.where(eq(schema.referenceDocRepoOffers.accountId, accountId))
|
|
183
|
+
.execute();
|
|
165
184
|
const docChatRows = await db
|
|
166
185
|
.select({
|
|
167
186
|
workspaceId: schema.referenceDocChatMessages.workspaceId,
|
|
@@ -386,6 +405,7 @@ export async function exportAccountData(accountId) {
|
|
|
386
405
|
// ⚠ PRESENCE ONLY — never the stored token, in any form.
|
|
387
406
|
workspaceWikiCredentials: wikiCredentialRows.map((t) => ({ ...t, hasToken: t.hasToken != null && t.hasToken !== '' })),
|
|
388
407
|
referenceDocFigureDescriptions: figureRows,
|
|
408
|
+
referenceDocRepoOffers: repoOfferRows,
|
|
389
409
|
repoRelations: repoRelationRows,
|
|
390
410
|
sprintCodeJudgements: capped('sprintCodeJudgements', codeJudgementRows),
|
|
391
411
|
referenceDocChatMessages: capped('referenceDocChatMessages', docChatRows),
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { and, eq, inArray, isNotNull } from 'drizzle-orm';
|
|
2
|
+
import { db, schema } from './client.js';
|
|
3
|
+
// SETTINGS → FEATURES — the facts behind the defaults (shared features.ts `resolveFeatures`). Read on
|
|
4
|
+
// every GET /api/me, so each is ONE small indexed DB read: no tracker call, no GitHub call. Every
|
|
5
|
+
// query predicates on accountId.
|
|
6
|
+
/** At least one of the account's repositories has finished its 90-day backfill (docs/SYNC.md: a
|
|
7
|
+
* repo stays "never fully synced" until phase 2 ends, so `last_full_sync_at` is that moment). */
|
|
8
|
+
export async function hasCompletedBackfill(accountId) {
|
|
9
|
+
const { repos, syncState } = schema;
|
|
10
|
+
const rows = await db
|
|
11
|
+
.select({ id: repos.id })
|
|
12
|
+
.from(syncState)
|
|
13
|
+
.innerJoin(repos, eq(repos.id, syncState.repoId))
|
|
14
|
+
.where(and(eq(repos.accountId, accountId), isNotNull(syncState.lastFullSyncAt)))
|
|
15
|
+
.limit(1)
|
|
16
|
+
.execute();
|
|
17
|
+
return rows.length > 0;
|
|
18
|
+
}
|
|
19
|
+
/** Is this stored sprint-read source a real sprint (not the status fallback)? */
|
|
20
|
+
export function isSprintSource(source) {
|
|
21
|
+
if (source == null)
|
|
22
|
+
return false;
|
|
23
|
+
switch (source.kind) {
|
|
24
|
+
case 'sprint':
|
|
25
|
+
case 'cycle':
|
|
26
|
+
case 'iteration':
|
|
27
|
+
return true;
|
|
28
|
+
case 'milestone':
|
|
29
|
+
// Only a milestone with a due date makes a sprint.
|
|
30
|
+
return source.dueOn != null;
|
|
31
|
+
case 'status_fallback':
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The trackers whose configuration names sprints or cycles (GitHub Issues is the automatic default
|
|
36
|
+
* in most workspaces, so its configuration alone says nothing). */
|
|
37
|
+
const SPRINT_TRACKERS = ['jira', 'linear', 'notion'];
|
|
38
|
+
/**
|
|
39
|
+
* Does any workspace of this account have a sprint source? True when:
|
|
40
|
+
* - the workspace's last sprint read found a sprint, a cycle, a GitHub Projects iteration (the
|
|
41
|
+
* current one, docs/TRACKERS.md § GitHub Projects iterations) or a milestone with a due date; or
|
|
42
|
+
* - the workspace has no successful read yet and the tracker worker's SPRINT-SOURCE PROBE (GitHub
|
|
43
|
+
* Issues workspaces only, tracker/sprint/probe.ts — never run by a view) found a current
|
|
44
|
+
* iteration or a dated milestone; or
|
|
45
|
+
* - the workspace has a Jira, Linear or Notion tracker configured and no read has said otherwise
|
|
46
|
+
* yet (the read runs once This Sprint is opened; until then, a configured tracker counts).
|
|
47
|
+
*
|
|
48
|
+
* ⚠ THE EXTENSION POINT for a further source: add it to `isSprintSource`, and the Features default
|
|
49
|
+
* follows.
|
|
50
|
+
*/
|
|
51
|
+
export async function hasSprintSource(accountId) {
|
|
52
|
+
const sr = schema.sprintReads;
|
|
53
|
+
const reads = await db
|
|
54
|
+
.select({ workspaceId: sr.workspaceId, source: sr.sourceJson, readAt: sr.readAt, probe: sr.probeSourceJson })
|
|
55
|
+
.from(sr)
|
|
56
|
+
.where(eq(sr.accountId, accountId))
|
|
57
|
+
.execute();
|
|
58
|
+
if (reads.some((r) => isSprintSource(r.source)))
|
|
59
|
+
return true;
|
|
60
|
+
// The probe's answer counts only until a read exists: a read's own source is the answer then.
|
|
61
|
+
if (reads.some((r) => r.readAt == null && isSprintSource(r.probe)))
|
|
62
|
+
return true;
|
|
63
|
+
const readWorkspaces = new Set(reads.filter((r) => r.readAt != null && r.source != null).map((r) => r.workspaceId));
|
|
64
|
+
const wt = schema.workspaceTrackers;
|
|
65
|
+
const trackers = await db
|
|
66
|
+
.select({ workspaceId: wt.workspaceId })
|
|
67
|
+
.from(wt)
|
|
68
|
+
.where(and(eq(wt.accountId, accountId), inArray(wt.provider, [...SPRINT_TRACKERS])))
|
|
69
|
+
.execute();
|
|
70
|
+
return trackers.some((t) => !readWorkspaces.has(t.workspaceId));
|
|
71
|
+
}
|
|
72
|
+
/** Everything `resolveFeatures` needs, for one account. */
|
|
73
|
+
export async function featureFacts(accountId, existingInstall) {
|
|
74
|
+
// An existing install shows everything whatever the facts say: skip the reads.
|
|
75
|
+
if (existingInstall)
|
|
76
|
+
return { existingInstall, reportsReady: true, sprintSource: true };
|
|
77
|
+
const [reportsReady, sprintSource] = await Promise.all([hasCompletedBackfill(accountId), hasSprintSource(accountId)]);
|
|
78
|
+
return { existingInstall, reportsReady, sprintSource };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=feature-gates.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- FULL-TEXT SEARCH OVER THE REFERENCE LIBRARY (2026-11-01; docs/DOCS-RAG.md, docs/REFERENCE-DOCS.md
|
|
2
|
+
-- § Retrieval). CORE, local only. Additive.
|
|
3
|
+
--
|
|
4
|
+
-- reference_doc_chunks NEW: a built document's section text cut into passages of ≤ 220 words,
|
|
5
|
+
-- each prefixed with "Document › trail › section" and the section's
|
|
6
|
+
-- one-line summary. Rewritten whole by every build (reference-docs/chunks.ts);
|
|
7
|
+
-- DERIVED from the files on disk, so a missing row is rebuilt at boot.
|
|
8
|
+
-- Composite FKs onto the document and the workspace; cascades.
|
|
9
|
+
-- reference_doc_chunks_fts NEW FTS5 index over the chunks' text (external content, porter + unicode61).
|
|
10
|
+
-- ⚠ Drizzle cannot declare a virtual table or a trigger: they live HERE only.
|
|
11
|
+
-- The three triggers keep it in step on every insert, update and delete —
|
|
12
|
+
-- including the FK cascades from a deleted document, workspace or account.
|
|
13
|
+
CREATE TABLE `reference_doc_chunks` (
|
|
14
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
15
|
+
`account_id` integer NOT NULL,
|
|
16
|
+
`workspace_id` integer NOT NULL,
|
|
17
|
+
`doc_id` integer NOT NULL,
|
|
18
|
+
`built_at` integer NOT NULL,
|
|
19
|
+
`section_path` text NOT NULL,
|
|
20
|
+
`anchor` text,
|
|
21
|
+
`ordinal` integer NOT NULL,
|
|
22
|
+
`text` text NOT NULL,
|
|
23
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
24
|
+
CONSTRAINT `reference_doc_chunks_doc_account_fk` FOREIGN KEY (`doc_id`,`account_id`) REFERENCES `reference_docs`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade,
|
|
25
|
+
CONSTRAINT `reference_doc_chunks_workspace_account_fk` FOREIGN KEY (`workspace_id`,`account_id`) REFERENCES `workspaces`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
|
|
26
|
+
);
|
|
27
|
+
--> statement-breakpoint
|
|
28
|
+
CREATE INDEX `reference_doc_chunks_scope` ON `reference_doc_chunks` (`account_id`,`workspace_id`,`doc_id`);
|
|
29
|
+
--> statement-breakpoint
|
|
30
|
+
CREATE INDEX `reference_doc_chunks_doc` ON `reference_doc_chunks` (`doc_id`,`account_id`);
|
|
31
|
+
--> statement-breakpoint
|
|
32
|
+
CREATE VIRTUAL TABLE `reference_doc_chunks_fts` USING fts5(`text`, content='reference_doc_chunks', content_rowid='id', tokenize='porter unicode61');
|
|
33
|
+
--> statement-breakpoint
|
|
34
|
+
CREATE TRIGGER `reference_doc_chunks_fts_ai` AFTER INSERT ON `reference_doc_chunks` BEGIN
|
|
35
|
+
INSERT INTO `reference_doc_chunks_fts`(rowid, `text`) VALUES (new.`id`, new.`text`);
|
|
36
|
+
END;
|
|
37
|
+
--> statement-breakpoint
|
|
38
|
+
CREATE TRIGGER `reference_doc_chunks_fts_ad` AFTER DELETE ON `reference_doc_chunks` BEGIN
|
|
39
|
+
INSERT INTO `reference_doc_chunks_fts`(`reference_doc_chunks_fts`, rowid, `text`) VALUES ('delete', old.`id`, old.`text`);
|
|
40
|
+
END;
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
CREATE TRIGGER `reference_doc_chunks_fts_au` AFTER UPDATE ON `reference_doc_chunks` BEGIN
|
|
43
|
+
INSERT INTO `reference_doc_chunks_fts`(`reference_doc_chunks_fts`, rowid, `text`) VALUES ('delete', old.`id`, old.`text`);
|
|
44
|
+
INSERT INTO `reference_doc_chunks_fts`(rowid, `text`) VALUES (new.`id`, new.`text`);
|
|
45
|
+
END;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- SETTINGS → FEATURES (2026-11-01; docs/FRONTEND.md § Features shown, shared features.ts). CORE,
|
|
2
|
+
-- additive, visibility only.
|
|
3
|
+
--
|
|
4
|
+
-- accounts.feature_settings NEW: the reader's switches, overrides only (JSON). NULL = every
|
|
5
|
+
-- default applies. Written only by PUT /api/me/feature-settings.
|
|
6
|
+
-- accounts.features_existing_install NEW: 1 for an account that already had repositories when this
|
|
7
|
+
-- migration ran — its defaults show every destination, so an
|
|
8
|
+
-- existing install looks exactly as before. A new install
|
|
9
|
+
-- (an account created later, or one with no repos yet) is 0 and
|
|
10
|
+
-- starts with Bots Monitoring and Feed hidden.
|
|
11
|
+
ALTER TABLE `accounts` ADD `feature_settings` text;
|
|
12
|
+
--> statement-breakpoint
|
|
13
|
+
ALTER TABLE `accounts` ADD `features_existing_install` integer DEFAULT 0 NOT NULL;
|
|
14
|
+
--> statement-breakpoint
|
|
15
|
+
UPDATE `accounts` SET `features_existing_install` = 1
|
|
16
|
+
WHERE EXISTS (SELECT 1 FROM `repos` WHERE `repos`.`account_id` = `accounts`.`id`);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- PENDING ON THE FIRST LANDING (2026-11-01; docs/BACKEND.md § My Turn — The first-onboarding
|
|
2
|
+
-- lookback, docs/FRONTEND.md § Pending on the first landing). CORE, additive, no backfill.
|
|
3
|
+
--
|
|
4
|
+
-- repos.first_onboarding NEW: 1 for a repo added while the account had no repos,
|
|
5
|
+
-- or in that same first-run batch. Its My Turn floor is
|
|
6
|
+
-- its add time minus FIRST_ONBOARDING_LOOKBACK_DAYS; a
|
|
7
|
+
-- repo added later keeps its add time. Written once, at
|
|
8
|
+
-- insert (sync/upsert.ts upsertRepo), never updated.
|
|
9
|
+
-- accounts.review_suggestion_dismissed_at NEW: when the reader dismissed Pending's one-time
|
|
10
|
+
-- "Review your PR with Claude" card. NULL = never.
|
|
11
|
+
--
|
|
12
|
+
-- Existing repos stay 0: an install that already has repos is past its first landing.
|
|
13
|
+
ALTER TABLE `repos` ADD `first_onboarding` integer DEFAULT 0 NOT NULL;
|
|
14
|
+
--> statement-breakpoint
|
|
15
|
+
ALTER TABLE `accounts` ADD `review_suggestion_dismissed_at` integer;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- DOCS OFFERED FROM THE REPOSITORIES (2026-11-01; docs/REFERENCE-DOCS.md § Docs offered from your
|
|
2
|
+
-- repositories). CORE, additive, no backfill.
|
|
3
|
+
--
|
|
4
|
+
-- reference_doc_repo_offers NEW: one row per (workspace, repository, folder) found to look like a docs
|
|
5
|
+
-- collection. `state` = 'offered' (waiting for the reader), 'declined'
|
|
6
|
+
-- ("Not now": never offered again) or 'added' (a document was queued;
|
|
7
|
+
-- `doc_id` names it, kept after the document is removed so the folder is
|
|
8
|
+
-- not offered again). Nothing is built from a row until the reader presses
|
|
9
|
+
-- Add. Composite FKs onto the workspace and the repository; both cascade,
|
|
10
|
+
-- and repos' delete clears it explicitly too.
|
|
11
|
+
-- repos.docs_offer_scanned_at NEW: when the repository's tree was last looked at for docs folders.
|
|
12
|
+
-- NULL = never; looked at again after DOCS_OFFER_RESCAN_DAYS.
|
|
13
|
+
CREATE TABLE `reference_doc_repo_offers` (
|
|
14
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
15
|
+
`account_id` integer NOT NULL,
|
|
16
|
+
`workspace_id` integer NOT NULL,
|
|
17
|
+
`repo_id` integer NOT NULL,
|
|
18
|
+
`path` text NOT NULL,
|
|
19
|
+
`doc_count` integer NOT NULL,
|
|
20
|
+
`word_estimate` integer NOT NULL,
|
|
21
|
+
`page_count` integer NOT NULL,
|
|
22
|
+
`too_large` integer DEFAULT 0 NOT NULL,
|
|
23
|
+
`state` text DEFAULT 'offered' NOT NULL,
|
|
24
|
+
`doc_id` integer,
|
|
25
|
+
`detected_at` integer NOT NULL,
|
|
26
|
+
`decided_at` integer,
|
|
27
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
28
|
+
CONSTRAINT `reference_doc_repo_offers_workspace_account_fk` FOREIGN KEY (`workspace_id`,`account_id`) REFERENCES `workspaces`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade,
|
|
29
|
+
CONSTRAINT `reference_doc_repo_offers_repo_account_fk` FOREIGN KEY (`repo_id`,`account_id`) REFERENCES `repos`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
|
|
30
|
+
);
|
|
31
|
+
--> statement-breakpoint
|
|
32
|
+
CREATE UNIQUE INDEX `reference_doc_repo_offers_folder` ON `reference_doc_repo_offers` (`account_id`,`workspace_id`,`repo_id`,`path`);
|
|
33
|
+
--> statement-breakpoint
|
|
34
|
+
CREATE INDEX `reference_doc_repo_offers_scope` ON `reference_doc_repo_offers` (`account_id`,`workspace_id`,`state`);
|
|
35
|
+
--> statement-breakpoint
|
|
36
|
+
ALTER TABLE `repos` ADD `docs_offer_scanned_at` integer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- THE CI-HISTORY "STILL OWED" STAMP (2026-11-02; docs/SYNC.md § The CI-history backfill is owed
|
|
2
|
+
-- until it ran). CORE, additive, no backfill.
|
|
3
|
+
--
|
|
4
|
+
-- sync_state.ci_history_pending_at NEW: set when a completed FULL walk owes the one-time
|
|
5
|
+
-- CI-history backfill (before it is run or parked in the
|
|
6
|
+
-- in-memory queue); cleared once the backfill ran. A
|
|
7
|
+
-- non-NULL value at boot, or at the repo's next walk, runs
|
|
8
|
+
-- it then. NULL = nothing owed.
|
|
9
|
+
ALTER TABLE `sync_state` ADD `ci_history_pending_at` integer;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- THE SPRINT-SOURCE PROBE (2026-11-02; docs/TRACKERS.md § The sprint-source probe). CORE, additive, no
|
|
2
|
+
-- backfill.
|
|
3
|
+
--
|
|
4
|
+
-- sprint_reads.probe_checked_at NEW: when the tracker worker last asked GitHub whether this GitHub
|
|
5
|
+
-- Issues workspace has a current Projects iteration or an open
|
|
6
|
+
-- milestone with a due date. NULL = never. Asked again after
|
|
7
|
+
-- SPRINT_PROBE_INTERVAL_MS, and never once a sprint read exists.
|
|
8
|
+
-- sprint_reads.probe_source_json NEW: what the probe found (a `SprintSource`, kind 'iteration' or
|
|
9
|
+
-- 'milestone'), NULL = nothing. Read ONLY by `hasSprintSource` (the
|
|
10
|
+
-- This Sprint default in Settings → Features), and only while the
|
|
11
|
+
-- workspace has no successful sprint read — a read's own source wins.
|
|
12
|
+
--
|
|
13
|
+
-- A probe may CREATE the workspace's row (`viewed_at` = epoch 0, `next_read_at` = epoch 0): the demand
|
|
14
|
+
-- gate is unchanged — a row whose tab was never opened is never read.
|
|
15
|
+
ALTER TABLE `sprint_reads` ADD `probe_checked_at` integer;
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
ALTER TABLE `sprint_reads` ADD `probe_source_json` text;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- A REPOSITORY WHOSE FILES CAN'T BE READ (2026-11-02; docs/REFERENCE-DOCS.md § A repository whose files
|
|
2
|
+
-- can't be read). CORE, additive, no backfill.
|
|
3
|
+
--
|
|
4
|
+
-- repos.docs_offer_failed_at NEW: when the docs-offer look at the repository's tree last FAILED. NULL =
|
|
5
|
+
-- the last look succeeded (or none failed). While set, the repository is not
|
|
6
|
+
-- looked at again until DOCS_OFFER_FAILURE_RETRY_HOURS have passed (then at
|
|
7
|
+
-- most once a day). Cleared by the next look that succeeds.
|
|
8
|
+
-- repos.docs_offer_failure NEW: why, as a code: 'no_access' | 'empty' | 'not_found' | 'unavailable'.
|
|
9
|
+
-- Set and cleared with `docs_offer_failed_at`. Never touches the PR sync.
|
|
10
|
+
ALTER TABLE `repos` ADD `docs_offer_failed_at` integer;
|
|
11
|
+
--> statement-breakpoint
|
|
12
|
+
ALTER TABLE `repos` ADD `docs_offer_failure` text;
|
|
@@ -1065,6 +1065,55 @@
|
|
|
1065
1065
|
"when": 1792224054016,
|
|
1066
1066
|
"tag": "0151_limn_finding_bots",
|
|
1067
1067
|
"breakpoints": true
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
"idx": 152,
|
|
1071
|
+
"version": "6",
|
|
1072
|
+
"when": 1792224055016,
|
|
1073
|
+
"tag": "0152_reference_doc_chunks",
|
|
1074
|
+
"breakpoints": true
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
"idx": 153,
|
|
1078
|
+
"version": "6",
|
|
1079
|
+
"when": 1792224056016,
|
|
1080
|
+
"tag": "0153_feature_visibility",
|
|
1081
|
+
"breakpoints": true
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
"idx": 154,
|
|
1085
|
+
"version": "6",
|
|
1086
|
+
"when": 1792224057016,
|
|
1087
|
+
"tag": "0154_first_landing",
|
|
1088
|
+
"breakpoints": true
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
"idx": 155,
|
|
1092
|
+
"version": "6",
|
|
1093
|
+
"when": 1792224058016,
|
|
1094
|
+
"tag": "0155_repo_docs_offers",
|
|
1095
|
+
"breakpoints": true
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
"idx": 156,
|
|
1099
|
+
"version": "6",
|
|
1100
|
+
"when": 1792224059016,
|
|
1101
|
+
"tag": "0156_ci_history_pending",
|
|
1102
|
+
"breakpoints": true
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
"idx": 157,
|
|
1106
|
+
"version": "6",
|
|
1107
|
+
"when": 1792224060016,
|
|
1108
|
+
"tag": "0157_sprint_source_probe",
|
|
1109
|
+
"breakpoints": true
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
"idx": 158,
|
|
1113
|
+
"version": "6",
|
|
1114
|
+
"when": 1792224061016,
|
|
1115
|
+
"tag": "0158_repo_docs_unreadable",
|
|
1116
|
+
"breakpoints": true
|
|
1068
1117
|
}
|
|
1069
1118
|
]
|
|
1070
1119
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// ── MY TURN'S ONBOARDING FLOOR (docs/BACKEND.md § My Turn — The first-onboarding lookback) ──────
|
|
2
|
+
//
|
|
3
|
+
// An event before a repo's floor never summons anyone: without it, adding a repo and its 90-day
|
|
4
|
+
// backfill would surface two months of old replies, mentions and pushes at once. The floor is the
|
|
5
|
+
// repo's add time (`repos.createdAt`) — except for a repo of the account's FIRST onboarding
|
|
6
|
+
// (`repos.first_onboarding`), whose floor sits `FIRST_ONBOARDING_LOOKBACK_DAYS` earlier, so the
|
|
7
|
+
// first landing on Pending shows the last week's open business instead of nothing.
|
|
8
|
+
//
|
|
9
|
+
// ⚠ ONE spelling: `getMyTurn` builds its per-repo floor map through this and every rule that reads
|
|
10
|
+
// the floor (S2, S3c, S3d, S5, S6) reads that map. Never re-derive it from `createdAt` elsewhere.
|
|
11
|
+
import { FIRST_ONBOARDING_LOOKBACK_DAYS } from '../shared/index.js';
|
|
12
|
+
const DAY_MS = 86_400_000;
|
|
13
|
+
export function myTurnFloorOf(repo) {
|
|
14
|
+
if (!repo.firstOnboarding)
|
|
15
|
+
return repo.createdAt;
|
|
16
|
+
return new Date(repo.createdAt.getTime() - FIRST_ONBOARDING_LOOKBACK_DAYS * DAY_MS);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=my-turn-floor.js.map
|