@pratik7368patil/anchor-core 0.1.23 → 0.1.24

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/index.js CHANGED
@@ -1541,10 +1541,17 @@ function defaultDatabasePath(cwd) {
1541
1541
  function openAnchorDatabase(cwd, databasePath = defaultDatabasePath(cwd)) {
1542
1542
  fs3.mkdirSync(path4.dirname(databasePath), { recursive: true });
1543
1543
  const db = new Database(databasePath);
1544
+ db.pragma("busy_timeout = 5000");
1544
1545
  db.pragma("journal_mode = WAL");
1545
1546
  db.pragma("foreign_keys = ON");
1546
1547
  return db;
1547
1548
  }
1549
+ function openAnchorDatabaseReadOnly(databasePath) {
1550
+ const db = new Database(databasePath, { readonly: true, fileMustExist: true });
1551
+ db.pragma("busy_timeout = 5000");
1552
+ db.pragma("foreign_keys = ON");
1553
+ return db;
1554
+ }
1548
1555
  function initializeSchema(db) {
1549
1556
  db.exec(SCHEMA_SQL);
1550
1557
  ensureColumn(db, "sync_state", "history_coverage", "TEXT");
@@ -3101,6 +3108,18 @@ function indexCodebase(db, options) {
3101
3108
  architecture
3102
3109
  );
3103
3110
  refreshTestCommands(db, options.cwd, options.repo);
3111
+ options.onProgress?.({
3112
+ stage: "completed_code_index",
3113
+ repo: options.repo,
3114
+ files: summary.indexedFiles,
3115
+ chunks: summary.codeChunksCreated,
3116
+ skippedFiles: summary.skippedFiles,
3117
+ testFiles: summary.testFilesIndexed,
3118
+ testLinks: summary.testLinksCreated,
3119
+ architectureComponents: summary.architectureComponentsIndexed,
3120
+ architecturePatterns: summary.architecturePatternsIndexed,
3121
+ architectureImports: summary.architectureImportsIndexed
3122
+ });
3104
3123
  return summary;
3105
3124
  }
3106
3125
  function emptyCodeIndexSummary(cwd) {
@@ -3425,7 +3444,8 @@ function indexPullRequests(db, pullRequests, options) {
3425
3444
  current: index + 1,
3426
3445
  total: pullRequests.length,
3427
3446
  prNumber: pr.number,
3428
- wisdomUnitsCreated: result.wisdom
3447
+ wisdomUnitsCreated: result.wisdom,
3448
+ regressionEventsCreated: result.regressions
3429
3449
  });
3430
3450
  }
3431
3451
  if (options.updateSyncStateAfter !== false) {
@@ -6292,6 +6312,18 @@ function parseGraphQLResponse(text, status, headers) {
6292
6312
  );
6293
6313
  }
6294
6314
  }
6315
+ function sleep2(ms) {
6316
+ return new Promise((resolve) => setTimeout(resolve, ms));
6317
+ }
6318
+ function isTransientGraphQLError(error) {
6319
+ const status = error.status;
6320
+ if (status === 502 || status === 503 || status === 504) return true;
6321
+ const message = (error.message ?? "").toLowerCase();
6322
+ return message.includes("fetch failed") || message.includes("econnreset") || message.includes("etimedout") || message.includes("socket hang up") || message.includes("network") || message.includes("non-json response") && (message.includes("text/html") || message.includes("<!doctype") || message.includes("<html") || typeof status === "number" && status >= 500);
6323
+ }
6324
+ function transientRetryDelayMs(attempt) {
6325
+ return Math.min(4e3, 500 * 2 ** Math.max(0, attempt - 1));
6326
+ }
6295
6327
  function createGitHubGraphQLRequester(options) {
6296
6328
  if (!options.token.trim()) {
6297
6329
  throw new Error(
@@ -6303,36 +6335,55 @@ function createGitHubGraphQLRequester(options) {
6303
6335
  return async function requestGitHubGraphQL(query, variables, requestOptions) {
6304
6336
  return requestWithGitHubRateLimit(
6305
6337
  async () => {
6306
- const response = await fetchImpl("https://api.github.com/graphql", {
6307
- method: "POST",
6308
- headers: {
6309
- accept: "application/vnd.github+json",
6310
- authorization: `Bearer ${options.token}`,
6311
- "content-type": "application/json",
6312
- "user-agent": "anchor-local-mcp"
6313
- },
6314
- body: JSON.stringify({ query, variables })
6315
- });
6316
- const headers = headersToRecord(response.headers);
6317
- const raw = parseGraphQLResponse(await response.text(), response.status, headers);
6318
- if (!response.ok || raw.errors?.length) {
6319
- throw new GitHubGraphQLError(errorMessage(response.status, raw.errors), {
6320
- status: errorStatus(response.status, raw.errors),
6321
- headers
6322
- });
6323
- }
6324
- if (!raw.data) {
6325
- throw new GitHubGraphQLError("GitHub GraphQL response did not include data.", {
6326
- status: response.status,
6327
- headers
6328
- });
6338
+ const maxAttempts = Math.max(1, requestOptions.maxTransientRetries ?? 3);
6339
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
6340
+ try {
6341
+ const response = await fetchImpl("https://api.github.com/graphql", {
6342
+ method: "POST",
6343
+ headers: {
6344
+ accept: "application/vnd.github+json",
6345
+ authorization: `Bearer ${options.token}`,
6346
+ "content-type": "application/json",
6347
+ "user-agent": "anchor-local-mcp"
6348
+ },
6349
+ body: JSON.stringify({ query, variables })
6350
+ });
6351
+ const headers = headersToRecord(response.headers);
6352
+ const raw = parseGraphQLResponse(await response.text(), response.status, headers);
6353
+ if (!response.ok || raw.errors?.length) {
6354
+ throw new GitHubGraphQLError(errorMessage(response.status, raw.errors), {
6355
+ status: errorStatus(response.status, raw.errors),
6356
+ headers
6357
+ });
6358
+ }
6359
+ if (!raw.data) {
6360
+ throw new GitHubGraphQLError("GitHub GraphQL response did not include data.", {
6361
+ status: response.status,
6362
+ headers
6363
+ });
6364
+ }
6365
+ updateGitHubGraphQLRateLimitState(
6366
+ requestOptions.controller,
6367
+ raw.data.rateLimit,
6368
+ requestOptions.requestName
6369
+ );
6370
+ return { data: raw.data, headers };
6371
+ } catch (error) {
6372
+ if (attempt >= maxAttempts || !isTransientGraphQLError(error)) throw error;
6373
+ const waitMs = transientRetryDelayMs(attempt);
6374
+ requestOptions.onTransientRetry?.({
6375
+ attempt,
6376
+ maxAttempts,
6377
+ waitMs,
6378
+ reason: error instanceof Error ? error.message : String(error)
6379
+ });
6380
+ await sleep2(waitMs);
6381
+ }
6329
6382
  }
6330
- updateGitHubGraphQLRateLimitState(
6331
- requestOptions.controller,
6332
- raw.data.rateLimit,
6333
- requestOptions.requestName
6334
- );
6335
- return { data: raw.data, headers };
6383
+ throw new GitHubGraphQLError("GitHub GraphQL request retry loop exited unexpectedly.", {
6384
+ status: 500,
6385
+ headers: {}
6386
+ });
6336
6387
  },
6337
6388
  {
6338
6389
  controller: requestOptions.controller,
@@ -6518,6 +6569,16 @@ var GraphQLBudget = class {
6518
6569
  };
6519
6570
  }
6520
6571
  };
6572
+ function graphqlRetryProgress(repo, onProgress) {
6573
+ return (retry) => onProgress?.({
6574
+ stage: "github_graphql_retry",
6575
+ repo,
6576
+ attempt: retry.attempt,
6577
+ maxAttempts: retry.maxAttempts,
6578
+ waitMs: retry.waitMs,
6579
+ reason: retry.reason
6580
+ });
6581
+ }
6521
6582
  var PULL_REQUEST_FIELDS = `
6522
6583
  number
6523
6584
  url
@@ -6655,7 +6716,8 @@ function connectionNodes(connection) {
6655
6716
  async function requestGraphQLWithBudget(requestGraphQL, query, variables, options) {
6656
6717
  const response = await requestGraphQL(query, variables, {
6657
6718
  controller: options.controller,
6658
- requestName: options.requestName
6719
+ requestName: options.requestName,
6720
+ onTransientRetry: options.onTransientRetry
6659
6721
  });
6660
6722
  options.budget.observe(response.data.rateLimit);
6661
6723
  return response;
@@ -6741,7 +6803,8 @@ async function appendAdditionalFiles(requestGraphQL, record, initialConnection,
6741
6803
  {
6742
6804
  controller: options.controller,
6743
6805
  requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/files`,
6744
- budget: options.budget
6806
+ budget: options.budget,
6807
+ onTransientRetry: options.onTransientRetry
6745
6808
  }
6746
6809
  );
6747
6810
  record.files.push(
@@ -6767,7 +6830,8 @@ async function appendAdditionalIssueComments(requestGraphQL, record, initialConn
6767
6830
  {
6768
6831
  controller: options.controller,
6769
6832
  requestName: `GraphQL /repos/${record.repo}/issues/${record.number}/comments`,
6770
- budget: options.budget
6833
+ budget: options.budget,
6834
+ onTransientRetry: options.onTransientRetry
6771
6835
  }
6772
6836
  );
6773
6837
  record.issueComments?.push(...connectionNodes(connection).map(mapIssueComment));
@@ -6791,7 +6855,8 @@ async function appendAdditionalCommits(requestGraphQL, record, initialConnection
6791
6855
  {
6792
6856
  controller: options.controller,
6793
6857
  requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/commits`,
6794
- budget: options.budget
6858
+ budget: options.budget,
6859
+ onTransientRetry: options.onTransientRetry
6795
6860
  }
6796
6861
  );
6797
6862
  record.commits?.push(
@@ -6816,7 +6881,8 @@ async function appendAdditionalReviewComments(requestGraphQL, record, review, op
6816
6881
  {
6817
6882
  controller: options.controller,
6818
6883
  requestName: `GraphQL /pull-request-reviews/${review.id}/comments`,
6819
- budget: options.budget
6884
+ budget: options.budget,
6885
+ onTransientRetry: options.onTransientRetry
6820
6886
  }
6821
6887
  );
6822
6888
  const connection = response.data.node?.comments;
@@ -6842,7 +6908,8 @@ async function appendAdditionalReviews(requestGraphQL, record, initialConnection
6842
6908
  {
6843
6909
  controller: options.controller,
6844
6910
  requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/reviews`,
6845
- budget: options.budget
6911
+ budget: options.budget,
6912
+ onTransientRetry: options.onTransientRetry
6846
6913
  }
6847
6914
  );
6848
6915
  const reviewNodes = connectionNodes(connection);
@@ -6856,7 +6923,8 @@ async function appendAdditionalReviews(requestGraphQL, record, initialConnection
6856
6923
  for (const review of reviewsToHydrate) {
6857
6924
  await appendAdditionalReviewComments(requestGraphQL, record, review, {
6858
6925
  controller: options.controller,
6859
- budget: options.budget
6926
+ budget: options.budget,
6927
+ onTransientRetry: options.onTransientRetry
6860
6928
  });
6861
6929
  }
6862
6930
  }
@@ -7012,6 +7080,7 @@ async function fetchMergedPullRequestsWithGraphQL(options) {
7012
7080
  checkpoint?.pageSize ?? Math.min(INITIAL_PULL_REQUEST_PAGE_SIZE, options.limit ?? INITIAL_PULL_REQUEST_PAGE_SIZE)
7013
7081
  );
7014
7082
  const budget = new GraphQLBudget(GRAPHQL_RATE_LIMIT_RESERVE);
7083
+ const onTransientRetry = graphqlRetryProgress(options.repo, options.onProgress);
7015
7084
  const checkpointScope = checkpoint?.scope ?? `${options.repo}|${options.limit === void 0 ? "all" : `limit:${options.limit}`}|since:${options.since ?? ""}`;
7016
7085
  options.onProgress?.({
7017
7086
  stage: "discovering_pull_requests",
@@ -7038,7 +7107,8 @@ async function fetchMergedPullRequestsWithGraphQL(options) {
7038
7107
  {
7039
7108
  controller: options.controller,
7040
7109
  requestName: "GraphQL rate limit preflight",
7041
- budget
7110
+ budget,
7111
+ onTransientRetry
7042
7112
  }
7043
7113
  );
7044
7114
  const preflightRateLimit = budget.rateLimit();
@@ -7100,7 +7170,8 @@ async function fetchMergedPullRequestsWithGraphQL(options) {
7100
7170
  {
7101
7171
  controller: options.controller,
7102
7172
  requestName: `GraphQL /repos/${options.repo}/pullRequests`,
7103
- budget
7173
+ budget,
7174
+ onTransientRetry
7104
7175
  }
7105
7176
  );
7106
7177
  } catch (error) {
@@ -7131,7 +7202,8 @@ async function fetchMergedPullRequestsWithGraphQL(options) {
7131
7202
  owner,
7132
7203
  name,
7133
7204
  controller: options.controller,
7134
- budget
7205
+ budget,
7206
+ onTransientRetry
7135
7207
  });
7136
7208
  records.push(record);
7137
7209
  if (options.limit !== void 0 && records.length >= options.limit) break;
@@ -7553,6 +7625,9 @@ function openOrgDatabase(org, baseDir) {
7553
7625
  initializeSchema(db);
7554
7626
  return db;
7555
7627
  }
7628
+ function openOrgDatabaseReadOnly(org, baseDir) {
7629
+ return openAnchorDatabaseReadOnly(orgDatabasePath(org, baseDir));
7630
+ }
7556
7631
  function syncOrgConfigToDatabase(db, config, baseDir) {
7557
7632
  initializeSchema(db);
7558
7633
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -7730,9 +7805,11 @@ function grade(score) {
7730
7805
  if (score < 80) return "good";
7731
7806
  return "excellent";
7732
7807
  }
7733
- function getOrgStatus(db, config, baseDir) {
7734
- initializeSchema(db);
7735
- syncOrgConfigToDatabase(db, config, baseDir);
7808
+ function getOrgStatus(db, config, baseDir, options = {}) {
7809
+ if (options.syncConfig !== false) {
7810
+ initializeSchema(db);
7811
+ syncOrgConfigToDatabase(db, config, baseDir);
7812
+ }
7736
7813
  const enabledRepos = config.repos.filter((repo) => repo.enabled);
7737
7814
  const states = new Map(
7738
7815
  db.prepare("SELECT * FROM org_repo_state WHERE org = ?").all(config.org).map((row) => [row.repo, row])
@@ -7779,6 +7856,8 @@ function getOrgStatus(db, config, baseDir) {
7779
7856
  org: config.org,
7780
7857
  root: orgRoot(config.org, baseDir),
7781
7858
  databasePath: orgDatabasePath(config.org, baseDir),
7859
+ statusReadError: options.statusReadError,
7860
+ activeRun: options.activeRun,
7782
7861
  repoCount: config.repos.length,
7783
7862
  enabledRepoCount: enabledRepos.length,
7784
7863
  clonedRepoCount,
@@ -7813,10 +7892,84 @@ function getOrgStatus(db, config, baseDir) {
7813
7892
  };
7814
7893
  }
7815
7894
 
7816
- // src/org/clone.ts
7817
- import { execFileSync as execFileSync4 } from "child_process";
7895
+ // src/org/heartbeat.ts
7818
7896
  import fs11 from "fs";
7819
7897
  import path21 from "path";
7898
+ var HEARTBEAT_STALE_AFTER_MS = 2 * 60 * 1e3;
7899
+ function orgHeartbeatPath(org, baseDir) {
7900
+ return path21.join(orgRoot(validateOrgName(org), baseDir), "sync-heartbeat.json");
7901
+ }
7902
+ function atomicWriteJson2(filePath, value) {
7903
+ fs11.mkdirSync(path21.dirname(filePath), { recursive: true });
7904
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
7905
+ fs11.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
7906
+ `, { mode: 384 });
7907
+ fs11.renameSync(tmp, filePath);
7908
+ }
7909
+ function processIsRunning(pid) {
7910
+ if (!Number.isInteger(pid) || pid <= 0) return false;
7911
+ try {
7912
+ process.kill(pid, 0);
7913
+ return true;
7914
+ } catch {
7915
+ return false;
7916
+ }
7917
+ }
7918
+ function parseHeartbeat(value) {
7919
+ if (!value || typeof value !== "object") return void 0;
7920
+ const candidate = value;
7921
+ if (typeof candidate.pid !== "number" || typeof candidate.command !== "string" || typeof candidate.org !== "string" || typeof candidate.phase !== "string" || typeof candidate.startedAt !== "string" || typeof candidate.updatedAt !== "string") {
7922
+ return void 0;
7923
+ }
7924
+ return {
7925
+ pid: candidate.pid,
7926
+ command: candidate.command,
7927
+ org: candidate.org,
7928
+ repo: typeof candidate.repo === "string" ? candidate.repo : void 0,
7929
+ repoIndex: typeof candidate.repoIndex === "number" ? candidate.repoIndex : void 0,
7930
+ repoTotal: typeof candidate.repoTotal === "number" ? candidate.repoTotal : void 0,
7931
+ phase: candidate.phase,
7932
+ startedAt: candidate.startedAt,
7933
+ updatedAt: candidate.updatedAt
7934
+ };
7935
+ }
7936
+ function writeOrgHeartbeat(heartbeat, baseDir) {
7937
+ atomicWriteJson2(orgHeartbeatPath(heartbeat.org, baseDir), heartbeat);
7938
+ }
7939
+ function clearOrgHeartbeat(org, baseDir) {
7940
+ try {
7941
+ const filePath = orgHeartbeatPath(org, baseDir);
7942
+ if (fs11.existsSync(filePath)) fs11.unlinkSync(filePath);
7943
+ } catch {
7944
+ }
7945
+ }
7946
+ function readOrgHeartbeat(org, baseDir) {
7947
+ const filePath = orgHeartbeatPath(org, baseDir);
7948
+ if (!fs11.existsSync(filePath)) return void 0;
7949
+ try {
7950
+ const heartbeat = parseHeartbeat(JSON.parse(fs11.readFileSync(filePath, "utf8")));
7951
+ if (!heartbeat) return void 0;
7952
+ const now = Date.now();
7953
+ const startedAtMs = Date.parse(heartbeat.startedAt);
7954
+ const updatedAtMs = Date.parse(heartbeat.updatedAt);
7955
+ const pidRunning = processIsRunning(heartbeat.pid);
7956
+ const lastUpdateAgeSeconds = Number.isFinite(updatedAtMs) ? Math.max(0, Math.floor((now - updatedAtMs) / 1e3)) : 0;
7957
+ return {
7958
+ ...heartbeat,
7959
+ pidRunning,
7960
+ stale: !pidRunning || !Number.isFinite(updatedAtMs) || now - updatedAtMs > HEARTBEAT_STALE_AFTER_MS,
7961
+ elapsedSeconds: Number.isFinite(startedAtMs) ? Math.max(0, Math.floor((now - startedAtMs) / 1e3)) : 0,
7962
+ lastUpdateAgeSeconds
7963
+ };
7964
+ } catch {
7965
+ return void 0;
7966
+ }
7967
+ }
7968
+
7969
+ // src/org/clone.ts
7970
+ import { execFileSync as execFileSync4 } from "child_process";
7971
+ import fs12 from "fs";
7972
+ import path22 from "path";
7820
7973
  function defaultGitCommandRunner(command, args, options = {}) {
7821
7974
  return execFileSync4(command, args, {
7822
7975
  cwd: options.cwd,
@@ -7832,7 +7985,7 @@ function currentCommit(runner, localPath) {
7832
7985
  }
7833
7986
  }
7834
7987
  function plannedOrgCloneCommands(repo, localPath) {
7835
- if (!fs11.existsSync(path21.join(localPath, ".git"))) {
7988
+ if (!fs12.existsSync(path22.join(localPath, ".git"))) {
7836
7989
  return [
7837
7990
  {
7838
7991
  command: "git",
@@ -7861,8 +8014,8 @@ function plannedOrgCloneCommands(repo, localPath) {
7861
8014
  function cloneOrPullOrgRepo(input) {
7862
8015
  const runner = input.runner ?? defaultGitCommandRunner;
7863
8016
  const localPath = orgRepoLocalPath(input.org, input.repo, input.baseDir);
7864
- const existed = fs11.existsSync(path21.join(localPath, ".git"));
7865
- fs11.mkdirSync(path21.dirname(localPath), { recursive: true });
8017
+ const existed = fs12.existsSync(path22.join(localPath, ".git"));
8018
+ fs12.mkdirSync(path22.dirname(localPath), { recursive: true });
7866
8019
  const now = (/* @__PURE__ */ new Date()).toISOString();
7867
8020
  try {
7868
8021
  const commands = plannedOrgCloneCommands(input.repo, localPath);
@@ -7963,8 +8116,8 @@ function orgCloneStateFromResult(org, repo, result) {
7963
8116
 
7964
8117
  // src/org/graph.ts
7965
8118
  import crypto9 from "crypto";
7966
- import fs12 from "fs";
7967
- import path22 from "path";
8119
+ import fs13 from "fs";
8120
+ import path23 from "path";
7968
8121
  function stableId(parts) {
7969
8122
  return crypto9.createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
7970
8123
  }
@@ -7978,10 +8131,10 @@ function fileEvidence(repo, filePath, note) {
7978
8131
  };
7979
8132
  }
7980
8133
  function readPackageManifest(repoPath) {
7981
- const packagePath = path22.join(repoPath, "package.json");
7982
- if (!fs12.existsSync(packagePath)) return void 0;
8134
+ const packagePath = path23.join(repoPath, "package.json");
8135
+ if (!fs13.existsSync(packagePath)) return void 0;
7983
8136
  try {
7984
- return JSON.parse(fs12.readFileSync(packagePath, "utf8"));
8137
+ return JSON.parse(fs13.readFileSync(packagePath, "utf8"));
7985
8138
  } catch {
7986
8139
  return void 0;
7987
8140
  }
@@ -8274,7 +8427,11 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8274
8427
  const insertEdge = db.prepare(
8275
8428
  `INSERT INTO org_cross_repo_edges
8276
8429
  (id, org, source_repo, source_path, target_repo, target_path, relationship, evidence_json, confidence, created_at)
8277
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8430
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8431
+ ON CONFLICT(id) DO UPDATE SET
8432
+ evidence_json = excluded.evidence_json,
8433
+ confidence = excluded.confidence,
8434
+ created_at = excluded.created_at`
8278
8435
  );
8279
8436
  for (const edge of edges) {
8280
8437
  insertEdge.run(
@@ -8293,7 +8450,12 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8293
8450
  const insertContract = db.prepare(
8294
8451
  `INSERT INTO org_api_contracts
8295
8452
  (id, org, repo, file_path, contract, evidence_json, confidence, created_at)
8296
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
8453
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
8454
+ ON CONFLICT(id) DO UPDATE SET
8455
+ contract = excluded.contract,
8456
+ evidence_json = excluded.evidence_json,
8457
+ confidence = excluded.confidence,
8458
+ created_at = excluded.created_at`
8297
8459
  );
8298
8460
  for (const contract of apiContracts) {
8299
8461
  insertContract.run(
@@ -8310,7 +8472,12 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8310
8472
  const insertConsumer = db.prepare(
8311
8473
  `INSERT INTO org_api_consumers
8312
8474
  (id, org, provider_repo, provider_path, consumer_repo, consumer_path, contract, evidence_json, confidence, created_at)
8313
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8475
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8476
+ ON CONFLICT(id) DO UPDATE SET
8477
+ contract = excluded.contract,
8478
+ evidence_json = excluded.evidence_json,
8479
+ confidence = excluded.confidence,
8480
+ created_at = excluded.created_at`
8314
8481
  );
8315
8482
  for (const consumer of apiConsumers) {
8316
8483
  insertConsumer.run(
@@ -8373,7 +8540,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8373
8540
  }
8374
8541
 
8375
8542
  // src/org/index.ts
8376
- import fs13 from "fs";
8543
+ import fs14 from "fs";
8377
8544
  var ORG_SYNC_RESUME_WINDOW_MS = 12 * 60 * 60 * 1e3;
8378
8545
  function readCommit(runner, cwd) {
8379
8546
  try {
@@ -8417,14 +8584,42 @@ async function indexOrgRepos(db, config, options = {}) {
8417
8584
  const auth = options.token ? { token: options.token } : resolveGitHubToken();
8418
8585
  const results = [];
8419
8586
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8587
+ const startedAtMs = Date.now();
8420
8588
  const graphState = getOrgGraphState(db, config.org);
8421
- for (const repo of repos) {
8589
+ const command = options.command ?? "org index";
8590
+ const emit = (progress) => options.onLifecycleProgress?.(progress);
8591
+ emit({
8592
+ stage: "org_sync_started",
8593
+ org: config.org,
8594
+ command,
8595
+ totalRepos: repos.length
8596
+ });
8597
+ for (const [repoIndex, repo] of repos.entries()) {
8598
+ const repoPosition = repoIndex + 1;
8422
8599
  const localPath = orgRepoLocalPath(config.org, repo, options.baseDir);
8423
8600
  const repoStartedAt = (/* @__PURE__ */ new Date()).toISOString();
8601
+ const repoStartedAtMs = Date.now();
8424
8602
  let prsIndexed = 0;
8425
8603
  let codeFilesIndexed = 0;
8426
8604
  try {
8427
- if (!fs13.existsSync(localPath)) throw new Error(missingCloneError(repo.fullName, localPath));
8605
+ emit({
8606
+ stage: "org_repo_started",
8607
+ org: config.org,
8608
+ command,
8609
+ repo: repo.fullName,
8610
+ current: repoPosition,
8611
+ total: repos.length
8612
+ });
8613
+ if (!fs14.existsSync(localPath)) throw new Error(missingCloneError(repo.fullName, localPath));
8614
+ emit({
8615
+ stage: "org_repo_phase",
8616
+ org: config.org,
8617
+ command,
8618
+ repo: repo.fullName,
8619
+ current: repoPosition,
8620
+ total: repos.length,
8621
+ phase: "Reading current commit"
8622
+ });
8428
8623
  const currentCommit2 = readCommit(runner, localPath);
8429
8624
  const state = getOrgRepoState(db, config.org, repo.fullName);
8430
8625
  let history;
@@ -8442,6 +8637,15 @@ async function indexOrgRepos(db, config, options = {}) {
8442
8637
  })) {
8443
8638
  skippedHistory = true;
8444
8639
  historySkippedReason = "PR history already synced; resuming unfinished org graph/index work.";
8640
+ emit({
8641
+ stage: "org_repo_skipped_history",
8642
+ org: config.org,
8643
+ command,
8644
+ repo: repo.fullName,
8645
+ current: repoPosition,
8646
+ total: repos.length,
8647
+ reason: historySkippedReason
8648
+ });
8445
8649
  options.onFetchProgress?.({
8446
8650
  stage: "skipped_pull_request_fetch",
8447
8651
  repo: repo.fullName,
@@ -8453,6 +8657,15 @@ async function indexOrgRepos(db, config, options = {}) {
8453
8657
  );
8454
8658
  } else {
8455
8659
  try {
8660
+ emit({
8661
+ stage: "org_repo_phase",
8662
+ org: config.org,
8663
+ command,
8664
+ repo: repo.fullName,
8665
+ current: repoPosition,
8666
+ total: repos.length,
8667
+ phase: "Fetching PR history"
8668
+ });
8456
8669
  const since = options.since ?? (options.command === "org sync" ? state?.lastPrSyncAt ?? getLastSyncTime(db, repo.fullName) : void 0);
8457
8670
  const pullRequests = await fetchPullRequests({
8458
8671
  token: auth.token,
@@ -8462,6 +8675,16 @@ async function indexOrgRepos(db, config, options = {}) {
8462
8675
  detailConcurrency: options.concurrency,
8463
8676
  onProgress: options.onFetchProgress
8464
8677
  });
8678
+ emit({
8679
+ stage: "org_repo_phase",
8680
+ org: config.org,
8681
+ command,
8682
+ repo: repo.fullName,
8683
+ current: repoPosition,
8684
+ total: repos.length,
8685
+ phase: "Indexing PR history into SQLite",
8686
+ detail: `${pullRequests.length} PR(s)`
8687
+ });
8465
8688
  history = indexPullRequests(db, pullRequests, {
8466
8689
  cwd: localPath,
8467
8690
  repo: repo.fullName,
@@ -8486,6 +8709,15 @@ async function indexOrgRepos(db, config, options = {}) {
8486
8709
  }
8487
8710
  const codeUnchanged = !options.force && currentCommit2 && state?.lastCodeIndexedCommit && currentCommit2 === state.lastCodeIndexedCommit;
8488
8711
  if (!options.prsOnly && !codeUnchanged) {
8712
+ emit({
8713
+ stage: "org_repo_phase",
8714
+ org: config.org,
8715
+ command,
8716
+ repo: repo.fullName,
8717
+ current: repoPosition,
8718
+ total: repos.length,
8719
+ phase: "Indexing code and architecture"
8720
+ });
8489
8721
  code = indexCodebase(db, {
8490
8722
  cwd: localPath,
8491
8723
  repo: repo.fullName,
@@ -8501,6 +8733,26 @@ async function indexOrgRepos(db, config, options = {}) {
8501
8733
  lastCodeIndexedCommit: currentCommit2,
8502
8734
  lastCodeIndexedAt: (/* @__PURE__ */ new Date()).toISOString()
8503
8735
  });
8736
+ } else if (!options.prsOnly && codeUnchanged) {
8737
+ emit({
8738
+ stage: "org_repo_skipped_code",
8739
+ org: config.org,
8740
+ command,
8741
+ repo: repo.fullName,
8742
+ current: repoPosition,
8743
+ total: repos.length,
8744
+ reason: "Code skipped: current commit already indexed."
8745
+ });
8746
+ } else if (options.prsOnly) {
8747
+ emit({
8748
+ stage: "org_repo_skipped_code",
8749
+ org: config.org,
8750
+ command,
8751
+ repo: repo.fullName,
8752
+ current: repoPosition,
8753
+ total: repos.length,
8754
+ reason: "Code skipped because --prs-only was passed."
8755
+ });
8504
8756
  }
8505
8757
  if (repoFailures.length > 0) {
8506
8758
  updateOrgRepoState(db, {
@@ -8512,6 +8764,14 @@ async function indexOrgRepos(db, config, options = {}) {
8512
8764
  lastError: repoFailures.join("; ")
8513
8765
  });
8514
8766
  }
8767
+ emit({
8768
+ stage: "org_repo_finalizing",
8769
+ org: config.org,
8770
+ command,
8771
+ repo: repo.fullName,
8772
+ current: repoPosition,
8773
+ total: repos.length
8774
+ });
8515
8775
  results.push({
8516
8776
  repo: repo.fullName,
8517
8777
  skippedCode: Boolean(codeUnchanged || options.prsOnly),
@@ -8525,7 +8785,7 @@ async function indexOrgRepos(db, config, options = {}) {
8525
8785
  recordOrgIndexRun(db, {
8526
8786
  org: config.org,
8527
8787
  repo: repo.fullName,
8528
- command: options.command ?? "org index",
8788
+ command,
8529
8789
  startedAt: repoStartedAt,
8530
8790
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8531
8791
  status: repoFailures.length > 0 ? "partial" : "success",
@@ -8533,6 +8793,20 @@ async function indexOrgRepos(db, config, options = {}) {
8533
8793
  codeFilesIndexed,
8534
8794
  failures: repoFailures
8535
8795
  });
8796
+ emit({
8797
+ stage: "org_repo_completed",
8798
+ org: config.org,
8799
+ command,
8800
+ repo: repo.fullName,
8801
+ current: repoPosition,
8802
+ total: repos.length,
8803
+ skippedHistory,
8804
+ skippedCode: Boolean(codeUnchanged || options.prsOnly),
8805
+ prsIndexed,
8806
+ codeFilesIndexed,
8807
+ durationMs: Date.now() - repoStartedAtMs,
8808
+ error: repoFailures.join("; ") || void 0
8809
+ });
8536
8810
  } catch (error) {
8537
8811
  const message = error instanceof Error ? error.message : String(error);
8538
8812
  updateOrgRepoState(db, {
@@ -8545,7 +8819,7 @@ async function indexOrgRepos(db, config, options = {}) {
8545
8819
  recordOrgIndexRun(db, {
8546
8820
  org: config.org,
8547
8821
  repo: repo.fullName,
8548
- command: options.command ?? "org index",
8822
+ command,
8549
8823
  startedAt: repoStartedAt,
8550
8824
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8551
8825
  status: "failed",
@@ -8556,6 +8830,20 @@ async function indexOrgRepos(db, config, options = {}) {
8556
8830
  skippedCode: false,
8557
8831
  error: message
8558
8832
  });
8833
+ emit({
8834
+ stage: "org_repo_completed",
8835
+ org: config.org,
8836
+ command,
8837
+ repo: repo.fullName,
8838
+ current: repoPosition,
8839
+ total: repos.length,
8840
+ skippedHistory: false,
8841
+ skippedCode: false,
8842
+ prsIndexed,
8843
+ codeFilesIndexed,
8844
+ durationMs: Date.now() - repoStartedAtMs,
8845
+ error: message
8846
+ });
8559
8847
  }
8560
8848
  }
8561
8849
  let graph;
@@ -8568,6 +8856,12 @@ async function indexOrgRepos(db, config, options = {}) {
8568
8856
  apiContractCount: counts.apiContracts,
8569
8857
  apiConsumerCount: counts.apiConsumers
8570
8858
  });
8859
+ emit({
8860
+ stage: "org_graph_skipped",
8861
+ org: config.org,
8862
+ command,
8863
+ reason: "Graph skipped because --no-graph was passed."
8864
+ });
8571
8865
  graph = { ...counts, skipped: true };
8572
8866
  } else {
8573
8867
  try {
@@ -8588,7 +8882,7 @@ async function indexOrgRepos(db, config, options = {}) {
8588
8882
  }
8589
8883
  recordOrgIndexRun(db, {
8590
8884
  org: config.org,
8591
- command: options.command ?? "org index",
8885
+ command,
8592
8886
  startedAt,
8593
8887
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8594
8888
  status: results.some((result) => result.error) || graph.error ? "partial" : "success",
@@ -8596,6 +8890,15 @@ async function indexOrgRepos(db, config, options = {}) {
8596
8890
  codeFilesIndexed: results.reduce((sum, result) => sum + (result.code?.indexedFiles ?? 0), 0),
8597
8891
  failures: results.map((result) => result.error).concat(graph.error ? [graph.error] : []).filter((error) => Boolean(error))
8598
8892
  });
8893
+ emit({
8894
+ stage: "org_sync_completed",
8895
+ org: config.org,
8896
+ command,
8897
+ totalRepos: repos.length,
8898
+ succeededRepos: results.filter((result) => !result.error).length,
8899
+ failedRepos: results.filter((result) => result.error).length,
8900
+ durationMs: Date.now() - startedAtMs
8901
+ });
8599
8902
  return {
8600
8903
  org: config.org,
8601
8904
  repos: results.sort((a, b) => a.repo.localeCompare(b.repo)),
@@ -9220,8 +9523,8 @@ function isTestPath2(filePath) {
9220
9523
  }
9221
9524
 
9222
9525
  // src/doctor.ts
9223
- import fs14 from "fs";
9224
- import path23 from "path";
9526
+ import fs15 from "fs";
9527
+ import path24 from "path";
9225
9528
  function check(name, ok, message, fix) {
9226
9529
  return { name, ok, message, fix: ok ? void 0 : fix };
9227
9530
  }
@@ -9325,12 +9628,12 @@ async function runDoctor(options) {
9325
9628
  )
9326
9629
  );
9327
9630
  }
9328
- const cursorConfigPath = path23.join(gitRoot ?? cwd, ".cursor", "mcp.json");
9631
+ const cursorConfigPath = path24.join(gitRoot ?? cwd, ".cursor", "mcp.json");
9329
9632
  let cursorConfig;
9330
9633
  let cursorConfigValid = false;
9331
- if (fs14.existsSync(cursorConfigPath)) {
9634
+ if (fs15.existsSync(cursorConfigPath)) {
9332
9635
  try {
9333
- cursorConfig = JSON.parse(fs14.readFileSync(cursorConfigPath, "utf8"));
9636
+ cursorConfig = JSON.parse(fs15.readFileSync(cursorConfigPath, "utf8"));
9334
9637
  cursorConfigValid = true;
9335
9638
  } catch {
9336
9639
  cursorConfigValid = false;
@@ -9339,7 +9642,7 @@ async function runDoctor(options) {
9339
9642
  checks.push(
9340
9643
  check(
9341
9644
  ".cursor/mcp.json valid",
9342
- fs14.existsSync(cursorConfigPath) && cursorConfigValid,
9645
+ fs15.existsSync(cursorConfigPath) && cursorConfigValid,
9343
9646
  cursorConfigValid ? ".cursor/mcp.json exists and is valid JSON." : ".cursor/mcp.json is missing or invalid.",
9344
9647
  "Run anchor init. If the file is malformed, fix the JSON and rerun anchor init."
9345
9648
  )
@@ -9356,7 +9659,7 @@ async function runDoctor(options) {
9356
9659
  )
9357
9660
  );
9358
9661
  const dbPath = defaultDatabasePath(gitRoot ?? cwd);
9359
- const dbExists = fs14.existsSync(dbPath);
9662
+ const dbExists = fs15.existsSync(dbPath);
9360
9663
  checks.push(
9361
9664
  check(
9362
9665
  ".anchor/index.sqlite exists",
@@ -9400,12 +9703,12 @@ async function runDoctor(options) {
9400
9703
  "Run pnpm build, then try anchor serve from the repository."
9401
9704
  )
9402
9705
  );
9403
- const rulePath = path23.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
9706
+ const rulePath = path24.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
9404
9707
  checks.push(
9405
9708
  check(
9406
9709
  "Cursor rule file exists",
9407
- fs14.existsSync(rulePath),
9408
- fs14.existsSync(rulePath) ? "Cursor rule file exists." : "Cursor rule file is missing.",
9710
+ fs15.existsSync(rulePath),
9711
+ fs15.existsSync(rulePath) ? "Cursor rule file exists." : "Cursor rule file is missing.",
9409
9712
  "Run anchor init to create .cursor/rules/anchor.mdc."
9410
9713
  )
9411
9714
  );
@@ -9482,6 +9785,7 @@ export {
9482
9785
  clampMaxResults,
9483
9786
  classifyArchitectureArea,
9484
9787
  clearGraphQLFetchCheckpoint,
9788
+ clearOrgHeartbeat,
9485
9789
  clipSentence,
9486
9790
  cloneOrPullOrgRepo,
9487
9791
  cloneOrgRepos,
@@ -9568,10 +9872,13 @@ export {
9568
9872
  mergeAnchorMcpConfig,
9569
9873
  normalizePullRequest,
9570
9874
  openAnchorDatabase,
9875
+ openAnchorDatabaseReadOnly,
9571
9876
  openOrgDatabase,
9877
+ openOrgDatabaseReadOnly,
9572
9878
  orgCloneStateFromResult,
9573
9879
  orgConfigPath,
9574
9880
  orgDatabasePath,
9881
+ orgHeartbeatPath,
9575
9882
  orgRepoLocalPath,
9576
9883
  orgReposRoot,
9577
9884
  orgRoot,
@@ -9585,6 +9892,7 @@ export {
9585
9892
  rankRelevantTests,
9586
9893
  rankTeamRules,
9587
9894
  rankWisdomUnits,
9895
+ readOrgHeartbeat,
9588
9896
  rebuildOrgGraph,
9589
9897
  recordFeedback,
9590
9898
  recordIndexRun,
@@ -9628,6 +9936,7 @@ export {
9628
9936
  validateOrgRepoFullName,
9629
9937
  validateOrgRepoGroup,
9630
9938
  validateTeamRulesFile,
9631
- watchCodebase
9939
+ watchCodebase,
9940
+ writeOrgHeartbeat
9632
9941
  };
9633
9942
  //# sourceMappingURL=index.js.map