@pratik7368patil/anchor-core 0.1.21 → 0.1.23

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
@@ -657,6 +657,18 @@ CREATE TABLE IF NOT EXISTS org_anomaly_events (
657
657
  created_at TEXT NOT NULL
658
658
  );
659
659
 
660
+ CREATE TABLE IF NOT EXISTS org_graph_state (
661
+ org TEXT PRIMARY KEY,
662
+ last_built_at TEXT,
663
+ last_status TEXT NOT NULL DEFAULT 'unknown',
664
+ last_duration_ms INTEGER,
665
+ edge_count INTEGER NOT NULL DEFAULT 0,
666
+ api_contract_count INTEGER NOT NULL DEFAULT 0,
667
+ api_consumer_count INTEGER NOT NULL DEFAULT 0,
668
+ last_error TEXT,
669
+ updated_at TEXT NOT NULL
670
+ );
671
+
660
672
  CREATE TABLE IF NOT EXISTS org_sync_checkpoints (
661
673
  org TEXT NOT NULL,
662
674
  repo TEXT NOT NULL,
@@ -694,6 +706,7 @@ CREATE INDEX IF NOT EXISTS idx_org_edges_target ON org_cross_repo_edges(org, tar
694
706
  CREATE INDEX IF NOT EXISTS idx_org_consumers_provider ON org_api_consumers(org, provider_repo);
695
707
  CREATE INDEX IF NOT EXISTS idx_org_consumers_consumer ON org_api_consumers(org, consumer_repo);
696
708
  CREATE INDEX IF NOT EXISTS idx_org_anomalies_org ON org_anomaly_events(org, severity);
709
+ CREATE INDEX IF NOT EXISTS idx_org_graph_state_status ON org_graph_state(org, last_status);
697
710
  `;
698
711
 
699
712
  // src/rules/team-rules.ts
@@ -1573,7 +1586,8 @@ function checkSchema(db) {
1573
1586
  "org_repo_state",
1574
1587
  "org_cross_repo_edges",
1575
1588
  "org_api_consumers",
1576
- "org_anomaly_events"
1589
+ "org_anomaly_events",
1590
+ "org_graph_state"
1577
1591
  ].every(
1578
1592
  (tableName) => db.prepare("SELECT name FROM sqlite_master WHERE name = ?").all(tableName).length > 0
1579
1593
  );
@@ -7653,10 +7667,62 @@ function recordOrgIndexRun(db, input) {
7653
7667
  JSON.stringify(input.failures ?? [])
7654
7668
  );
7655
7669
  }
7670
+ function recordOrgGraphState(db, input) {
7671
+ initializeSchema(db);
7672
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7673
+ db.prepare(
7674
+ `INSERT INTO org_graph_state
7675
+ (org, last_built_at, last_status, last_duration_ms, edge_count, api_contract_count,
7676
+ api_consumer_count, last_error, updated_at)
7677
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
7678
+ ON CONFLICT(org) DO UPDATE SET
7679
+ last_built_at = COALESCE(excluded.last_built_at, org_graph_state.last_built_at),
7680
+ last_status = excluded.last_status,
7681
+ last_duration_ms = COALESCE(excluded.last_duration_ms, org_graph_state.last_duration_ms),
7682
+ edge_count = excluded.edge_count,
7683
+ api_contract_count = excluded.api_contract_count,
7684
+ api_consumer_count = excluded.api_consumer_count,
7685
+ last_error = excluded.last_error,
7686
+ updated_at = excluded.updated_at`
7687
+ ).run(
7688
+ input.org,
7689
+ input.builtAt ?? null,
7690
+ input.status,
7691
+ input.durationMs ?? null,
7692
+ input.edgeCount ?? 0,
7693
+ input.apiContractCount ?? 0,
7694
+ input.apiConsumerCount ?? 0,
7695
+ input.error ?? null,
7696
+ now
7697
+ );
7698
+ }
7699
+ function getOrgGraphState(db, org) {
7700
+ initializeSchema(db);
7701
+ const row = db.prepare("SELECT * FROM org_graph_state WHERE org = ?").get(org);
7702
+ if (!row) return void 0;
7703
+ return {
7704
+ org: row.org,
7705
+ lastBuiltAt: row.last_built_at ?? void 0,
7706
+ lastStatus: row.last_status ?? void 0,
7707
+ lastDurationMs: row.last_duration_ms ?? void 0,
7708
+ edgeCount: row.edge_count ?? void 0,
7709
+ apiContractCount: row.api_contract_count ?? void 0,
7710
+ apiConsumerCount: row.api_consumer_count ?? void 0,
7711
+ lastError: row.last_error ?? void 0
7712
+ };
7713
+ }
7656
7714
  function count(db, table, where = "", params = []) {
7657
7715
  const row = db.prepare(`SELECT COUNT(*) AS count FROM ${table} ${where}`).get(...params);
7658
7716
  return row.count;
7659
7717
  }
7718
+ function getOrgGraphCounts(db, org) {
7719
+ initializeSchema(db);
7720
+ return {
7721
+ edges: count(db, "org_cross_repo_edges", "WHERE org = ?", [org]),
7722
+ apiContracts: count(db, "org_api_contracts", "WHERE org = ?", [org]),
7723
+ apiConsumers: count(db, "org_api_consumers", "WHERE org = ?", [org])
7724
+ };
7725
+ }
7660
7726
  function grade(score) {
7661
7727
  if (score <= 0) return "empty";
7662
7728
  if (score < 35) return "poor";
@@ -7678,8 +7744,10 @@ function getOrgStatus(db, config, baseDir) {
7678
7744
  const codeChunkCount = count(db, "code_chunks");
7679
7745
  const wisdomUnitCount = count(db, "wisdom_units");
7680
7746
  const crossRepoEdgeCount = count(db, "org_cross_repo_edges", "WHERE org = ?", [config.org]);
7747
+ const apiContractCount = count(db, "org_api_contracts", "WHERE org = ?", [config.org]);
7681
7748
  const apiConsumerCount = count(db, "org_api_consumers", "WHERE org = ?", [config.org]);
7682
7749
  const anomalyCount = count(db, "org_anomaly_events", "WHERE org = ?", [config.org]);
7750
+ const graphState = db.prepare("SELECT * FROM org_graph_state WHERE org = ?").get(config.org);
7683
7751
  let score = 0;
7684
7752
  const reasons = [];
7685
7753
  if (enabledRepos.length > 0) {
@@ -7718,8 +7786,13 @@ function getOrgStatus(db, config, baseDir) {
7718
7786
  codeChunkCount,
7719
7787
  wisdomUnitCount,
7720
7788
  crossRepoEdgeCount,
7789
+ apiContractCount,
7721
7790
  apiConsumerCount,
7722
7791
  anomalyCount,
7792
+ graphLastBuiltAt: graphState?.last_built_at ?? void 0,
7793
+ graphLastStatus: graphState?.last_status ?? void 0,
7794
+ graphLastDurationMs: graphState?.last_duration_ms ?? void 0,
7795
+ graphLastError: graphState?.last_error ?? void 0,
7723
7796
  coverageScore: score,
7724
7797
  coverageGrade: grade(score),
7725
7798
  coverageReasons: reasons,
@@ -7845,16 +7918,32 @@ async function cloneOrgRepos(input) {
7845
7918
  const repo = repos[next];
7846
7919
  next += 1;
7847
7920
  if (!repo) continue;
7848
- input.onProgress?.(`cloning or pulling ${repo.fullName}`);
7849
- results.push(
7850
- cloneOrPullOrgRepo({
7851
- org: input.config.org,
7852
- repo,
7853
- db: input.db,
7854
- baseDir: input.baseDir,
7855
- runner: input.runner
7856
- })
7857
- );
7921
+ const current = next;
7922
+ input.onProgress?.({
7923
+ stage: "cloning_or_pulling_repo",
7924
+ org: input.config.org,
7925
+ repo: repo.fullName,
7926
+ current,
7927
+ total: repos.length
7928
+ });
7929
+ const result = cloneOrPullOrgRepo({
7930
+ org: input.config.org,
7931
+ repo,
7932
+ db: input.db,
7933
+ baseDir: input.baseDir,
7934
+ runner: input.runner
7935
+ });
7936
+ results.push(result);
7937
+ input.onProgress?.({
7938
+ stage: "cloned_or_pulled_repo",
7939
+ org: input.config.org,
7940
+ repo: repo.fullName,
7941
+ current,
7942
+ total: repos.length,
7943
+ cloned: result.cloned,
7944
+ pulled: result.pulled,
7945
+ error: result.error
7946
+ });
7858
7947
  }
7859
7948
  }
7860
7949
  await Promise.all(Array.from({ length: Math.min(limit, repos.length) }, () => worker()));
@@ -7952,210 +8041,340 @@ function isApiConsumerText(text) {
7952
8041
  function evidenceJson(evidence) {
7953
8042
  return JSON.stringify(evidence);
7954
8043
  }
7955
- function rebuildOrgGraph(db, config, baseDir) {
8044
+ function shouldEmitProgress(current, total, interval = 100) {
8045
+ return current === 1 || current === total || current % interval === 0;
8046
+ }
8047
+ function resolveOptions(baseDirOrOptions) {
8048
+ return typeof baseDirOrOptions === "string" ? { baseDir: baseDirOrOptions } : baseDirOrOptions ?? {};
8049
+ }
8050
+ function rebuildOrgGraph(db, config, baseDirOrOptions) {
7956
8051
  initializeSchema(db);
7957
- const packageNames = repoPackageNames(config, baseDir);
7958
- const enabledRepos = config.repos.filter((repo) => repo.enabled);
7959
- const repoByName = new Map(enabledRepos.map((repo) => [repo.fullName, repo]));
7960
- const packageToRepo = /* @__PURE__ */ new Map();
7961
- for (const [repo, names] of packageNames.entries()) {
7962
- for (const name of names) packageToRepo.set(name, repo);
7963
- }
7964
- const edges = [];
7965
- const addEdge = (edge) => {
7966
- if (edge.sourceRepo === edge.targetRepo) return;
7967
- const key = [
7968
- edge.sourceRepo,
7969
- edge.sourcePath,
7970
- edge.targetRepo,
7971
- edge.targetPath ?? "",
7972
- edge.relationship
7973
- ].join("\0");
7974
- if (edges.some(
7975
- (existing) => [
7976
- existing.sourceRepo,
7977
- existing.sourcePath,
7978
- existing.targetRepo,
7979
- existing.targetPath ?? "",
7980
- existing.relationship
7981
- ].join("\0") === key
7982
- )) {
7983
- return;
7984
- }
7985
- edges.push(edge);
7986
- };
7987
- for (const repo of enabledRepos) {
7988
- const manifest = readPackageManifest(orgRepoLocalPath(config.org, repo, baseDir));
7989
- for (const dependency of dependenciesFor(manifest)) {
7990
- const targetRepo = packageToRepo.get(dependency);
7991
- if (!targetRepo || targetRepo === repo.fullName) continue;
7992
- addEdge({
7993
- org: config.org,
7994
- sourceRepo: repo.fullName,
7995
- sourcePath: "package.json",
7996
- targetRepo,
7997
- relationship: "depends_on_package",
7998
- evidence: [fileEvidence(repo.fullName, "package.json", `depends on ${dependency}`)],
7999
- confidence: 0.9
8000
- });
8001
- }
8002
- }
8003
- const imports = db.prepare(
8004
- `SELECT r.full_name AS repo, ci.source_path, ci.specifier, ci.imported_path, ci.imported_symbols_json
8005
- FROM code_imports ci
8006
- JOIN repositories r ON r.id = ci.repo_id`
8007
- ).all();
8008
- for (const item of imports) {
8009
- const sourceRepo = repoByName.get(item.repo);
8010
- if (!sourceRepo) continue;
8011
- for (const [targetRepo, names] of packageNames.entries()) {
8012
- if (targetRepo === item.repo) continue;
8013
- const matchedName = names.find(
8014
- (name) => item.specifier === name || item.specifier.startsWith(`${name}/`)
8015
- );
8016
- if (!matchedName) continue;
8017
- addEdge({
8018
- org: config.org,
8019
- sourceRepo: item.repo,
8020
- sourcePath: item.source_path,
8021
- targetRepo,
8022
- targetPath: item.imported_path ?? void 0,
8023
- relationship: "imports",
8024
- evidence: [
8025
- fileEvidence(
8026
- item.repo,
8027
- item.source_path,
8028
- `imports ${sanitizeHistoricalText(matchedName)}`
8029
- )
8030
- ],
8031
- confidence: parseJsonArray9(item.imported_symbols_json).length > 0 ? 0.88 : 0.76
8032
- });
8033
- }
8034
- }
8035
- const chunks = db.prepare(
8036
- `SELECT r.full_name AS repo, cc.file_path, cc.sanitized_text, cc.symbols_json
8037
- FROM code_chunks cc
8038
- JOIN repositories r ON r.id = cc.repo_id`
8039
- ).all();
8040
- const apiContracts = chunks.filter((chunk) => repoByName.has(chunk.repo) && isApiProviderPath(chunk.file_path)).flatMap(
8041
- (chunk) => extractContracts(chunk.sanitized_text).map((contract) => ({
8042
- repo: chunk.repo,
8043
- filePath: chunk.file_path,
8044
- contract,
8045
- evidence: [fileEvidence(chunk.repo, chunk.file_path, `defines ${contract}`)],
8046
- confidence: 0.74
8047
- }))
8048
- );
8049
- const apiConsumers = [];
8050
- for (const contract of apiContracts) {
8051
- for (const chunk of chunks) {
8052
- if (chunk.repo === contract.repo || !repoByName.has(chunk.repo)) continue;
8053
- if (!isApiConsumerText(chunk.sanitized_text)) continue;
8054
- if (!chunk.sanitized_text.includes(contract.contract)) continue;
8055
- const consumer = {
8056
- org: config.org,
8057
- providerRepo: contract.repo,
8058
- providerPath: contract.filePath,
8059
- consumerRepo: chunk.repo,
8060
- consumerPath: chunk.file_path,
8061
- contract: contract.contract,
8062
- evidence: [
8063
- ...contract.evidence,
8064
- fileEvidence(chunk.repo, chunk.file_path, `consumes ${contract.contract}`)
8065
- ],
8066
- confidence: 0.86
8067
- };
8068
- apiConsumers.push(consumer);
8069
- addEdge({
8070
- org: config.org,
8071
- sourceRepo: chunk.repo,
8072
- sourcePath: chunk.file_path,
8073
- targetRepo: contract.repo,
8074
- targetPath: contract.filePath,
8075
- relationship: "api_consumer",
8076
- evidence: consumer.evidence,
8077
- confidence: consumer.confidence
8078
- });
8052
+ const options = resolveOptions(baseDirOrOptions);
8053
+ const startedAt = Date.now();
8054
+ try {
8055
+ options.onProgress?.({
8056
+ stage: "loading_package_manifests",
8057
+ org: config.org,
8058
+ totalRepos: config.repos.filter((repo) => repo.enabled).length
8059
+ });
8060
+ const packageNames = repoPackageNames(config, options.baseDir);
8061
+ const enabledRepos = config.repos.filter((repo) => repo.enabled);
8062
+ const repoByName = new Map(enabledRepos.map((repo) => [repo.fullName, repo]));
8063
+ const packageToRepo = /* @__PURE__ */ new Map();
8064
+ for (const [repo, names] of packageNames.entries()) {
8065
+ for (const name of names) packageToRepo.set(name, repo);
8079
8066
  }
8080
- }
8081
- const now = (/* @__PURE__ */ new Date()).toISOString();
8082
- const transaction = db.transaction(() => {
8083
- db.prepare("DELETE FROM org_cross_repo_edges WHERE org = ?").run(config.org);
8084
- db.prepare("DELETE FROM org_api_contracts WHERE org = ?").run(config.org);
8085
- db.prepare("DELETE FROM org_api_consumers WHERE org = ?").run(config.org);
8086
- const insertEdge = db.prepare(
8087
- `INSERT INTO org_cross_repo_edges
8088
- (id, org, source_repo, source_path, target_repo, target_path, relationship, evidence_json, confidence, created_at)
8089
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8090
- );
8091
- for (const edge of edges) {
8092
- insertEdge.run(
8093
- `oge_${stableId([edge.org, edge.sourceRepo, edge.sourcePath, edge.targetRepo, edge.targetPath ?? "", edge.relationship])}`,
8094
- edge.org,
8067
+ options.onProgress?.({
8068
+ stage: "loaded_package_manifests",
8069
+ org: config.org,
8070
+ repos: enabledRepos.length,
8071
+ packageNames: packageToRepo.size
8072
+ });
8073
+ const edges = [];
8074
+ const edgeKeys = /* @__PURE__ */ new Set();
8075
+ const addEdge = (edge) => {
8076
+ if (edge.sourceRepo === edge.targetRepo) return;
8077
+ const key = [
8095
8078
  edge.sourceRepo,
8096
8079
  edge.sourcePath,
8097
8080
  edge.targetRepo,
8098
- edge.targetPath ?? null,
8099
- edge.relationship,
8100
- evidenceJson(edge.evidence),
8101
- edge.confidence,
8102
- now
8103
- );
8104
- }
8105
- const insertContract = db.prepare(
8106
- `INSERT INTO org_api_contracts
8107
- (id, org, repo, file_path, contract, evidence_json, confidence, created_at)
8108
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
8081
+ edge.targetPath ?? "",
8082
+ edge.relationship
8083
+ ].join("\0");
8084
+ if (edgeKeys.has(key)) return;
8085
+ edgeKeys.add(key);
8086
+ edges.push(edge);
8087
+ };
8088
+ enabledRepos.forEach((repo, index) => {
8089
+ const manifest = readPackageManifest(orgRepoLocalPath(config.org, repo, options.baseDir));
8090
+ for (const dependency of dependenciesFor(manifest)) {
8091
+ const targetRepo = packageToRepo.get(dependency);
8092
+ if (!targetRepo || targetRepo === repo.fullName) continue;
8093
+ addEdge({
8094
+ org: config.org,
8095
+ sourceRepo: repo.fullName,
8096
+ sourcePath: "package.json",
8097
+ targetRepo,
8098
+ relationship: "depends_on_package",
8099
+ evidence: [
8100
+ fileEvidence(
8101
+ repo.fullName,
8102
+ "package.json",
8103
+ `depends on ${sanitizeHistoricalText(dependency)}`
8104
+ )
8105
+ ],
8106
+ confidence: 0.9
8107
+ });
8108
+ }
8109
+ options.onProgress?.({
8110
+ stage: "building_package_edges",
8111
+ org: config.org,
8112
+ current: index + 1,
8113
+ total: enabledRepos.length,
8114
+ repo: repo.fullName,
8115
+ edges: edges.length
8116
+ });
8117
+ });
8118
+ options.onProgress?.({ stage: "loading_imports", org: config.org });
8119
+ const imports = db.prepare(
8120
+ `SELECT r.full_name AS repo, ci.source_path, ci.specifier, ci.imported_path, ci.imported_symbols_json
8121
+ FROM code_imports ci
8122
+ JOIN repositories r ON r.id = ci.repo_id`
8123
+ ).all();
8124
+ const packageMatchers = [...packageNames.entries()].flatMap(([repo, names]) => names.map((name) => ({ repo, name }))).sort((a, b) => b.name.length - a.name.length);
8125
+ imports.forEach((item, index) => {
8126
+ const sourceRepo = repoByName.get(item.repo);
8127
+ if (!sourceRepo) return;
8128
+ for (const candidate of packageMatchers) {
8129
+ if (candidate.repo === item.repo) continue;
8130
+ const matched = item.specifier === candidate.name || item.specifier.startsWith(`${candidate.name}/`);
8131
+ if (!matched) continue;
8132
+ addEdge({
8133
+ org: config.org,
8134
+ sourceRepo: item.repo,
8135
+ sourcePath: item.source_path,
8136
+ targetRepo: candidate.repo,
8137
+ targetPath: item.imported_path ?? void 0,
8138
+ relationship: "imports",
8139
+ evidence: [
8140
+ fileEvidence(
8141
+ item.repo,
8142
+ item.source_path,
8143
+ `imports ${sanitizeHistoricalText(candidate.name)}`
8144
+ )
8145
+ ],
8146
+ confidence: parseJsonArray9(item.imported_symbols_json).length > 0 ? 0.88 : 0.76
8147
+ });
8148
+ break;
8149
+ }
8150
+ if (shouldEmitProgress(index + 1, imports.length)) {
8151
+ options.onProgress?.({
8152
+ stage: "building_import_edges",
8153
+ org: config.org,
8154
+ current: index + 1,
8155
+ total: imports.length,
8156
+ sourcePath: item.source_path,
8157
+ edges: edges.length
8158
+ });
8159
+ }
8160
+ });
8161
+ options.onProgress?.({ stage: "loading_code_chunks", org: config.org });
8162
+ const chunks = db.prepare(
8163
+ `SELECT r.full_name AS repo, cc.file_path, cc.sanitized_text, cc.symbols_json
8164
+ FROM code_chunks cc
8165
+ JOIN repositories r ON r.id = cc.repo_id`
8166
+ ).all();
8167
+ const providerChunks = chunks.filter(
8168
+ (chunk) => repoByName.has(chunk.repo) && isApiProviderPath(chunk.file_path)
8109
8169
  );
8110
- for (const contract of apiContracts) {
8111
- insertContract.run(
8112
- `oac_${stableId([config.org, contract.repo, contract.filePath, contract.contract])}`,
8113
- config.org,
8114
- contract.repo,
8115
- contract.filePath,
8116
- sanitizeHistoricalText(contract.contract),
8117
- evidenceJson(contract.evidence),
8118
- contract.confidence,
8119
- now
8120
- );
8121
- }
8122
- const insertConsumer = db.prepare(
8123
- `INSERT INTO org_api_consumers
8124
- (id, org, provider_repo, provider_path, consumer_repo, consumer_path, contract, evidence_json, confidence, created_at)
8125
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8170
+ const apiContracts = [];
8171
+ const contractKeys = /* @__PURE__ */ new Set();
8172
+ const contractsByToken = /* @__PURE__ */ new Map();
8173
+ providerChunks.forEach((chunk, index) => {
8174
+ for (const contract of extractContracts(chunk.sanitized_text)) {
8175
+ const sanitizedContract = sanitizeHistoricalText(contract);
8176
+ const key = [chunk.repo, chunk.file_path, sanitizedContract].join("\0");
8177
+ if (contractKeys.has(key)) continue;
8178
+ contractKeys.add(key);
8179
+ const apiContract = {
8180
+ repo: chunk.repo,
8181
+ filePath: chunk.file_path,
8182
+ contract: sanitizedContract,
8183
+ evidence: [fileEvidence(chunk.repo, chunk.file_path, `defines ${sanitizedContract}`)],
8184
+ confidence: 0.74
8185
+ };
8186
+ apiContracts.push(apiContract);
8187
+ const bucket = contractsByToken.get(sanitizedContract) ?? [];
8188
+ bucket.push(apiContract);
8189
+ contractsByToken.set(sanitizedContract, bucket);
8190
+ }
8191
+ if (shouldEmitProgress(index + 1, providerChunks.length)) {
8192
+ options.onProgress?.({
8193
+ stage: "extracting_api_contracts",
8194
+ org: config.org,
8195
+ current: index + 1,
8196
+ total: providerChunks.length,
8197
+ filePath: chunk.file_path,
8198
+ contracts: apiContracts.length
8199
+ });
8200
+ }
8201
+ });
8202
+ const apiConsumers = [];
8203
+ const consumerKeys = /* @__PURE__ */ new Set();
8204
+ const consumerChunks = chunks.filter(
8205
+ (chunk) => repoByName.has(chunk.repo) && isApiConsumerText(chunk.sanitized_text)
8126
8206
  );
8127
- for (const consumer of apiConsumers) {
8128
- insertConsumer.run(
8129
- `oap_${stableId([
8207
+ consumerChunks.forEach((chunk, index) => {
8208
+ const consumerTokens = extractContracts(chunk.sanitized_text);
8209
+ let chunkMatches = 0;
8210
+ for (const token of consumerTokens) {
8211
+ const contracts = contractsByToken.get(sanitizeHistoricalText(token));
8212
+ if (!contracts) continue;
8213
+ for (const contract of contracts) {
8214
+ if (chunk.repo === contract.repo) continue;
8215
+ const consumerKey = [
8216
+ contract.repo,
8217
+ contract.filePath,
8218
+ chunk.repo,
8219
+ chunk.file_path,
8220
+ contract.contract
8221
+ ].join("\0");
8222
+ if (consumerKeys.has(consumerKey)) continue;
8223
+ consumerKeys.add(consumerKey);
8224
+ const consumer = {
8225
+ org: config.org,
8226
+ providerRepo: contract.repo,
8227
+ providerPath: contract.filePath,
8228
+ consumerRepo: chunk.repo,
8229
+ consumerPath: chunk.file_path,
8230
+ contract: contract.contract,
8231
+ evidence: [
8232
+ ...contract.evidence,
8233
+ fileEvidence(chunk.repo, chunk.file_path, `consumes ${contract.contract}`)
8234
+ ],
8235
+ confidence: 0.86
8236
+ };
8237
+ chunkMatches += 1;
8238
+ apiConsumers.push(consumer);
8239
+ addEdge({
8240
+ org: config.org,
8241
+ sourceRepo: chunk.repo,
8242
+ sourcePath: chunk.file_path,
8243
+ targetRepo: contract.repo,
8244
+ targetPath: contract.filePath,
8245
+ relationship: "api_consumer",
8246
+ evidence: consumer.evidence,
8247
+ confidence: consumer.confidence
8248
+ });
8249
+ }
8250
+ }
8251
+ if (shouldEmitProgress(index + 1, consumerChunks.length)) {
8252
+ options.onProgress?.({
8253
+ stage: "matching_api_consumers",
8254
+ org: config.org,
8255
+ current: index + 1,
8256
+ total: consumerChunks.length,
8257
+ filePath: chunk.file_path,
8258
+ matches: chunkMatches
8259
+ });
8260
+ }
8261
+ });
8262
+ options.onProgress?.({
8263
+ stage: "writing_org_graph",
8264
+ org: config.org,
8265
+ edges: edges.length,
8266
+ apiContracts: apiContracts.length,
8267
+ apiConsumers: apiConsumers.length
8268
+ });
8269
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8270
+ const transaction = db.transaction(() => {
8271
+ db.prepare("DELETE FROM org_cross_repo_edges WHERE org = ?").run(config.org);
8272
+ db.prepare("DELETE FROM org_api_contracts WHERE org = ?").run(config.org);
8273
+ db.prepare("DELETE FROM org_api_consumers WHERE org = ?").run(config.org);
8274
+ const insertEdge = db.prepare(
8275
+ `INSERT INTO org_cross_repo_edges
8276
+ (id, org, source_repo, source_path, target_repo, target_path, relationship, evidence_json, confidence, created_at)
8277
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8278
+ );
8279
+ for (const edge of edges) {
8280
+ insertEdge.run(
8281
+ `oge_${stableId([edge.org, edge.sourceRepo, edge.sourcePath, edge.targetRepo, edge.targetPath ?? "", edge.relationship])}`,
8282
+ edge.org,
8283
+ edge.sourceRepo,
8284
+ edge.sourcePath,
8285
+ edge.targetRepo,
8286
+ edge.targetPath ?? null,
8287
+ edge.relationship,
8288
+ evidenceJson(edge.evidence),
8289
+ edge.confidence,
8290
+ now
8291
+ );
8292
+ }
8293
+ const insertContract = db.prepare(
8294
+ `INSERT INTO org_api_contracts
8295
+ (id, org, repo, file_path, contract, evidence_json, confidence, created_at)
8296
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
8297
+ );
8298
+ for (const contract of apiContracts) {
8299
+ insertContract.run(
8300
+ `oac_${stableId([config.org, contract.repo, contract.filePath, contract.contract])}`,
8301
+ config.org,
8302
+ contract.repo,
8303
+ contract.filePath,
8304
+ sanitizeHistoricalText(contract.contract),
8305
+ evidenceJson(contract.evidence),
8306
+ contract.confidence,
8307
+ now
8308
+ );
8309
+ }
8310
+ const insertConsumer = db.prepare(
8311
+ `INSERT INTO org_api_consumers
8312
+ (id, org, provider_repo, provider_path, consumer_repo, consumer_path, contract, evidence_json, confidence, created_at)
8313
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8314
+ );
8315
+ for (const consumer of apiConsumers) {
8316
+ insertConsumer.run(
8317
+ `oap_${stableId([
8318
+ consumer.org,
8319
+ consumer.providerRepo,
8320
+ consumer.providerPath ?? "",
8321
+ consumer.consumerRepo,
8322
+ consumer.consumerPath,
8323
+ consumer.contract
8324
+ ])}`,
8130
8325
  consumer.org,
8131
8326
  consumer.providerRepo,
8132
- consumer.providerPath ?? "",
8327
+ consumer.providerPath ?? null,
8133
8328
  consumer.consumerRepo,
8134
8329
  consumer.consumerPath,
8135
- consumer.contract
8136
- ])}`,
8137
- consumer.org,
8138
- consumer.providerRepo,
8139
- consumer.providerPath ?? null,
8140
- consumer.consumerRepo,
8141
- consumer.consumerPath,
8142
- sanitizeHistoricalText(consumer.contract),
8143
- evidenceJson(consumer.evidence),
8144
- consumer.confidence,
8145
- now
8146
- );
8147
- }
8148
- });
8149
- transaction();
8150
- return {
8151
- edges,
8152
- apiConsumers,
8153
- apiContracts
8154
- };
8330
+ sanitizeHistoricalText(consumer.contract),
8331
+ evidenceJson(consumer.evidence),
8332
+ consumer.confidence,
8333
+ now
8334
+ );
8335
+ }
8336
+ });
8337
+ transaction();
8338
+ const durationMs = Date.now() - startedAt;
8339
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
8340
+ recordOrgGraphState(db, {
8341
+ org: config.org,
8342
+ status: "success",
8343
+ builtAt: finishedAt,
8344
+ durationMs,
8345
+ edgeCount: edges.length,
8346
+ apiContractCount: apiContracts.length,
8347
+ apiConsumerCount: apiConsumers.length
8348
+ });
8349
+ options.onProgress?.({
8350
+ stage: "completed_org_graph",
8351
+ org: config.org,
8352
+ edges: edges.length,
8353
+ apiContracts: apiContracts.length,
8354
+ apiConsumers: apiConsumers.length,
8355
+ durationMs
8356
+ });
8357
+ return {
8358
+ edges,
8359
+ apiConsumers,
8360
+ apiContracts,
8361
+ durationMs
8362
+ };
8363
+ } catch (error) {
8364
+ const message = error instanceof Error ? error.message : String(error);
8365
+ recordOrgGraphState(db, {
8366
+ org: config.org,
8367
+ status: "failed",
8368
+ durationMs: Date.now() - startedAt,
8369
+ error: message
8370
+ });
8371
+ throw error;
8372
+ }
8155
8373
  }
8156
8374
 
8157
8375
  // src/org/index.ts
8158
8376
  import fs13 from "fs";
8377
+ var ORG_SYNC_RESUME_WINDOW_MS = 12 * 60 * 60 * 1e3;
8159
8378
  function readCommit(runner, cwd) {
8160
8379
  try {
8161
8380
  return runner("git", ["rev-parse", "HEAD"], { cwd });
@@ -8166,6 +8385,27 @@ function readCommit(runner, cwd) {
8166
8385
  function missingCloneError(repo, localPath) {
8167
8386
  return `Repo ${repo} is not cloned at ${localPath}. Run anchor org clone --repo ${repo} --org <org>.`;
8168
8387
  }
8388
+ function latestIsoDate(dates) {
8389
+ return dates.filter((date) => Boolean(date)).sort().at(-1);
8390
+ }
8391
+ function graphIsFreshForState(input) {
8392
+ const latestRepoIndexAt = latestIsoDate([input.lastPrSyncAt, input.lastCodeIndexedAt]);
8393
+ return Boolean(
8394
+ latestRepoIndexAt && input.graphStatus === "success" && input.graphBuiltAt && input.graphBuiltAt >= latestRepoIndexAt
8395
+ );
8396
+ }
8397
+ function isWithinResumeWindow(date) {
8398
+ const parsed = Date.parse(date);
8399
+ return Number.isFinite(parsed) && Date.now() - parsed <= ORG_SYNC_RESUME_WINDOW_MS;
8400
+ }
8401
+ function shouldSkipPrFetchForResume(input) {
8402
+ if (input.options.command !== "org sync") return false;
8403
+ if (input.options.force || input.options.since || input.options.noGraph) return false;
8404
+ if (input.options.codeOnly || input.options.prsOnly) return false;
8405
+ if (!input.lastPrSyncAt) return false;
8406
+ if (!isWithinResumeWindow(input.lastPrSyncAt)) return false;
8407
+ return !graphIsFreshForState(input);
8408
+ }
8169
8409
  async function indexOrgRepos(db, config, options = {}) {
8170
8410
  initializeSchema(db);
8171
8411
  syncOrgConfigToDatabase(db, config, options.baseDir);
@@ -8173,9 +8413,11 @@ async function indexOrgRepos(db, config, options = {}) {
8173
8413
  (repo) => repo.enabled && (!options.repo || repo.fullName === options.repo)
8174
8414
  );
8175
8415
  const runner = options.runner ?? defaultGitCommandRunner;
8416
+ const fetchPullRequests = options.fetchPullRequests ?? fetchMergedPullRequests;
8176
8417
  const auth = options.token ? { token: options.token } : resolveGitHubToken();
8177
8418
  const results = [];
8178
8419
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8420
+ const graphState = getOrgGraphState(db, config.org);
8179
8421
  for (const repo of repos) {
8180
8422
  const localPath = orgRepoLocalPath(config.org, repo, options.baseDir);
8181
8423
  const repoStartedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -8187,16 +8429,32 @@ async function indexOrgRepos(db, config, options = {}) {
8187
8429
  const state = getOrgRepoState(db, config.org, repo.fullName);
8188
8430
  let history;
8189
8431
  let code;
8432
+ let skippedHistory = false;
8433
+ let historySkippedReason;
8190
8434
  const repoFailures = [];
8191
8435
  if (!options.codeOnly) {
8192
- if (!auth.token) {
8436
+ if (shouldSkipPrFetchForResume({
8437
+ options,
8438
+ lastPrSyncAt: state?.lastPrSyncAt,
8439
+ lastCodeIndexedAt: state?.lastCodeIndexedAt,
8440
+ graphBuiltAt: graphState?.lastBuiltAt,
8441
+ graphStatus: graphState?.lastStatus
8442
+ })) {
8443
+ skippedHistory = true;
8444
+ historySkippedReason = "PR history already synced; resuming unfinished org graph/index work.";
8445
+ options.onFetchProgress?.({
8446
+ stage: "skipped_pull_request_fetch",
8447
+ repo: repo.fullName,
8448
+ reason: historySkippedReason
8449
+ });
8450
+ } else if (!auth.token) {
8193
8451
  repoFailures.push(
8194
8452
  "GitHub authentication is required for org PR indexing. Run gh auth login, or export GITHUB_TOKEN/GH_TOKEN with read-only access."
8195
8453
  );
8196
8454
  } else {
8197
8455
  try {
8198
8456
  const since = options.since ?? (options.command === "org sync" ? state?.lastPrSyncAt ?? getLastSyncTime(db, repo.fullName) : void 0);
8199
- const pullRequests = await fetchMergedPullRequests({
8457
+ const pullRequests = await fetchPullRequests({
8200
8458
  token: auth.token,
8201
8459
  repo: repo.fullName,
8202
8460
  limit: 200,
@@ -8257,6 +8515,8 @@ async function indexOrgRepos(db, config, options = {}) {
8257
8515
  results.push({
8258
8516
  repo: repo.fullName,
8259
8517
  skippedCode: Boolean(codeUnchanged || options.prsOnly),
8518
+ skippedHistory,
8519
+ historySkippedReason,
8260
8520
  currentCommit: currentCommit2,
8261
8521
  history,
8262
8522
  code,
@@ -8298,25 +8558,48 @@ async function indexOrgRepos(db, config, options = {}) {
8298
8558
  });
8299
8559
  }
8300
8560
  }
8301
- const graph = rebuildOrgGraph(db, config, options.baseDir);
8561
+ let graph;
8562
+ if (options.noGraph) {
8563
+ const counts = getOrgGraphCounts(db, config.org);
8564
+ recordOrgGraphState(db, {
8565
+ org: config.org,
8566
+ status: "skipped",
8567
+ edgeCount: counts.edges,
8568
+ apiContractCount: counts.apiContracts,
8569
+ apiConsumerCount: counts.apiConsumers
8570
+ });
8571
+ graph = { ...counts, skipped: true };
8572
+ } else {
8573
+ try {
8574
+ const rebuiltGraph = rebuildOrgGraph(db, config, {
8575
+ baseDir: options.baseDir,
8576
+ onProgress: options.onGraphProgress
8577
+ });
8578
+ graph = {
8579
+ edges: rebuiltGraph.edges.length,
8580
+ apiConsumers: rebuiltGraph.apiConsumers.length,
8581
+ apiContracts: rebuiltGraph.apiContracts.length
8582
+ };
8583
+ } catch (error) {
8584
+ const message = error instanceof Error ? error.message : String(error);
8585
+ const counts = getOrgGraphCounts(db, config.org);
8586
+ graph = { ...counts, error: message };
8587
+ }
8588
+ }
8302
8589
  recordOrgIndexRun(db, {
8303
8590
  org: config.org,
8304
8591
  command: options.command ?? "org index",
8305
8592
  startedAt,
8306
8593
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8307
- status: results.some((result) => result.error) ? "partial" : "success",
8594
+ status: results.some((result) => result.error) || graph.error ? "partial" : "success",
8308
8595
  prsIndexed: results.reduce((sum, result) => sum + (result.history?.indexedPrs ?? 0), 0),
8309
8596
  codeFilesIndexed: results.reduce((sum, result) => sum + (result.code?.indexedFiles ?? 0), 0),
8310
- failures: results.map((result) => result.error).filter((error) => Boolean(error))
8597
+ failures: results.map((result) => result.error).concat(graph.error ? [graph.error] : []).filter((error) => Boolean(error))
8311
8598
  });
8312
8599
  return {
8313
8600
  org: config.org,
8314
8601
  repos: results.sort((a, b) => a.repo.localeCompare(b.repo)),
8315
- graph: {
8316
- edges: graph.edges.length,
8317
- apiConsumers: graph.apiConsumers.length,
8318
- apiContracts: graph.apiContracts.length
8319
- }
8602
+ graph
8320
8603
  };
8321
8604
  }
8322
8605
 
@@ -9251,6 +9534,8 @@ export {
9251
9534
  getIndexStatus,
9252
9535
  getLastSyncTime,
9253
9536
  getOrgArchitectureMap,
9537
+ getOrgGraphCounts,
9538
+ getOrgGraphState,
9254
9539
  getOrgRepoState,
9255
9540
  getOrgStatus,
9256
9541
  getPlaybook,
@@ -9303,6 +9588,7 @@ export {
9303
9588
  rebuildOrgGraph,
9304
9589
  recordFeedback,
9305
9590
  recordIndexRun,
9591
+ recordOrgGraphState,
9306
9592
  recordOrgIndexRun,
9307
9593
  redactSecrets,
9308
9594
  redactedHistoricalText,