@pratik7368patil/anchor-core 0.1.22 → 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();
@@ -7696,6 +7771,21 @@ function recordOrgGraphState(db, input) {
7696
7771
  now
7697
7772
  );
7698
7773
  }
7774
+ function getOrgGraphState(db, org) {
7775
+ initializeSchema(db);
7776
+ const row = db.prepare("SELECT * FROM org_graph_state WHERE org = ?").get(org);
7777
+ if (!row) return void 0;
7778
+ return {
7779
+ org: row.org,
7780
+ lastBuiltAt: row.last_built_at ?? void 0,
7781
+ lastStatus: row.last_status ?? void 0,
7782
+ lastDurationMs: row.last_duration_ms ?? void 0,
7783
+ edgeCount: row.edge_count ?? void 0,
7784
+ apiContractCount: row.api_contract_count ?? void 0,
7785
+ apiConsumerCount: row.api_consumer_count ?? void 0,
7786
+ lastError: row.last_error ?? void 0
7787
+ };
7788
+ }
7699
7789
  function count(db, table, where = "", params = []) {
7700
7790
  const row = db.prepare(`SELECT COUNT(*) AS count FROM ${table} ${where}`).get(...params);
7701
7791
  return row.count;
@@ -7715,9 +7805,11 @@ function grade(score) {
7715
7805
  if (score < 80) return "good";
7716
7806
  return "excellent";
7717
7807
  }
7718
- function getOrgStatus(db, config, baseDir) {
7719
- initializeSchema(db);
7720
- 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
+ }
7721
7813
  const enabledRepos = config.repos.filter((repo) => repo.enabled);
7722
7814
  const states = new Map(
7723
7815
  db.prepare("SELECT * FROM org_repo_state WHERE org = ?").all(config.org).map((row) => [row.repo, row])
@@ -7764,6 +7856,8 @@ function getOrgStatus(db, config, baseDir) {
7764
7856
  org: config.org,
7765
7857
  root: orgRoot(config.org, baseDir),
7766
7858
  databasePath: orgDatabasePath(config.org, baseDir),
7859
+ statusReadError: options.statusReadError,
7860
+ activeRun: options.activeRun,
7767
7861
  repoCount: config.repos.length,
7768
7862
  enabledRepoCount: enabledRepos.length,
7769
7863
  clonedRepoCount,
@@ -7798,10 +7892,84 @@ function getOrgStatus(db, config, baseDir) {
7798
7892
  };
7799
7893
  }
7800
7894
 
7801
- // src/org/clone.ts
7802
- import { execFileSync as execFileSync4 } from "child_process";
7895
+ // src/org/heartbeat.ts
7803
7896
  import fs11 from "fs";
7804
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";
7805
7973
  function defaultGitCommandRunner(command, args, options = {}) {
7806
7974
  return execFileSync4(command, args, {
7807
7975
  cwd: options.cwd,
@@ -7817,7 +7985,7 @@ function currentCommit(runner, localPath) {
7817
7985
  }
7818
7986
  }
7819
7987
  function plannedOrgCloneCommands(repo, localPath) {
7820
- if (!fs11.existsSync(path21.join(localPath, ".git"))) {
7988
+ if (!fs12.existsSync(path22.join(localPath, ".git"))) {
7821
7989
  return [
7822
7990
  {
7823
7991
  command: "git",
@@ -7846,8 +8014,8 @@ function plannedOrgCloneCommands(repo, localPath) {
7846
8014
  function cloneOrPullOrgRepo(input) {
7847
8015
  const runner = input.runner ?? defaultGitCommandRunner;
7848
8016
  const localPath = orgRepoLocalPath(input.org, input.repo, input.baseDir);
7849
- const existed = fs11.existsSync(path21.join(localPath, ".git"));
7850
- fs11.mkdirSync(path21.dirname(localPath), { recursive: true });
8017
+ const existed = fs12.existsSync(path22.join(localPath, ".git"));
8018
+ fs12.mkdirSync(path22.dirname(localPath), { recursive: true });
7851
8019
  const now = (/* @__PURE__ */ new Date()).toISOString();
7852
8020
  try {
7853
8021
  const commands = plannedOrgCloneCommands(input.repo, localPath);
@@ -7948,8 +8116,8 @@ function orgCloneStateFromResult(org, repo, result) {
7948
8116
 
7949
8117
  // src/org/graph.ts
7950
8118
  import crypto9 from "crypto";
7951
- import fs12 from "fs";
7952
- import path22 from "path";
8119
+ import fs13 from "fs";
8120
+ import path23 from "path";
7953
8121
  function stableId(parts) {
7954
8122
  return crypto9.createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
7955
8123
  }
@@ -7963,10 +8131,10 @@ function fileEvidence(repo, filePath, note) {
7963
8131
  };
7964
8132
  }
7965
8133
  function readPackageManifest(repoPath) {
7966
- const packagePath = path22.join(repoPath, "package.json");
7967
- if (!fs12.existsSync(packagePath)) return void 0;
8134
+ const packagePath = path23.join(repoPath, "package.json");
8135
+ if (!fs13.existsSync(packagePath)) return void 0;
7968
8136
  try {
7969
- return JSON.parse(fs12.readFileSync(packagePath, "utf8"));
8137
+ return JSON.parse(fs13.readFileSync(packagePath, "utf8"));
7970
8138
  } catch {
7971
8139
  return void 0;
7972
8140
  }
@@ -8259,7 +8427,11 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8259
8427
  const insertEdge = db.prepare(
8260
8428
  `INSERT INTO org_cross_repo_edges
8261
8429
  (id, org, source_repo, source_path, target_repo, target_path, relationship, evidence_json, confidence, created_at)
8262
- 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`
8263
8435
  );
8264
8436
  for (const edge of edges) {
8265
8437
  insertEdge.run(
@@ -8278,7 +8450,12 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8278
8450
  const insertContract = db.prepare(
8279
8451
  `INSERT INTO org_api_contracts
8280
8452
  (id, org, repo, file_path, contract, evidence_json, confidence, created_at)
8281
- 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`
8282
8459
  );
8283
8460
  for (const contract of apiContracts) {
8284
8461
  insertContract.run(
@@ -8295,7 +8472,12 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8295
8472
  const insertConsumer = db.prepare(
8296
8473
  `INSERT INTO org_api_consumers
8297
8474
  (id, org, provider_repo, provider_path, consumer_repo, consumer_path, contract, evidence_json, confidence, created_at)
8298
- 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`
8299
8481
  );
8300
8482
  for (const consumer of apiConsumers) {
8301
8483
  insertConsumer.run(
@@ -8358,7 +8540,8 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8358
8540
  }
8359
8541
 
8360
8542
  // src/org/index.ts
8361
- import fs13 from "fs";
8543
+ import fs14 from "fs";
8544
+ var ORG_SYNC_RESUME_WINDOW_MS = 12 * 60 * 60 * 1e3;
8362
8545
  function readCommit(runner, cwd) {
8363
8546
  try {
8364
8547
  return runner("git", ["rev-parse", "HEAD"], { cwd });
@@ -8369,6 +8552,27 @@ function readCommit(runner, cwd) {
8369
8552
  function missingCloneError(repo, localPath) {
8370
8553
  return `Repo ${repo} is not cloned at ${localPath}. Run anchor org clone --repo ${repo} --org <org>.`;
8371
8554
  }
8555
+ function latestIsoDate(dates) {
8556
+ return dates.filter((date) => Boolean(date)).sort().at(-1);
8557
+ }
8558
+ function graphIsFreshForState(input) {
8559
+ const latestRepoIndexAt = latestIsoDate([input.lastPrSyncAt, input.lastCodeIndexedAt]);
8560
+ return Boolean(
8561
+ latestRepoIndexAt && input.graphStatus === "success" && input.graphBuiltAt && input.graphBuiltAt >= latestRepoIndexAt
8562
+ );
8563
+ }
8564
+ function isWithinResumeWindow(date) {
8565
+ const parsed = Date.parse(date);
8566
+ return Number.isFinite(parsed) && Date.now() - parsed <= ORG_SYNC_RESUME_WINDOW_MS;
8567
+ }
8568
+ function shouldSkipPrFetchForResume(input) {
8569
+ if (input.options.command !== "org sync") return false;
8570
+ if (input.options.force || input.options.since || input.options.noGraph) return false;
8571
+ if (input.options.codeOnly || input.options.prsOnly) return false;
8572
+ if (!input.lastPrSyncAt) return false;
8573
+ if (!isWithinResumeWindow(input.lastPrSyncAt)) return false;
8574
+ return !graphIsFreshForState(input);
8575
+ }
8372
8576
  async function indexOrgRepos(db, config, options = {}) {
8373
8577
  initializeSchema(db);
8374
8578
  syncOrgConfigToDatabase(db, config, options.baseDir);
@@ -8376,30 +8580,94 @@ async function indexOrgRepos(db, config, options = {}) {
8376
8580
  (repo) => repo.enabled && (!options.repo || repo.fullName === options.repo)
8377
8581
  );
8378
8582
  const runner = options.runner ?? defaultGitCommandRunner;
8583
+ const fetchPullRequests = options.fetchPullRequests ?? fetchMergedPullRequests;
8379
8584
  const auth = options.token ? { token: options.token } : resolveGitHubToken();
8380
8585
  const results = [];
8381
8586
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8382
- for (const repo of repos) {
8587
+ const startedAtMs = Date.now();
8588
+ const graphState = getOrgGraphState(db, config.org);
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;
8383
8599
  const localPath = orgRepoLocalPath(config.org, repo, options.baseDir);
8384
8600
  const repoStartedAt = (/* @__PURE__ */ new Date()).toISOString();
8601
+ const repoStartedAtMs = Date.now();
8385
8602
  let prsIndexed = 0;
8386
8603
  let codeFilesIndexed = 0;
8387
8604
  try {
8388
- 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
+ });
8389
8623
  const currentCommit2 = readCommit(runner, localPath);
8390
8624
  const state = getOrgRepoState(db, config.org, repo.fullName);
8391
8625
  let history;
8392
8626
  let code;
8627
+ let skippedHistory = false;
8628
+ let historySkippedReason;
8393
8629
  const repoFailures = [];
8394
8630
  if (!options.codeOnly) {
8395
- if (!auth.token) {
8631
+ if (shouldSkipPrFetchForResume({
8632
+ options,
8633
+ lastPrSyncAt: state?.lastPrSyncAt,
8634
+ lastCodeIndexedAt: state?.lastCodeIndexedAt,
8635
+ graphBuiltAt: graphState?.lastBuiltAt,
8636
+ graphStatus: graphState?.lastStatus
8637
+ })) {
8638
+ skippedHistory = true;
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
+ });
8649
+ options.onFetchProgress?.({
8650
+ stage: "skipped_pull_request_fetch",
8651
+ repo: repo.fullName,
8652
+ reason: historySkippedReason
8653
+ });
8654
+ } else if (!auth.token) {
8396
8655
  repoFailures.push(
8397
8656
  "GitHub authentication is required for org PR indexing. Run gh auth login, or export GITHUB_TOKEN/GH_TOKEN with read-only access."
8398
8657
  );
8399
8658
  } else {
8400
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
+ });
8401
8669
  const since = options.since ?? (options.command === "org sync" ? state?.lastPrSyncAt ?? getLastSyncTime(db, repo.fullName) : void 0);
8402
- const pullRequests = await fetchMergedPullRequests({
8670
+ const pullRequests = await fetchPullRequests({
8403
8671
  token: auth.token,
8404
8672
  repo: repo.fullName,
8405
8673
  limit: 200,
@@ -8407,6 +8675,16 @@ async function indexOrgRepos(db, config, options = {}) {
8407
8675
  detailConcurrency: options.concurrency,
8408
8676
  onProgress: options.onFetchProgress
8409
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
+ });
8410
8688
  history = indexPullRequests(db, pullRequests, {
8411
8689
  cwd: localPath,
8412
8690
  repo: repo.fullName,
@@ -8431,6 +8709,15 @@ async function indexOrgRepos(db, config, options = {}) {
8431
8709
  }
8432
8710
  const codeUnchanged = !options.force && currentCommit2 && state?.lastCodeIndexedCommit && currentCommit2 === state.lastCodeIndexedCommit;
8433
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
+ });
8434
8721
  code = indexCodebase(db, {
8435
8722
  cwd: localPath,
8436
8723
  repo: repo.fullName,
@@ -8446,6 +8733,26 @@ async function indexOrgRepos(db, config, options = {}) {
8446
8733
  lastCodeIndexedCommit: currentCommit2,
8447
8734
  lastCodeIndexedAt: (/* @__PURE__ */ new Date()).toISOString()
8448
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
+ });
8449
8756
  }
8450
8757
  if (repoFailures.length > 0) {
8451
8758
  updateOrgRepoState(db, {
@@ -8457,9 +8764,19 @@ async function indexOrgRepos(db, config, options = {}) {
8457
8764
  lastError: repoFailures.join("; ")
8458
8765
  });
8459
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
+ });
8460
8775
  results.push({
8461
8776
  repo: repo.fullName,
8462
8777
  skippedCode: Boolean(codeUnchanged || options.prsOnly),
8778
+ skippedHistory,
8779
+ historySkippedReason,
8463
8780
  currentCommit: currentCommit2,
8464
8781
  history,
8465
8782
  code,
@@ -8468,7 +8785,7 @@ async function indexOrgRepos(db, config, options = {}) {
8468
8785
  recordOrgIndexRun(db, {
8469
8786
  org: config.org,
8470
8787
  repo: repo.fullName,
8471
- command: options.command ?? "org index",
8788
+ command,
8472
8789
  startedAt: repoStartedAt,
8473
8790
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8474
8791
  status: repoFailures.length > 0 ? "partial" : "success",
@@ -8476,6 +8793,20 @@ async function indexOrgRepos(db, config, options = {}) {
8476
8793
  codeFilesIndexed,
8477
8794
  failures: repoFailures
8478
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
+ });
8479
8810
  } catch (error) {
8480
8811
  const message = error instanceof Error ? error.message : String(error);
8481
8812
  updateOrgRepoState(db, {
@@ -8488,7 +8819,7 @@ async function indexOrgRepos(db, config, options = {}) {
8488
8819
  recordOrgIndexRun(db, {
8489
8820
  org: config.org,
8490
8821
  repo: repo.fullName,
8491
- command: options.command ?? "org index",
8822
+ command,
8492
8823
  startedAt: repoStartedAt,
8493
8824
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8494
8825
  status: "failed",
@@ -8499,6 +8830,20 @@ async function indexOrgRepos(db, config, options = {}) {
8499
8830
  skippedCode: false,
8500
8831
  error: message
8501
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
+ });
8502
8847
  }
8503
8848
  }
8504
8849
  let graph;
@@ -8511,6 +8856,12 @@ async function indexOrgRepos(db, config, options = {}) {
8511
8856
  apiContractCount: counts.apiContracts,
8512
8857
  apiConsumerCount: counts.apiConsumers
8513
8858
  });
8859
+ emit({
8860
+ stage: "org_graph_skipped",
8861
+ org: config.org,
8862
+ command,
8863
+ reason: "Graph skipped because --no-graph was passed."
8864
+ });
8514
8865
  graph = { ...counts, skipped: true };
8515
8866
  } else {
8516
8867
  try {
@@ -8531,7 +8882,7 @@ async function indexOrgRepos(db, config, options = {}) {
8531
8882
  }
8532
8883
  recordOrgIndexRun(db, {
8533
8884
  org: config.org,
8534
- command: options.command ?? "org index",
8885
+ command,
8535
8886
  startedAt,
8536
8887
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8537
8888
  status: results.some((result) => result.error) || graph.error ? "partial" : "success",
@@ -8539,6 +8890,15 @@ async function indexOrgRepos(db, config, options = {}) {
8539
8890
  codeFilesIndexed: results.reduce((sum, result) => sum + (result.code?.indexedFiles ?? 0), 0),
8540
8891
  failures: results.map((result) => result.error).concat(graph.error ? [graph.error] : []).filter((error) => Boolean(error))
8541
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
+ });
8542
8902
  return {
8543
8903
  org: config.org,
8544
8904
  repos: results.sort((a, b) => a.repo.localeCompare(b.repo)),
@@ -9163,8 +9523,8 @@ function isTestPath2(filePath) {
9163
9523
  }
9164
9524
 
9165
9525
  // src/doctor.ts
9166
- import fs14 from "fs";
9167
- import path23 from "path";
9526
+ import fs15 from "fs";
9527
+ import path24 from "path";
9168
9528
  function check(name, ok, message, fix) {
9169
9529
  return { name, ok, message, fix: ok ? void 0 : fix };
9170
9530
  }
@@ -9268,12 +9628,12 @@ async function runDoctor(options) {
9268
9628
  )
9269
9629
  );
9270
9630
  }
9271
- const cursorConfigPath = path23.join(gitRoot ?? cwd, ".cursor", "mcp.json");
9631
+ const cursorConfigPath = path24.join(gitRoot ?? cwd, ".cursor", "mcp.json");
9272
9632
  let cursorConfig;
9273
9633
  let cursorConfigValid = false;
9274
- if (fs14.existsSync(cursorConfigPath)) {
9634
+ if (fs15.existsSync(cursorConfigPath)) {
9275
9635
  try {
9276
- cursorConfig = JSON.parse(fs14.readFileSync(cursorConfigPath, "utf8"));
9636
+ cursorConfig = JSON.parse(fs15.readFileSync(cursorConfigPath, "utf8"));
9277
9637
  cursorConfigValid = true;
9278
9638
  } catch {
9279
9639
  cursorConfigValid = false;
@@ -9282,7 +9642,7 @@ async function runDoctor(options) {
9282
9642
  checks.push(
9283
9643
  check(
9284
9644
  ".cursor/mcp.json valid",
9285
- fs14.existsSync(cursorConfigPath) && cursorConfigValid,
9645
+ fs15.existsSync(cursorConfigPath) && cursorConfigValid,
9286
9646
  cursorConfigValid ? ".cursor/mcp.json exists and is valid JSON." : ".cursor/mcp.json is missing or invalid.",
9287
9647
  "Run anchor init. If the file is malformed, fix the JSON and rerun anchor init."
9288
9648
  )
@@ -9299,7 +9659,7 @@ async function runDoctor(options) {
9299
9659
  )
9300
9660
  );
9301
9661
  const dbPath = defaultDatabasePath(gitRoot ?? cwd);
9302
- const dbExists = fs14.existsSync(dbPath);
9662
+ const dbExists = fs15.existsSync(dbPath);
9303
9663
  checks.push(
9304
9664
  check(
9305
9665
  ".anchor/index.sqlite exists",
@@ -9343,12 +9703,12 @@ async function runDoctor(options) {
9343
9703
  "Run pnpm build, then try anchor serve from the repository."
9344
9704
  )
9345
9705
  );
9346
- const rulePath = path23.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
9706
+ const rulePath = path24.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
9347
9707
  checks.push(
9348
9708
  check(
9349
9709
  "Cursor rule file exists",
9350
- fs14.existsSync(rulePath),
9351
- 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.",
9352
9712
  "Run anchor init to create .cursor/rules/anchor.mdc."
9353
9713
  )
9354
9714
  );
@@ -9425,6 +9785,7 @@ export {
9425
9785
  clampMaxResults,
9426
9786
  classifyArchitectureArea,
9427
9787
  clearGraphQLFetchCheckpoint,
9788
+ clearOrgHeartbeat,
9428
9789
  clipSentence,
9429
9790
  cloneOrPullOrgRepo,
9430
9791
  cloneOrgRepos,
@@ -9478,6 +9839,7 @@ export {
9478
9839
  getLastSyncTime,
9479
9840
  getOrgArchitectureMap,
9480
9841
  getOrgGraphCounts,
9842
+ getOrgGraphState,
9481
9843
  getOrgRepoState,
9482
9844
  getOrgStatus,
9483
9845
  getPlaybook,
@@ -9510,10 +9872,13 @@ export {
9510
9872
  mergeAnchorMcpConfig,
9511
9873
  normalizePullRequest,
9512
9874
  openAnchorDatabase,
9875
+ openAnchorDatabaseReadOnly,
9513
9876
  openOrgDatabase,
9877
+ openOrgDatabaseReadOnly,
9514
9878
  orgCloneStateFromResult,
9515
9879
  orgConfigPath,
9516
9880
  orgDatabasePath,
9881
+ orgHeartbeatPath,
9517
9882
  orgRepoLocalPath,
9518
9883
  orgReposRoot,
9519
9884
  orgRoot,
@@ -9527,6 +9892,7 @@ export {
9527
9892
  rankRelevantTests,
9528
9893
  rankTeamRules,
9529
9894
  rankWisdomUnits,
9895
+ readOrgHeartbeat,
9530
9896
  rebuildOrgGraph,
9531
9897
  recordFeedback,
9532
9898
  recordIndexRun,
@@ -9570,6 +9936,7 @@ export {
9570
9936
  validateOrgRepoFullName,
9571
9937
  validateOrgRepoGroup,
9572
9938
  validateTeamRulesFile,
9573
- watchCodebase
9939
+ watchCodebase,
9940
+ writeOrgHeartbeat
9574
9941
  };
9575
9942
  //# sourceMappingURL=index.js.map