@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.d.ts +105 -3
- package/dist/index.js +464 -212
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/db/schema.sql +13 -0
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(
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
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
|
|
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
|
|
7934
|
-
const
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
const
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
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
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
const
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
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 ??
|
|
8075
|
-
edge.relationship
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
);
|
|
8080
|
-
}
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
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
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
contract
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
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
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
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
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
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
|
-
|
|
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,
|