@pratik7368patil/anchor-core 0.1.21 → 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
  );
@@ -7653,10 +7667,47 @@ 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
+ }
7656
7699
  function count(db, table, where = "", params = []) {
7657
7700
  const row = db.prepare(`SELECT COUNT(*) AS count FROM ${table} ${where}`).get(...params);
7658
7701
  return row.count;
7659
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
+ }
7660
7711
  function grade(score) {
7661
7712
  if (score <= 0) return "empty";
7662
7713
  if (score < 35) return "poor";
@@ -7678,8 +7729,10 @@ function getOrgStatus(db, config, baseDir) {
7678
7729
  const codeChunkCount = count(db, "code_chunks");
7679
7730
  const wisdomUnitCount = count(db, "wisdom_units");
7680
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]);
7681
7733
  const apiConsumerCount = count(db, "org_api_consumers", "WHERE org = ?", [config.org]);
7682
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);
7683
7736
  let score = 0;
7684
7737
  const reasons = [];
7685
7738
  if (enabledRepos.length > 0) {
@@ -7718,8 +7771,13 @@ function getOrgStatus(db, config, baseDir) {
7718
7771
  codeChunkCount,
7719
7772
  wisdomUnitCount,
7720
7773
  crossRepoEdgeCount,
7774
+ apiContractCount,
7721
7775
  apiConsumerCount,
7722
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,
7723
7781
  coverageScore: score,
7724
7782
  coverageGrade: grade(score),
7725
7783
  coverageReasons: reasons,
@@ -7845,16 +7903,32 @@ async function cloneOrgRepos(input) {
7845
7903
  const repo = repos[next];
7846
7904
  next += 1;
7847
7905
  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
- );
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
+ });
7858
7932
  }
7859
7933
  }
7860
7934
  await Promise.all(Array.from({ length: Math.min(limit, repos.length) }, () => worker()));
@@ -7952,206 +8026,335 @@ function isApiConsumerText(text) {
7952
8026
  function evidenceJson(evidence) {
7953
8027
  return JSON.stringify(evidence);
7954
8028
  }
7955
- 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) {
7956
8036
  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
- });
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);
8079
8051
  }
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,
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 = [
8095
8063
  edge.sourceRepo,
8096
8064
  edge.sourcePath,
8097
8065
  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 (?, ?, ?, ?, ?, ?, ?, ?)`
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)
8109
8154
  );
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
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)
8126
8191
  );
8127
- for (const consumer of apiConsumers) {
8128
- insertConsumer.run(
8129
- `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
+ ])}`,
8130
8310
  consumer.org,
8131
8311
  consumer.providerRepo,
8132
- consumer.providerPath ?? "",
8312
+ consumer.providerPath ?? null,
8133
8313
  consumer.consumerRepo,
8134
8314
  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
- };
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
+ }
8155
8358
  }
8156
8359
 
8157
8360
  // src/org/index.ts
@@ -8298,25 +8501,48 @@ async function indexOrgRepos(db, config, options = {}) {
8298
8501
  });
8299
8502
  }
8300
8503
  }
8301
- 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
+ }
8302
8532
  recordOrgIndexRun(db, {
8303
8533
  org: config.org,
8304
8534
  command: options.command ?? "org index",
8305
8535
  startedAt,
8306
8536
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8307
- status: results.some((result) => result.error) ? "partial" : "success",
8537
+ status: results.some((result) => result.error) || graph.error ? "partial" : "success",
8308
8538
  prsIndexed: results.reduce((sum, result) => sum + (result.history?.indexedPrs ?? 0), 0),
8309
8539
  codeFilesIndexed: results.reduce((sum, result) => sum + (result.code?.indexedFiles ?? 0), 0),
8310
- 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))
8311
8541
  });
8312
8542
  return {
8313
8543
  org: config.org,
8314
8544
  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
- }
8545
+ graph
8320
8546
  };
8321
8547
  }
8322
8548
 
@@ -9251,6 +9477,7 @@ export {
9251
9477
  getIndexStatus,
9252
9478
  getLastSyncTime,
9253
9479
  getOrgArchitectureMap,
9480
+ getOrgGraphCounts,
9254
9481
  getOrgRepoState,
9255
9482
  getOrgStatus,
9256
9483
  getPlaybook,
@@ -9303,6 +9530,7 @@ export {
9303
9530
  rebuildOrgGraph,
9304
9531
  recordFeedback,
9305
9532
  recordIndexRun,
9533
+ recordOrgGraphState,
9306
9534
  recordOrgIndexRun,
9307
9535
  redactSecrets,
9308
9536
  redactedHistoricalText,