@pratik7368patil/anchor-core 0.1.20 → 0.1.22

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
  );
@@ -6260,9 +6274,29 @@ function errorMessage(status, errors) {
6260
6274
  if (messages.length > 0) return messages.join("; ");
6261
6275
  return `GitHub GraphQL request failed with status ${status}.`;
6262
6276
  }
6277
+ function responsePreview(text) {
6278
+ return text.replace(/\s+/g, " ").trim().slice(0, 120);
6279
+ }
6280
+ function parseGraphQLResponse(text, status, headers) {
6281
+ try {
6282
+ return JSON.parse(text);
6283
+ } catch {
6284
+ const contentType = String(headers["content-type"] ?? "unknown");
6285
+ const preview = responsePreview(text);
6286
+ throw new GitHubGraphQLError(
6287
+ `GitHub GraphQL returned a non-JSON response with status ${status} and content-type ${contentType}.${preview ? ` Response preview: ${preview}` : ""}`,
6288
+ {
6289
+ status,
6290
+ headers
6291
+ }
6292
+ );
6293
+ }
6294
+ }
6263
6295
  function createGitHubGraphQLRequester(options) {
6264
6296
  if (!options.token.trim()) {
6265
- throw new Error("GitHub authentication is required. Run gh auth login, or export GITHUB_TOKEN/GH_TOKEN.");
6297
+ throw new Error(
6298
+ "GitHub authentication is required. Run gh auth login, or export GITHUB_TOKEN/GH_TOKEN."
6299
+ );
6266
6300
  }
6267
6301
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
6268
6302
  if (!fetchImpl) throw new Error("Global fetch is unavailable in this Node.js runtime.");
@@ -6280,7 +6314,7 @@ function createGitHubGraphQLRequester(options) {
6280
6314
  body: JSON.stringify({ query, variables })
6281
6315
  });
6282
6316
  const headers = headersToRecord(response.headers);
6283
- const raw = await response.json();
6317
+ const raw = parseGraphQLResponse(await response.text(), response.status, headers);
6284
6318
  if (!response.ok || raw.errors?.length) {
6285
6319
  throw new GitHubGraphQLError(errorMessage(response.status, raw.errors), {
6286
6320
  status: errorStatus(response.status, raw.errors),
@@ -7198,7 +7232,8 @@ function createProgressRateLimitController(repo, onProgress) {
7198
7232
  };
7199
7233
  }
7200
7234
  function shouldFallbackToRestAfterGraphQLError(error) {
7201
- return !isGitHubRateLimitError(error) && !isGitHubGraphQLResourceLimitError(error);
7235
+ const message = (error.message ?? "").toLowerCase();
7236
+ return !isGitHubRateLimitError(error) && !isGitHubGraphQLResourceLimitError(error) && !message.includes("non-json response");
7202
7237
  }
7203
7238
  async function fetchPullRequestDetailsConcurrently(options) {
7204
7239
  const results = new Array(options.pullNumbers.length);
@@ -7329,7 +7364,10 @@ async function fetchMergedPullRequests(options) {
7329
7364
  options.repo,
7330
7365
  options.onProgress
7331
7366
  );
7332
- const restRateLimitController = createProgressRateLimitController(options.repo, options.onProgress);
7367
+ const restRateLimitController = createProgressRateLimitController(
7368
+ options.repo,
7369
+ options.onProgress
7370
+ );
7333
7371
  try {
7334
7372
  return await fetchMergedPullRequestsWithGraphQL({
7335
7373
  token: options.token,
@@ -7629,10 +7667,47 @@ function recordOrgIndexRun(db, input) {
7629
7667
  JSON.stringify(input.failures ?? [])
7630
7668
  );
7631
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
+ }
7632
7699
  function count(db, table, where = "", params = []) {
7633
7700
  const row = db.prepare(`SELECT COUNT(*) AS count FROM ${table} ${where}`).get(...params);
7634
7701
  return row.count;
7635
7702
  }
7703
+ function getOrgGraphCounts(db, org) {
7704
+ initializeSchema(db);
7705
+ return {
7706
+ edges: count(db, "org_cross_repo_edges", "WHERE org = ?", [org]),
7707
+ apiContracts: count(db, "org_api_contracts", "WHERE org = ?", [org]),
7708
+ apiConsumers: count(db, "org_api_consumers", "WHERE org = ?", [org])
7709
+ };
7710
+ }
7636
7711
  function grade(score) {
7637
7712
  if (score <= 0) return "empty";
7638
7713
  if (score < 35) return "poor";
@@ -7654,8 +7729,10 @@ function getOrgStatus(db, config, baseDir) {
7654
7729
  const codeChunkCount = count(db, "code_chunks");
7655
7730
  const wisdomUnitCount = count(db, "wisdom_units");
7656
7731
  const crossRepoEdgeCount = count(db, "org_cross_repo_edges", "WHERE org = ?", [config.org]);
7732
+ const apiContractCount = count(db, "org_api_contracts", "WHERE org = ?", [config.org]);
7657
7733
  const apiConsumerCount = count(db, "org_api_consumers", "WHERE org = ?", [config.org]);
7658
7734
  const anomalyCount = count(db, "org_anomaly_events", "WHERE org = ?", [config.org]);
7735
+ const graphState = db.prepare("SELECT * FROM org_graph_state WHERE org = ?").get(config.org);
7659
7736
  let score = 0;
7660
7737
  const reasons = [];
7661
7738
  if (enabledRepos.length > 0) {
@@ -7694,8 +7771,13 @@ function getOrgStatus(db, config, baseDir) {
7694
7771
  codeChunkCount,
7695
7772
  wisdomUnitCount,
7696
7773
  crossRepoEdgeCount,
7774
+ apiContractCount,
7697
7775
  apiConsumerCount,
7698
7776
  anomalyCount,
7777
+ graphLastBuiltAt: graphState?.last_built_at ?? void 0,
7778
+ graphLastStatus: graphState?.last_status ?? void 0,
7779
+ graphLastDurationMs: graphState?.last_duration_ms ?? void 0,
7780
+ graphLastError: graphState?.last_error ?? void 0,
7699
7781
  coverageScore: score,
7700
7782
  coverageGrade: grade(score),
7701
7783
  coverageReasons: reasons,
@@ -7821,16 +7903,32 @@ async function cloneOrgRepos(input) {
7821
7903
  const repo = repos[next];
7822
7904
  next += 1;
7823
7905
  if (!repo) continue;
7824
- input.onProgress?.(`cloning or pulling ${repo.fullName}`);
7825
- results.push(
7826
- cloneOrPullOrgRepo({
7827
- org: input.config.org,
7828
- repo,
7829
- db: input.db,
7830
- baseDir: input.baseDir,
7831
- runner: input.runner
7832
- })
7833
- );
7906
+ const current = next;
7907
+ input.onProgress?.({
7908
+ stage: "cloning_or_pulling_repo",
7909
+ org: input.config.org,
7910
+ repo: repo.fullName,
7911
+ current,
7912
+ total: repos.length
7913
+ });
7914
+ const result = cloneOrPullOrgRepo({
7915
+ org: input.config.org,
7916
+ repo,
7917
+ db: input.db,
7918
+ baseDir: input.baseDir,
7919
+ runner: input.runner
7920
+ });
7921
+ results.push(result);
7922
+ input.onProgress?.({
7923
+ stage: "cloned_or_pulled_repo",
7924
+ org: input.config.org,
7925
+ repo: repo.fullName,
7926
+ current,
7927
+ total: repos.length,
7928
+ cloned: result.cloned,
7929
+ pulled: result.pulled,
7930
+ error: result.error
7931
+ });
7834
7932
  }
7835
7933
  }
7836
7934
  await Promise.all(Array.from({ length: Math.min(limit, repos.length) }, () => worker()));
@@ -7928,206 +8026,335 @@ function isApiConsumerText(text) {
7928
8026
  function evidenceJson(evidence) {
7929
8027
  return JSON.stringify(evidence);
7930
8028
  }
7931
- function rebuildOrgGraph(db, config, baseDir) {
8029
+ function shouldEmitProgress(current, total, interval = 100) {
8030
+ return current === 1 || current === total || current % interval === 0;
8031
+ }
8032
+ function resolveOptions(baseDirOrOptions) {
8033
+ return typeof baseDirOrOptions === "string" ? { baseDir: baseDirOrOptions } : baseDirOrOptions ?? {};
8034
+ }
8035
+ function rebuildOrgGraph(db, config, baseDirOrOptions) {
7932
8036
  initializeSchema(db);
7933
- const packageNames = repoPackageNames(config, baseDir);
7934
- const enabledRepos = config.repos.filter((repo) => repo.enabled);
7935
- const repoByName = new Map(enabledRepos.map((repo) => [repo.fullName, repo]));
7936
- const packageToRepo = /* @__PURE__ */ new Map();
7937
- for (const [repo, names] of packageNames.entries()) {
7938
- for (const name of names) packageToRepo.set(name, repo);
7939
- }
7940
- const edges = [];
7941
- const addEdge = (edge) => {
7942
- if (edge.sourceRepo === edge.targetRepo) return;
7943
- const key = [
7944
- edge.sourceRepo,
7945
- edge.sourcePath,
7946
- edge.targetRepo,
7947
- edge.targetPath ?? "",
7948
- edge.relationship
7949
- ].join("\0");
7950
- if (edges.some(
7951
- (existing) => [
7952
- existing.sourceRepo,
7953
- existing.sourcePath,
7954
- existing.targetRepo,
7955
- existing.targetPath ?? "",
7956
- existing.relationship
7957
- ].join("\0") === key
7958
- )) {
7959
- return;
7960
- }
7961
- edges.push(edge);
7962
- };
7963
- for (const repo of enabledRepos) {
7964
- const manifest = readPackageManifest(orgRepoLocalPath(config.org, repo, baseDir));
7965
- for (const dependency of dependenciesFor(manifest)) {
7966
- const targetRepo = packageToRepo.get(dependency);
7967
- if (!targetRepo || targetRepo === repo.fullName) continue;
7968
- addEdge({
7969
- org: config.org,
7970
- sourceRepo: repo.fullName,
7971
- sourcePath: "package.json",
7972
- targetRepo,
7973
- relationship: "depends_on_package",
7974
- evidence: [fileEvidence(repo.fullName, "package.json", `depends on ${dependency}`)],
7975
- confidence: 0.9
7976
- });
7977
- }
7978
- }
7979
- const imports = db.prepare(
7980
- `SELECT r.full_name AS repo, ci.source_path, ci.specifier, ci.imported_path, ci.imported_symbols_json
7981
- FROM code_imports ci
7982
- JOIN repositories r ON r.id = ci.repo_id`
7983
- ).all();
7984
- for (const item of imports) {
7985
- const sourceRepo = repoByName.get(item.repo);
7986
- if (!sourceRepo) continue;
7987
- for (const [targetRepo, names] of packageNames.entries()) {
7988
- if (targetRepo === item.repo) continue;
7989
- const matchedName = names.find(
7990
- (name) => item.specifier === name || item.specifier.startsWith(`${name}/`)
7991
- );
7992
- if (!matchedName) continue;
7993
- addEdge({
7994
- org: config.org,
7995
- sourceRepo: item.repo,
7996
- sourcePath: item.source_path,
7997
- targetRepo,
7998
- targetPath: item.imported_path ?? void 0,
7999
- relationship: "imports",
8000
- evidence: [
8001
- fileEvidence(
8002
- item.repo,
8003
- item.source_path,
8004
- `imports ${sanitizeHistoricalText(matchedName)}`
8005
- )
8006
- ],
8007
- confidence: parseJsonArray9(item.imported_symbols_json).length > 0 ? 0.88 : 0.76
8008
- });
8009
- }
8010
- }
8011
- const chunks = db.prepare(
8012
- `SELECT r.full_name AS repo, cc.file_path, cc.sanitized_text, cc.symbols_json
8013
- FROM code_chunks cc
8014
- JOIN repositories r ON r.id = cc.repo_id`
8015
- ).all();
8016
- const apiContracts = chunks.filter((chunk) => repoByName.has(chunk.repo) && isApiProviderPath(chunk.file_path)).flatMap(
8017
- (chunk) => extractContracts(chunk.sanitized_text).map((contract) => ({
8018
- repo: chunk.repo,
8019
- filePath: chunk.file_path,
8020
- contract,
8021
- evidence: [fileEvidence(chunk.repo, chunk.file_path, `defines ${contract}`)],
8022
- confidence: 0.74
8023
- }))
8024
- );
8025
- const apiConsumers = [];
8026
- for (const contract of apiContracts) {
8027
- for (const chunk of chunks) {
8028
- if (chunk.repo === contract.repo || !repoByName.has(chunk.repo)) continue;
8029
- if (!isApiConsumerText(chunk.sanitized_text)) continue;
8030
- if (!chunk.sanitized_text.includes(contract.contract)) continue;
8031
- const consumer = {
8032
- org: config.org,
8033
- providerRepo: contract.repo,
8034
- providerPath: contract.filePath,
8035
- consumerRepo: chunk.repo,
8036
- consumerPath: chunk.file_path,
8037
- contract: contract.contract,
8038
- evidence: [
8039
- ...contract.evidence,
8040
- fileEvidence(chunk.repo, chunk.file_path, `consumes ${contract.contract}`)
8041
- ],
8042
- confidence: 0.86
8043
- };
8044
- apiConsumers.push(consumer);
8045
- addEdge({
8046
- org: config.org,
8047
- sourceRepo: chunk.repo,
8048
- sourcePath: chunk.file_path,
8049
- targetRepo: contract.repo,
8050
- targetPath: contract.filePath,
8051
- relationship: "api_consumer",
8052
- evidence: consumer.evidence,
8053
- confidence: consumer.confidence
8054
- });
8037
+ const options = resolveOptions(baseDirOrOptions);
8038
+ const startedAt = Date.now();
8039
+ try {
8040
+ options.onProgress?.({
8041
+ stage: "loading_package_manifests",
8042
+ org: config.org,
8043
+ totalRepos: config.repos.filter((repo) => repo.enabled).length
8044
+ });
8045
+ const packageNames = repoPackageNames(config, options.baseDir);
8046
+ const enabledRepos = config.repos.filter((repo) => repo.enabled);
8047
+ const repoByName = new Map(enabledRepos.map((repo) => [repo.fullName, repo]));
8048
+ const packageToRepo = /* @__PURE__ */ new Map();
8049
+ for (const [repo, names] of packageNames.entries()) {
8050
+ for (const name of names) packageToRepo.set(name, repo);
8055
8051
  }
8056
- }
8057
- const now = (/* @__PURE__ */ new Date()).toISOString();
8058
- const transaction = db.transaction(() => {
8059
- db.prepare("DELETE FROM org_cross_repo_edges WHERE org = ?").run(config.org);
8060
- db.prepare("DELETE FROM org_api_contracts WHERE org = ?").run(config.org);
8061
- db.prepare("DELETE FROM org_api_consumers WHERE org = ?").run(config.org);
8062
- const insertEdge = db.prepare(
8063
- `INSERT INTO org_cross_repo_edges
8064
- (id, org, source_repo, source_path, target_repo, target_path, relationship, evidence_json, confidence, created_at)
8065
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8066
- );
8067
- for (const edge of edges) {
8068
- insertEdge.run(
8069
- `oge_${stableId([edge.org, edge.sourceRepo, edge.sourcePath, edge.targetRepo, edge.targetPath ?? "", edge.relationship])}`,
8070
- edge.org,
8052
+ options.onProgress?.({
8053
+ stage: "loaded_package_manifests",
8054
+ org: config.org,
8055
+ repos: enabledRepos.length,
8056
+ packageNames: packageToRepo.size
8057
+ });
8058
+ const edges = [];
8059
+ const edgeKeys = /* @__PURE__ */ new Set();
8060
+ const addEdge = (edge) => {
8061
+ if (edge.sourceRepo === edge.targetRepo) return;
8062
+ const key = [
8071
8063
  edge.sourceRepo,
8072
8064
  edge.sourcePath,
8073
8065
  edge.targetRepo,
8074
- edge.targetPath ?? null,
8075
- edge.relationship,
8076
- evidenceJson(edge.evidence),
8077
- edge.confidence,
8078
- now
8079
- );
8080
- }
8081
- const insertContract = db.prepare(
8082
- `INSERT INTO org_api_contracts
8083
- (id, org, repo, file_path, contract, evidence_json, confidence, created_at)
8084
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
8066
+ edge.targetPath ?? "",
8067
+ edge.relationship
8068
+ ].join("\0");
8069
+ if (edgeKeys.has(key)) return;
8070
+ edgeKeys.add(key);
8071
+ edges.push(edge);
8072
+ };
8073
+ enabledRepos.forEach((repo, index) => {
8074
+ const manifest = readPackageManifest(orgRepoLocalPath(config.org, repo, options.baseDir));
8075
+ for (const dependency of dependenciesFor(manifest)) {
8076
+ const targetRepo = packageToRepo.get(dependency);
8077
+ if (!targetRepo || targetRepo === repo.fullName) continue;
8078
+ addEdge({
8079
+ org: config.org,
8080
+ sourceRepo: repo.fullName,
8081
+ sourcePath: "package.json",
8082
+ targetRepo,
8083
+ relationship: "depends_on_package",
8084
+ evidence: [
8085
+ fileEvidence(
8086
+ repo.fullName,
8087
+ "package.json",
8088
+ `depends on ${sanitizeHistoricalText(dependency)}`
8089
+ )
8090
+ ],
8091
+ confidence: 0.9
8092
+ });
8093
+ }
8094
+ options.onProgress?.({
8095
+ stage: "building_package_edges",
8096
+ org: config.org,
8097
+ current: index + 1,
8098
+ total: enabledRepos.length,
8099
+ repo: repo.fullName,
8100
+ edges: edges.length
8101
+ });
8102
+ });
8103
+ options.onProgress?.({ stage: "loading_imports", org: config.org });
8104
+ const imports = db.prepare(
8105
+ `SELECT r.full_name AS repo, ci.source_path, ci.specifier, ci.imported_path, ci.imported_symbols_json
8106
+ FROM code_imports ci
8107
+ JOIN repositories r ON r.id = ci.repo_id`
8108
+ ).all();
8109
+ const packageMatchers = [...packageNames.entries()].flatMap(([repo, names]) => names.map((name) => ({ repo, name }))).sort((a, b) => b.name.length - a.name.length);
8110
+ imports.forEach((item, index) => {
8111
+ const sourceRepo = repoByName.get(item.repo);
8112
+ if (!sourceRepo) return;
8113
+ for (const candidate of packageMatchers) {
8114
+ if (candidate.repo === item.repo) continue;
8115
+ const matched = item.specifier === candidate.name || item.specifier.startsWith(`${candidate.name}/`);
8116
+ if (!matched) continue;
8117
+ addEdge({
8118
+ org: config.org,
8119
+ sourceRepo: item.repo,
8120
+ sourcePath: item.source_path,
8121
+ targetRepo: candidate.repo,
8122
+ targetPath: item.imported_path ?? void 0,
8123
+ relationship: "imports",
8124
+ evidence: [
8125
+ fileEvidence(
8126
+ item.repo,
8127
+ item.source_path,
8128
+ `imports ${sanitizeHistoricalText(candidate.name)}`
8129
+ )
8130
+ ],
8131
+ confidence: parseJsonArray9(item.imported_symbols_json).length > 0 ? 0.88 : 0.76
8132
+ });
8133
+ break;
8134
+ }
8135
+ if (shouldEmitProgress(index + 1, imports.length)) {
8136
+ options.onProgress?.({
8137
+ stage: "building_import_edges",
8138
+ org: config.org,
8139
+ current: index + 1,
8140
+ total: imports.length,
8141
+ sourcePath: item.source_path,
8142
+ edges: edges.length
8143
+ });
8144
+ }
8145
+ });
8146
+ options.onProgress?.({ stage: "loading_code_chunks", org: config.org });
8147
+ const chunks = db.prepare(
8148
+ `SELECT r.full_name AS repo, cc.file_path, cc.sanitized_text, cc.symbols_json
8149
+ FROM code_chunks cc
8150
+ JOIN repositories r ON r.id = cc.repo_id`
8151
+ ).all();
8152
+ const providerChunks = chunks.filter(
8153
+ (chunk) => repoByName.has(chunk.repo) && isApiProviderPath(chunk.file_path)
8085
8154
  );
8086
- for (const contract of apiContracts) {
8087
- insertContract.run(
8088
- `oac_${stableId([config.org, contract.repo, contract.filePath, contract.contract])}`,
8089
- config.org,
8090
- contract.repo,
8091
- contract.filePath,
8092
- sanitizeHistoricalText(contract.contract),
8093
- evidenceJson(contract.evidence),
8094
- contract.confidence,
8095
- now
8096
- );
8097
- }
8098
- const insertConsumer = db.prepare(
8099
- `INSERT INTO org_api_consumers
8100
- (id, org, provider_repo, provider_path, consumer_repo, consumer_path, contract, evidence_json, confidence, created_at)
8101
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8155
+ const apiContracts = [];
8156
+ const contractKeys = /* @__PURE__ */ new Set();
8157
+ const contractsByToken = /* @__PURE__ */ new Map();
8158
+ providerChunks.forEach((chunk, index) => {
8159
+ for (const contract of extractContracts(chunk.sanitized_text)) {
8160
+ const sanitizedContract = sanitizeHistoricalText(contract);
8161
+ const key = [chunk.repo, chunk.file_path, sanitizedContract].join("\0");
8162
+ if (contractKeys.has(key)) continue;
8163
+ contractKeys.add(key);
8164
+ const apiContract = {
8165
+ repo: chunk.repo,
8166
+ filePath: chunk.file_path,
8167
+ contract: sanitizedContract,
8168
+ evidence: [fileEvidence(chunk.repo, chunk.file_path, `defines ${sanitizedContract}`)],
8169
+ confidence: 0.74
8170
+ };
8171
+ apiContracts.push(apiContract);
8172
+ const bucket = contractsByToken.get(sanitizedContract) ?? [];
8173
+ bucket.push(apiContract);
8174
+ contractsByToken.set(sanitizedContract, bucket);
8175
+ }
8176
+ if (shouldEmitProgress(index + 1, providerChunks.length)) {
8177
+ options.onProgress?.({
8178
+ stage: "extracting_api_contracts",
8179
+ org: config.org,
8180
+ current: index + 1,
8181
+ total: providerChunks.length,
8182
+ filePath: chunk.file_path,
8183
+ contracts: apiContracts.length
8184
+ });
8185
+ }
8186
+ });
8187
+ const apiConsumers = [];
8188
+ const consumerKeys = /* @__PURE__ */ new Set();
8189
+ const consumerChunks = chunks.filter(
8190
+ (chunk) => repoByName.has(chunk.repo) && isApiConsumerText(chunk.sanitized_text)
8102
8191
  );
8103
- for (const consumer of apiConsumers) {
8104
- insertConsumer.run(
8105
- `oap_${stableId([
8192
+ consumerChunks.forEach((chunk, index) => {
8193
+ const consumerTokens = extractContracts(chunk.sanitized_text);
8194
+ let chunkMatches = 0;
8195
+ for (const token of consumerTokens) {
8196
+ const contracts = contractsByToken.get(sanitizeHistoricalText(token));
8197
+ if (!contracts) continue;
8198
+ for (const contract of contracts) {
8199
+ if (chunk.repo === contract.repo) continue;
8200
+ const consumerKey = [
8201
+ contract.repo,
8202
+ contract.filePath,
8203
+ chunk.repo,
8204
+ chunk.file_path,
8205
+ contract.contract
8206
+ ].join("\0");
8207
+ if (consumerKeys.has(consumerKey)) continue;
8208
+ consumerKeys.add(consumerKey);
8209
+ const consumer = {
8210
+ org: config.org,
8211
+ providerRepo: contract.repo,
8212
+ providerPath: contract.filePath,
8213
+ consumerRepo: chunk.repo,
8214
+ consumerPath: chunk.file_path,
8215
+ contract: contract.contract,
8216
+ evidence: [
8217
+ ...contract.evidence,
8218
+ fileEvidence(chunk.repo, chunk.file_path, `consumes ${contract.contract}`)
8219
+ ],
8220
+ confidence: 0.86
8221
+ };
8222
+ chunkMatches += 1;
8223
+ apiConsumers.push(consumer);
8224
+ addEdge({
8225
+ org: config.org,
8226
+ sourceRepo: chunk.repo,
8227
+ sourcePath: chunk.file_path,
8228
+ targetRepo: contract.repo,
8229
+ targetPath: contract.filePath,
8230
+ relationship: "api_consumer",
8231
+ evidence: consumer.evidence,
8232
+ confidence: consumer.confidence
8233
+ });
8234
+ }
8235
+ }
8236
+ if (shouldEmitProgress(index + 1, consumerChunks.length)) {
8237
+ options.onProgress?.({
8238
+ stage: "matching_api_consumers",
8239
+ org: config.org,
8240
+ current: index + 1,
8241
+ total: consumerChunks.length,
8242
+ filePath: chunk.file_path,
8243
+ matches: chunkMatches
8244
+ });
8245
+ }
8246
+ });
8247
+ options.onProgress?.({
8248
+ stage: "writing_org_graph",
8249
+ org: config.org,
8250
+ edges: edges.length,
8251
+ apiContracts: apiContracts.length,
8252
+ apiConsumers: apiConsumers.length
8253
+ });
8254
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8255
+ const transaction = db.transaction(() => {
8256
+ db.prepare("DELETE FROM org_cross_repo_edges WHERE org = ?").run(config.org);
8257
+ db.prepare("DELETE FROM org_api_contracts WHERE org = ?").run(config.org);
8258
+ db.prepare("DELETE FROM org_api_consumers WHERE org = ?").run(config.org);
8259
+ const insertEdge = db.prepare(
8260
+ `INSERT INTO org_cross_repo_edges
8261
+ (id, org, source_repo, source_path, target_repo, target_path, relationship, evidence_json, confidence, created_at)
8262
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8263
+ );
8264
+ for (const edge of edges) {
8265
+ insertEdge.run(
8266
+ `oge_${stableId([edge.org, edge.sourceRepo, edge.sourcePath, edge.targetRepo, edge.targetPath ?? "", edge.relationship])}`,
8267
+ edge.org,
8268
+ edge.sourceRepo,
8269
+ edge.sourcePath,
8270
+ edge.targetRepo,
8271
+ edge.targetPath ?? null,
8272
+ edge.relationship,
8273
+ evidenceJson(edge.evidence),
8274
+ edge.confidence,
8275
+ now
8276
+ );
8277
+ }
8278
+ const insertContract = db.prepare(
8279
+ `INSERT INTO org_api_contracts
8280
+ (id, org, repo, file_path, contract, evidence_json, confidence, created_at)
8281
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
8282
+ );
8283
+ for (const contract of apiContracts) {
8284
+ insertContract.run(
8285
+ `oac_${stableId([config.org, contract.repo, contract.filePath, contract.contract])}`,
8286
+ config.org,
8287
+ contract.repo,
8288
+ contract.filePath,
8289
+ sanitizeHistoricalText(contract.contract),
8290
+ evidenceJson(contract.evidence),
8291
+ contract.confidence,
8292
+ now
8293
+ );
8294
+ }
8295
+ const insertConsumer = db.prepare(
8296
+ `INSERT INTO org_api_consumers
8297
+ (id, org, provider_repo, provider_path, consumer_repo, consumer_path, contract, evidence_json, confidence, created_at)
8298
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8299
+ );
8300
+ for (const consumer of apiConsumers) {
8301
+ insertConsumer.run(
8302
+ `oap_${stableId([
8303
+ consumer.org,
8304
+ consumer.providerRepo,
8305
+ consumer.providerPath ?? "",
8306
+ consumer.consumerRepo,
8307
+ consumer.consumerPath,
8308
+ consumer.contract
8309
+ ])}`,
8106
8310
  consumer.org,
8107
8311
  consumer.providerRepo,
8108
- consumer.providerPath ?? "",
8312
+ consumer.providerPath ?? null,
8109
8313
  consumer.consumerRepo,
8110
8314
  consumer.consumerPath,
8111
- consumer.contract
8112
- ])}`,
8113
- consumer.org,
8114
- consumer.providerRepo,
8115
- consumer.providerPath ?? null,
8116
- consumer.consumerRepo,
8117
- consumer.consumerPath,
8118
- sanitizeHistoricalText(consumer.contract),
8119
- evidenceJson(consumer.evidence),
8120
- consumer.confidence,
8121
- now
8122
- );
8123
- }
8124
- });
8125
- transaction();
8126
- return {
8127
- edges,
8128
- apiConsumers,
8129
- apiContracts
8130
- };
8315
+ sanitizeHistoricalText(consumer.contract),
8316
+ evidenceJson(consumer.evidence),
8317
+ consumer.confidence,
8318
+ now
8319
+ );
8320
+ }
8321
+ });
8322
+ transaction();
8323
+ const durationMs = Date.now() - startedAt;
8324
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
8325
+ recordOrgGraphState(db, {
8326
+ org: config.org,
8327
+ status: "success",
8328
+ builtAt: finishedAt,
8329
+ durationMs,
8330
+ edgeCount: edges.length,
8331
+ apiContractCount: apiContracts.length,
8332
+ apiConsumerCount: apiConsumers.length
8333
+ });
8334
+ options.onProgress?.({
8335
+ stage: "completed_org_graph",
8336
+ org: config.org,
8337
+ edges: edges.length,
8338
+ apiContracts: apiContracts.length,
8339
+ apiConsumers: apiConsumers.length,
8340
+ durationMs
8341
+ });
8342
+ return {
8343
+ edges,
8344
+ apiConsumers,
8345
+ apiContracts,
8346
+ durationMs
8347
+ };
8348
+ } catch (error) {
8349
+ const message = error instanceof Error ? error.message : String(error);
8350
+ recordOrgGraphState(db, {
8351
+ org: config.org,
8352
+ status: "failed",
8353
+ durationMs: Date.now() - startedAt,
8354
+ error: message
8355
+ });
8356
+ throw error;
8357
+ }
8131
8358
  }
8132
8359
 
8133
8360
  // src/org/index.ts
@@ -8274,25 +8501,48 @@ async function indexOrgRepos(db, config, options = {}) {
8274
8501
  });
8275
8502
  }
8276
8503
  }
8277
- const graph = rebuildOrgGraph(db, config, options.baseDir);
8504
+ let graph;
8505
+ if (options.noGraph) {
8506
+ const counts = getOrgGraphCounts(db, config.org);
8507
+ recordOrgGraphState(db, {
8508
+ org: config.org,
8509
+ status: "skipped",
8510
+ edgeCount: counts.edges,
8511
+ apiContractCount: counts.apiContracts,
8512
+ apiConsumerCount: counts.apiConsumers
8513
+ });
8514
+ graph = { ...counts, skipped: true };
8515
+ } else {
8516
+ try {
8517
+ const rebuiltGraph = rebuildOrgGraph(db, config, {
8518
+ baseDir: options.baseDir,
8519
+ onProgress: options.onGraphProgress
8520
+ });
8521
+ graph = {
8522
+ edges: rebuiltGraph.edges.length,
8523
+ apiConsumers: rebuiltGraph.apiConsumers.length,
8524
+ apiContracts: rebuiltGraph.apiContracts.length
8525
+ };
8526
+ } catch (error) {
8527
+ const message = error instanceof Error ? error.message : String(error);
8528
+ const counts = getOrgGraphCounts(db, config.org);
8529
+ graph = { ...counts, error: message };
8530
+ }
8531
+ }
8278
8532
  recordOrgIndexRun(db, {
8279
8533
  org: config.org,
8280
8534
  command: options.command ?? "org index",
8281
8535
  startedAt,
8282
8536
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8283
- status: results.some((result) => result.error) ? "partial" : "success",
8537
+ status: results.some((result) => result.error) || graph.error ? "partial" : "success",
8284
8538
  prsIndexed: results.reduce((sum, result) => sum + (result.history?.indexedPrs ?? 0), 0),
8285
8539
  codeFilesIndexed: results.reduce((sum, result) => sum + (result.code?.indexedFiles ?? 0), 0),
8286
- failures: results.map((result) => result.error).filter((error) => Boolean(error))
8540
+ failures: results.map((result) => result.error).concat(graph.error ? [graph.error] : []).filter((error) => Boolean(error))
8287
8541
  });
8288
8542
  return {
8289
8543
  org: config.org,
8290
8544
  repos: results.sort((a, b) => a.repo.localeCompare(b.repo)),
8291
- graph: {
8292
- edges: graph.edges.length,
8293
- apiConsumers: graph.apiConsumers.length,
8294
- apiContracts: graph.apiContracts.length
8295
- }
8545
+ graph
8296
8546
  };
8297
8547
  }
8298
8548
 
@@ -9227,6 +9477,7 @@ export {
9227
9477
  getIndexStatus,
9228
9478
  getLastSyncTime,
9229
9479
  getOrgArchitectureMap,
9480
+ getOrgGraphCounts,
9230
9481
  getOrgRepoState,
9231
9482
  getOrgStatus,
9232
9483
  getPlaybook,
@@ -9279,6 +9530,7 @@ export {
9279
9530
  rebuildOrgGraph,
9280
9531
  recordFeedback,
9281
9532
  recordIndexRun,
9533
+ recordOrgGraphState,
9282
9534
  recordOrgIndexRun,
9283
9535
  redactSecrets,
9284
9536
  redactedHistoricalText,