seer-mcp 0.1.0
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/.vscode/settings.json +3 -0
- package/LICENSE +176 -0
- package/README.md +272 -0
- package/README_dev.md +199 -0
- package/dist/bundle/ci.d.ts +47 -0
- package/dist/bundle/ci.d.ts.map +1 -0
- package/dist/bundle/ci.js +113 -0
- package/dist/bundle/ci.js.map +1 -0
- package/dist/bundle/contract.d.ts +111 -0
- package/dist/bundle/contract.d.ts.map +1 -0
- package/dist/bundle/contract.js +352 -0
- package/dist/bundle/contract.js.map +1 -0
- package/dist/bundle/export.d.ts +36 -0
- package/dist/bundle/export.d.ts.map +1 -0
- package/dist/bundle/export.js +152 -0
- package/dist/bundle/export.js.map +1 -0
- package/dist/bundle/external.d.ts +66 -0
- package/dist/bundle/external.d.ts.map +1 -0
- package/dist/bundle/external.js +238 -0
- package/dist/bundle/external.js.map +1 -0
- package/dist/bundle/format.d.ts +94 -0
- package/dist/bundle/format.d.ts.map +1 -0
- package/dist/bundle/format.js +42 -0
- package/dist/bundle/format.js.map +1 -0
- package/dist/bundle/import.d.ts +49 -0
- package/dist/bundle/import.d.ts.map +1 -0
- package/dist/bundle/import.js +116 -0
- package/dist/bundle/import.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1402 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +48 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +284 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +616 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/store.d.ts +1011 -0
- package/dist/db/store.d.ts.map +1 -0
- package/dist/db/store.js +3888 -0
- package/dist/db/store.js.map +1 -0
- package/dist/graph/pagerank.d.ts +9 -0
- package/dist/graph/pagerank.d.ts.map +1 -0
- package/dist/graph/pagerank.js +47 -0
- package/dist/graph/pagerank.js.map +1 -0
- package/dist/indexer/architecture.d.ts +72 -0
- package/dist/indexer/architecture.d.ts.map +1 -0
- package/dist/indexer/architecture.js +112 -0
- package/dist/indexer/architecture.js.map +1 -0
- package/dist/indexer/behavior.d.ts +75 -0
- package/dist/indexer/behavior.d.ts.map +1 -0
- package/dist/indexer/behavior.js +395 -0
- package/dist/indexer/behavior.js.map +1 -0
- package/dist/indexer/boundaries.d.ts +60 -0
- package/dist/indexer/boundaries.d.ts.map +1 -0
- package/dist/indexer/boundaries.js +366 -0
- package/dist/indexer/boundaries.js.map +1 -0
- package/dist/indexer/churn.d.ts +15 -0
- package/dist/indexer/churn.d.ts.map +1 -0
- package/dist/indexer/churn.js +49 -0
- package/dist/indexer/churn.js.map +1 -0
- package/dist/indexer/classify.d.ts +9 -0
- package/dist/indexer/classify.d.ts.map +1 -0
- package/dist/indexer/classify.js +90 -0
- package/dist/indexer/classify.js.map +1 -0
- package/dist/indexer/context.d.ts +176 -0
- package/dist/indexer/context.d.ts.map +1 -0
- package/dist/indexer/context.js +193 -0
- package/dist/indexer/context.js.map +1 -0
- package/dist/indexer/continuity.d.ts +67 -0
- package/dist/indexer/continuity.d.ts.map +1 -0
- package/dist/indexer/continuity.js +288 -0
- package/dist/indexer/continuity.js.map +1 -0
- package/dist/indexer/detectchanges.d.ts +32 -0
- package/dist/indexer/detectchanges.d.ts.map +1 -0
- package/dist/indexer/detectchanges.js +74 -0
- package/dist/indexer/detectchanges.js.map +1 -0
- package/dist/indexer/discovery.d.ts +37 -0
- package/dist/indexer/discovery.d.ts.map +1 -0
- package/dist/indexer/discovery.js +136 -0
- package/dist/indexer/discovery.js.map +1 -0
- package/dist/indexer/externaldeps.d.ts +18 -0
- package/dist/indexer/externaldeps.d.ts.map +1 -0
- package/dist/indexer/externaldeps.js +288 -0
- package/dist/indexer/externaldeps.js.map +1 -0
- package/dist/indexer/freshness.d.ts +48 -0
- package/dist/indexer/freshness.d.ts.map +1 -0
- package/dist/indexer/freshness.js +128 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/git.d.ts +144 -0
- package/dist/indexer/git.d.ts.map +1 -0
- package/dist/indexer/git.js +444 -0
- package/dist/indexer/git.js.map +1 -0
- package/dist/indexer/index.d.ts +145 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +930 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/modules.d.ts +62 -0
- package/dist/indexer/modules.d.ts.map +1 -0
- package/dist/indexer/modules.js +293 -0
- package/dist/indexer/modules.js.map +1 -0
- package/dist/indexer/preflight.d.ts +154 -0
- package/dist/indexer/preflight.d.ts.map +1 -0
- package/dist/indexer/preflight.js +399 -0
- package/dist/indexer/preflight.js.map +1 -0
- package/dist/indexer/protoScanner.d.ts +34 -0
- package/dist/indexer/protoScanner.d.ts.map +1 -0
- package/dist/indexer/protoScanner.js +133 -0
- package/dist/indexer/protoScanner.js.map +1 -0
- package/dist/indexer/risk.d.ts +115 -0
- package/dist/indexer/risk.d.ts.map +1 -0
- package/dist/indexer/risk.js +194 -0
- package/dist/indexer/risk.js.map +1 -0
- package/dist/indexer/serviceHostScanner.d.ts +25 -0
- package/dist/indexer/serviceHostScanner.d.ts.map +1 -0
- package/dist/indexer/serviceHostScanner.js +95 -0
- package/dist/indexer/serviceHostScanner.js.map +1 -0
- package/dist/indexer/serviceLinks.d.ts +105 -0
- package/dist/indexer/serviceLinks.d.ts.map +1 -0
- package/dist/indexer/serviceLinks.js +509 -0
- package/dist/indexer/serviceLinks.js.map +1 -0
- package/dist/indexer/shapehash.d.ts +98 -0
- package/dist/indexer/shapehash.d.ts.map +1 -0
- package/dist/indexer/shapehash.js +354 -0
- package/dist/indexer/shapehash.js.map +1 -0
- package/dist/indexer/skeleton.d.ts +15 -0
- package/dist/indexer/skeleton.d.ts.map +1 -0
- package/dist/indexer/skeleton.js +136 -0
- package/dist/indexer/skeleton.js.map +1 -0
- package/dist/indexer/symbolhistory.d.ts +41 -0
- package/dist/indexer/symbolhistory.d.ts.map +1 -0
- package/dist/indexer/symbolhistory.js +124 -0
- package/dist/indexer/symbolhistory.js.map +1 -0
- package/dist/indexer/watcher.d.ts +68 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +179 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/server.d.ts +80 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +1610 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/parser/index.d.ts +8 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +33 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/languages/cpp.d.ts +3 -0
- package/dist/parser/languages/cpp.d.ts.map +1 -0
- package/dist/parser/languages/cpp.js +350 -0
- package/dist/parser/languages/cpp.js.map +1 -0
- package/dist/parser/languages/csharp.d.ts +3 -0
- package/dist/parser/languages/csharp.d.ts.map +1 -0
- package/dist/parser/languages/csharp.js +239 -0
- package/dist/parser/languages/csharp.js.map +1 -0
- package/dist/parser/languages/go.d.ts +3 -0
- package/dist/parser/languages/go.d.ts.map +1 -0
- package/dist/parser/languages/go.js +259 -0
- package/dist/parser/languages/go.js.map +1 -0
- package/dist/parser/languages/java.d.ts +3 -0
- package/dist/parser/languages/java.d.ts.map +1 -0
- package/dist/parser/languages/java.js +391 -0
- package/dist/parser/languages/java.js.map +1 -0
- package/dist/parser/languages/python.d.ts +3 -0
- package/dist/parser/languages/python.d.ts.map +1 -0
- package/dist/parser/languages/python.js +396 -0
- package/dist/parser/languages/python.js.map +1 -0
- package/dist/parser/languages/rust.d.ts +3 -0
- package/dist/parser/languages/rust.d.ts.map +1 -0
- package/dist/parser/languages/rust.js +159 -0
- package/dist/parser/languages/rust.js.map +1 -0
- package/dist/parser/languages/typescript.d.ts +3 -0
- package/dist/parser/languages/typescript.d.ts.map +1 -0
- package/dist/parser/languages/typescript.js +1442 -0
- package/dist/parser/languages/typescript.js.map +1 -0
- package/dist/parser/parserContext.d.ts +77 -0
- package/dist/parser/parserContext.d.ts.map +1 -0
- package/dist/parser/parserContext.js +354 -0
- package/dist/parser/parserContext.js.map +1 -0
- package/dist/parser/walker.d.ts +81 -0
- package/dist/parser/walker.d.ts.map +1 -0
- package/dist/parser/walker.js +217 -0
- package/dist/parser/walker.js.map +1 -0
- package/dist/parser/worker.d.ts +66 -0
- package/dist/parser/worker.d.ts.map +1 -0
- package/dist/parser/worker.js +129 -0
- package/dist/parser/worker.js.map +1 -0
- package/dist/parser/workerpool.d.ts +107 -0
- package/dist/parser/workerpool.d.ts.map +1 -0
- package/dist/parser/workerpool.js +383 -0
- package/dist/parser/workerpool.js.map +1 -0
- package/dist/scip/format.d.ts +87 -0
- package/dist/scip/format.d.ts.map +1 -0
- package/dist/scip/format.js +31 -0
- package/dist/scip/format.js.map +1 -0
- package/dist/scip/import.d.ts +37 -0
- package/dist/scip/import.d.ts.map +1 -0
- package/dist/scip/import.js +180 -0
- package/dist/scip/import.js.map +1 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/docs/architecture.md +105 -0
- package/docs/benchmarks/methodology.md +134 -0
- package/docs/benchmarks/raw-results.md +71 -0
- package/docs/benchmarks.md +74 -0
- package/docs/cli.md +148 -0
- package/docs/examples/behavior-tests.md +70 -0
- package/docs/examples/change-history.md +85 -0
- package/docs/examples/pre-edit-context.md +81 -0
- package/docs/examples/service-links.md +88 -0
- package/docs/examples.md +80 -0
- package/docs/faq.md +70 -0
- package/docs/internals.md +104 -0
- package/docs/languages.md +70 -0
- package/docs/limits.md +52 -0
- package/docs/mcp.md +199 -0
- package/docs/quickstart.md +119 -0
- package/docs/testing.md +123 -0
- package/docs/tools.md +115 -0
- package/package.json +52 -0
- package/research-codebase.md +578 -0
- package/seer-cli-docs.md +326 -0
- package/seer-master-guide.md +246 -0
- package/src/bundle/ci.ts +141 -0
- package/src/bundle/contract.ts +387 -0
- package/src/bundle/export.ts +175 -0
- package/src/bundle/external.ts +285 -0
- package/src/bundle/format.ts +92 -0
- package/src/bundle/import.ts +157 -0
- package/src/cli/index.ts +1249 -0
- package/src/cli/init.ts +389 -0
- package/src/db/schema.ts +614 -0
- package/src/db/store.ts +4306 -0
- package/src/graph/pagerank.ts +53 -0
- package/src/indexer/architecture.ts +148 -0
- package/src/indexer/behavior.ts +466 -0
- package/src/indexer/boundaries.ts +374 -0
- package/src/indexer/churn.ts +58 -0
- package/src/indexer/classify.ts +96 -0
- package/src/indexer/context.ts +340 -0
- package/src/indexer/continuity.ts +322 -0
- package/src/indexer/detectchanges.ts +94 -0
- package/src/indexer/discovery.ts +176 -0
- package/src/indexer/externaldeps.ts +243 -0
- package/src/indexer/freshness.ts +166 -0
- package/src/indexer/git.ts +453 -0
- package/src/indexer/index.ts +1092 -0
- package/src/indexer/modules.ts +358 -0
- package/src/indexer/preflight.ts +548 -0
- package/src/indexer/protoScanner.ts +147 -0
- package/src/indexer/risk.ts +304 -0
- package/src/indexer/serviceHostScanner.ts +92 -0
- package/src/indexer/serviceLinks.ts +543 -0
- package/src/indexer/shapehash.ts +370 -0
- package/src/indexer/skeleton.ts +169 -0
- package/src/indexer/symbolhistory.ts +172 -0
- package/src/indexer/watcher.ts +206 -0
- package/src/mcp/server.ts +1659 -0
- package/src/parser/index.ts +37 -0
- package/src/parser/languages/cpp.ts +361 -0
- package/src/parser/languages/csharp.ts +235 -0
- package/src/parser/languages/go.ts +259 -0
- package/src/parser/languages/java.ts +382 -0
- package/src/parser/languages/python.ts +370 -0
- package/src/parser/languages/rust.ts +164 -0
- package/src/parser/languages/typescript.ts +1435 -0
- package/src/parser/parserContext.ts +392 -0
- package/src/parser/walker.ts +306 -0
- package/src/parser/worker.ts +181 -0
- package/src/parser/workerpool.ts +448 -0
- package/src/scip/format.ts +83 -0
- package/src/scip/import.ts +216 -0
- package/src/types.ts +457 -0
- package/tests/benchmark-service-links.ts +244 -0
- package/tests/bug-regressions.ts +626 -0
- package/tests/filters.ts +264 -0
- package/tests/fixtures/Counter.tsx +38 -0
- package/tests/fixtures/caller.ts +7 -0
- package/tests/fixtures/collisions.ts +23 -0
- package/tests/fixtures/local_helper.ts +5 -0
- package/tests/fixtures/overloads.java +17 -0
- package/tests/fixtures/remote_helper.ts +4 -0
- package/tests/fixtures/sample.c +15 -0
- package/tests/fixtures/sample.cpp +47 -0
- package/tests/fixtures/sample.cs +62 -0
- package/tests/fixtures/sample.go +68 -0
- package/tests/fixtures/sample.h +30 -0
- package/tests/fixtures/sample.java +85 -0
- package/tests/fixtures/sample.py +46 -0
- package/tests/fixtures/sample.rs +78 -0
- package/tests/fixtures/sample.ts +76 -0
- package/tests/fixtures-service/HttpClients.cs +30 -0
- package/tests/fixtures-service/HttpClients.java +24 -0
- package/tests/fixtures-service/billing.ts +15 -0
- package/tests/fixtures-service/docker-compose.yml +15 -0
- package/tests/fixtures-service/gateway.ts +10 -0
- package/tests/fixtures-service/get_user.ts +11 -0
- package/tests/fixtures-service/graphql_client.ts +63 -0
- package/tests/fixtures-service/graphql_server.ts +30 -0
- package/tests/fixtures-service/grpc_client.go +30 -0
- package/tests/fixtures-service/http_clients.go +23 -0
- package/tests/fixtures-service/http_clients.py +38 -0
- package/tests/fixtures-service/http_clients.ts +49 -0
- package/tests/fixtures-service/k8s/payment-service.yaml +22 -0
- package/tests/fixtures-service/k8s_calls.ts +20 -0
- package/tests/fixtures-service/messaging.ts +87 -0
- package/tests/fixtures-service/trpc_client.ts +39 -0
- package/tests/fixtures-service/trpc_server.ts +39 -0
- package/tests/fixtures-service/user_service.proto +33 -0
- package/tests/fixtures-trackcd/Cargo.toml +11 -0
- package/tests/fixtures-trackcd/SpringController.java +36 -0
- package/tests/fixtures-trackcd/auth_service.ts +19 -0
- package/tests/fixtures-trackcd/complex_module.py +50 -0
- package/tests/fixtures-trackcd/express_app.js +30 -0
- package/tests/fixtures-trackcd/fastapi_app.py +49 -0
- package/tests/fixtures-trackcd/fastify_object_routes.js +32 -0
- package/tests/fixtures-trackcd/go.mod +8 -0
- package/tests/fixtures-trackcd/package.json +15 -0
- package/tests/fixtures-trackcd/requirements.txt +4 -0
- package/tests/fixtures-trackcd/tests/auth_service.test.ts +13 -0
- package/tests/fixtures-tracke/auth/AuthService.ts +23 -0
- package/tests/fixtures-tracke/auth/crypto.ts +7 -0
- package/tests/fixtures-tracke/billing/Billing.ts +20 -0
- package/tests/fixtures-tracke/billing/Invoice.ts +10 -0
- package/tests/fixtures-tracke/billing/server.ts +17 -0
- package/tests/fixtures-tracke/package.json +7 -0
- package/tests/fixtures-tracke/tests/auth.test.ts +23 -0
- package/tests/fixtures-tracke/tests/billing.test.ts +14 -0
- package/tests/fixtures-trackf/package.json +5 -0
- package/tests/fixtures-trackf/src/auth.ts +26 -0
- package/tests/fixtures-trackf/src/handlers.ts +35 -0
- package/tests/fixtures-tracki/billing/routes.ts +12 -0
- package/tests/fixtures-tracki/gateway/client.ts +13 -0
- package/tests/git-features.ts +267 -0
- package/tests/init.ts +141 -0
- package/tests/mcp-jit.ts +130 -0
- package/tests/mcp-smoke.ts +191 -0
- package/tests/mcp-trackcd.ts +169 -0
- package/tests/mcp-tracke.ts +229 -0
- package/tests/mcp-trackf.ts +330 -0
- package/tests/mcp-trackg.ts +219 -0
- package/tests/mcp-tracki.ts +174 -0
- package/tests/mcp-watcher.ts +126 -0
- package/tests/optspec.ts +194 -0
- package/tests/parallel-index.ts +333 -0
- package/tests/parallel-read.ts +125 -0
- package/tests/parallel-recovery.ts +241 -0
- package/tests/perf-callers.ts +145 -0
- package/tests/query-parity.ts +184 -0
- package/tests/query-perf.ts +55 -0
- package/tests/scale-parallel-parity.ts +225 -0
- package/tests/scale-test.ts +523 -0
- package/tests/smoke.ts +396 -0
- package/tests/trackcd.ts +325 -0
- package/tests/tracke-collisions.ts +255 -0
- package/tests/tracke.ts +314 -0
- package/tests/trackf-bugs.ts +406 -0
- package/tests/trackf.ts +390 -0
- package/tests/trackg.ts +1372 -0
- package/tests/tracki-boundaries.ts +202 -0
- package/tests/tracki-continuity.ts +253 -0
- package/tests/tracki-contract-diff.ts +249 -0
- package/tests/tracki-external-bundles.ts +341 -0
- package/tests/tracki-preflight.ts +251 -0
- package/tests/verify-roles.ts +51 -0
- package/tests/worker-parity.ts +286 -0
- package/tests/worker-pool.ts +262 -0
- package/tsconfig.json +20 -0
package/src/bundle/ci.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Store } from '../db/store.js';
|
|
4
|
+
import { Indexer } from '../indexer/index.js';
|
|
5
|
+
import { exportBundle, ExportResult } from './export.js';
|
|
6
|
+
import type { DiscoveryMode } from '../indexer/discovery.js';
|
|
7
|
+
|
|
8
|
+
export interface CiBuildOptions {
|
|
9
|
+
/** Repo root to index. Required. */
|
|
10
|
+
repoRoot: string;
|
|
11
|
+
/** Where to write the bundle. Defaults to `<repoRoot>/.seer/index.seerbundle`. */
|
|
12
|
+
out?: string;
|
|
13
|
+
/** Discovery mode for the index pass. CI defaults to `'standard'`. */
|
|
14
|
+
mode?: DiscoveryMode;
|
|
15
|
+
/** Wipe any existing DB before the indexing pass. CI defaults to `true`. */
|
|
16
|
+
reset?: boolean;
|
|
17
|
+
/** Pass through to the indexer's `parallel` toggle. */
|
|
18
|
+
parallel?: boolean;
|
|
19
|
+
/** Optional gitHead/gitBranch overrides (CI runners frequently know them). */
|
|
20
|
+
gitHead?: string;
|
|
21
|
+
gitBranch?: string;
|
|
22
|
+
/** Pin manifest.builtAt for reproducible bundles (defaults to Date.now()). */
|
|
23
|
+
builtAt?: number;
|
|
24
|
+
/** Logger. */
|
|
25
|
+
log?: (msg: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CiBuildResult {
|
|
29
|
+
/** Result of the indexing pass (file/symbol counts). */
|
|
30
|
+
index: {
|
|
31
|
+
filesIndexed: number;
|
|
32
|
+
symbols: number;
|
|
33
|
+
edges: number;
|
|
34
|
+
elapsedMs: number;
|
|
35
|
+
};
|
|
36
|
+
/** Result of the bundle export. */
|
|
37
|
+
bundle: ExportResult;
|
|
38
|
+
totalElapsedMs: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One-call CI pipeline: fresh-index a repo, export the result as a portable
|
|
43
|
+
* bundle, and report both phases. Exits with a non-zero status if either
|
|
44
|
+
* phase fails — wired up in the CLI command's caller.
|
|
45
|
+
*/
|
|
46
|
+
export async function buildCiBundle(options: CiBuildOptions): Promise<CiBuildResult> {
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
const log = options.log ?? ((msg: string) => process.stdout.write(`${msg}\n`));
|
|
49
|
+
const repoRoot = path.resolve(options.repoRoot);
|
|
50
|
+
if (!fs.existsSync(repoRoot)) {
|
|
51
|
+
throw new Error(`Repo not found: ${repoRoot}`);
|
|
52
|
+
}
|
|
53
|
+
const dbPath = path.join(repoRoot, '.seer', 'graph.db');
|
|
54
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
55
|
+
|
|
56
|
+
// CI mode wipes the DB by default — every pipeline run should be a clean
|
|
57
|
+
// snapshot, not an incremental update over whatever junk was on the runner.
|
|
58
|
+
const reset = options.reset ?? true;
|
|
59
|
+
if (reset && fs.existsSync(dbPath)) {
|
|
60
|
+
fs.unlinkSync(dbPath);
|
|
61
|
+
for (const sfx of ['-wal', '-shm']) {
|
|
62
|
+
try { fs.unlinkSync(dbPath + sfx); } catch { /* */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
log(`[ci] Indexing ${repoRoot}...`);
|
|
67
|
+
const store = new Store(dbPath);
|
|
68
|
+
const indexer = new Indexer(store);
|
|
69
|
+
let indexResult;
|
|
70
|
+
try {
|
|
71
|
+
indexResult = await indexer.indexDirectory(repoRoot, {
|
|
72
|
+
mode: options.mode ?? 'standard',
|
|
73
|
+
parallel: options.parallel,
|
|
74
|
+
quiet: true,
|
|
75
|
+
});
|
|
76
|
+
} finally {
|
|
77
|
+
store.close();
|
|
78
|
+
}
|
|
79
|
+
log(`[ci] Indexed ${indexResult.filesIndexed} files, ${indexResult.symbols} symbols, ${indexResult.edges} edges in ${indexResult.elapsedMs}ms`);
|
|
80
|
+
|
|
81
|
+
log(`[ci] Exporting bundle...`);
|
|
82
|
+
const bundle = await exportBundle(dbPath, repoRoot, {
|
|
83
|
+
out: options.out,
|
|
84
|
+
log: (m) => log(` ${m}`),
|
|
85
|
+
gitHead: options.gitHead,
|
|
86
|
+
gitBranch: options.gitBranch,
|
|
87
|
+
builtAt: options.builtAt,
|
|
88
|
+
});
|
|
89
|
+
log(`[ci] Wrote ${bundle.bundlePath} (${bundle.bytes.toLocaleString()} bytes) in ${bundle.elapsedMs}ms`);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
index: {
|
|
93
|
+
filesIndexed: indexResult.filesIndexed,
|
|
94
|
+
symbols: indexResult.symbols,
|
|
95
|
+
edges: indexResult.edges,
|
|
96
|
+
elapsedMs: indexResult.elapsedMs,
|
|
97
|
+
},
|
|
98
|
+
bundle,
|
|
99
|
+
totalElapsedMs: Date.now() - start,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Emit a ready-to-paste GitHub Actions workflow that runs the CI bundle
|
|
105
|
+
* pipeline on every push to main and uploads the resulting bundle as a
|
|
106
|
+
* build artifact. The workflow is self-contained — no per-repo edits
|
|
107
|
+
* required beyond dropping it at `.github/workflows/seer-bundle.yml`.
|
|
108
|
+
*/
|
|
109
|
+
export function workflowTemplate(): string {
|
|
110
|
+
return [
|
|
111
|
+
"name: Seer bundle",
|
|
112
|
+
"",
|
|
113
|
+
"on:",
|
|
114
|
+
" push:",
|
|
115
|
+
" branches: [main]",
|
|
116
|
+
" workflow_dispatch:",
|
|
117
|
+
"",
|
|
118
|
+
"jobs:",
|
|
119
|
+
" build:",
|
|
120
|
+
" runs-on: ubuntu-latest",
|
|
121
|
+
" permissions:",
|
|
122
|
+
" contents: read",
|
|
123
|
+
" steps:",
|
|
124
|
+
" - uses: actions/checkout@v4",
|
|
125
|
+
" with:",
|
|
126
|
+
" fetch-depth: 0 # full history so symbol-history pass works",
|
|
127
|
+
" - uses: actions/setup-node@v4",
|
|
128
|
+
" with:",
|
|
129
|
+
" node-version: '22'",
|
|
130
|
+
" - run: npm ci",
|
|
131
|
+
" - run: npm run build",
|
|
132
|
+
" - name: Build Seer bundle",
|
|
133
|
+
" run: node dist/cli/index.js ci bundle --workspace ${{ github.workspace }} --out seer-index.seerbundle",
|
|
134
|
+
" - uses: actions/upload-artifact@v4",
|
|
135
|
+
" with:",
|
|
136
|
+
" name: seer-index",
|
|
137
|
+
" path: seer-index.seerbundle",
|
|
138
|
+
" if-no-files-found: error",
|
|
139
|
+
"",
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v10 — Contract Diff between two `.seerbundle` artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Goal: deterministically compute the change set in the API/service surface
|
|
5
|
+
* exposed by an indexed repo, treating the bundle as the source of truth.
|
|
6
|
+
* The diff is ADVISORY — exit code is always 0 even when breaking changes
|
|
7
|
+
* appear. Seer-Core does not gate CI.
|
|
8
|
+
*
|
|
9
|
+
* Comparison happens at the protocol-aware "endpoint" level:
|
|
10
|
+
* - HTTP: key = `${method}|${path}`, framework recorded
|
|
11
|
+
* - tRPC: key = `trpc|${operation}` (method = 'query' / 'mutation' / 'subscription')
|
|
12
|
+
* - GraphQL: key = `graphql|${operation}`
|
|
13
|
+
* - gRPC: key = `grpc|${service}.${operation}`
|
|
14
|
+
* - Messaging (kafka/sqs/sns/rabbitmq/nats/redis_pubsub):
|
|
15
|
+
* key = `${protocol}|${topic|queue|exchange}`
|
|
16
|
+
*
|
|
17
|
+
* Output:
|
|
18
|
+
* - added[]
|
|
19
|
+
* - removed[]
|
|
20
|
+
* - changed[] (key matched; surfaced field differs — handler, framework,
|
|
21
|
+
* metadata, service)
|
|
22
|
+
* - optionally affectedCallers[] when both bundles include service-link
|
|
23
|
+
* evidence (caller symbol qualifiedName / file).
|
|
24
|
+
*
|
|
25
|
+
* Deterministic ordering: every list is sorted by (protocol, key) ASC.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from 'fs';
|
|
29
|
+
import path from 'path';
|
|
30
|
+
import os from 'os';
|
|
31
|
+
import zlib from 'zlib';
|
|
32
|
+
import { BUNDLE_MAGIC, BUNDLE_FORMAT_VERSION, BundleManifest } from './format.js';
|
|
33
|
+
import { Store } from '../db/store.js';
|
|
34
|
+
|
|
35
|
+
export interface ContractEndpoint {
|
|
36
|
+
protocol: string;
|
|
37
|
+
/** Stable identity key for this endpoint within its protocol. */
|
|
38
|
+
key: string;
|
|
39
|
+
method: string | null;
|
|
40
|
+
path: string | null;
|
|
41
|
+
framework: string | null;
|
|
42
|
+
handlerName: string | null;
|
|
43
|
+
operation: string | null;
|
|
44
|
+
topic: string | null;
|
|
45
|
+
queue: string | null;
|
|
46
|
+
exchange: string | null;
|
|
47
|
+
service: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AffectedCaller {
|
|
51
|
+
callerQualifiedName: string | null;
|
|
52
|
+
callerFile: string | null;
|
|
53
|
+
callerLine: number | null;
|
|
54
|
+
/** Where the caller is referenced — either 'old' or 'new' bundle. */
|
|
55
|
+
source: 'old' | 'new';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ContractChange {
|
|
59
|
+
protocol: string;
|
|
60
|
+
key: string;
|
|
61
|
+
before: ContractEndpoint;
|
|
62
|
+
after: ContractEndpoint;
|
|
63
|
+
/** Field names whose value differs between before/after. */
|
|
64
|
+
changedFields: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ContractDiff {
|
|
68
|
+
oldBundle: { path: string; gitHead: string | null; rosterHash: string };
|
|
69
|
+
newBundle: { path: string; gitHead: string | null; rosterHash: string };
|
|
70
|
+
/** Per-protocol totals on both sides, for sanity. */
|
|
71
|
+
totals: {
|
|
72
|
+
old: number;
|
|
73
|
+
new: number;
|
|
74
|
+
added: number;
|
|
75
|
+
removed: number;
|
|
76
|
+
changed: number;
|
|
77
|
+
};
|
|
78
|
+
added: Array<ContractEndpoint & { affectedCallers?: AffectedCaller[] }>;
|
|
79
|
+
removed: Array<ContractEndpoint & { affectedCallers?: AffectedCaller[] }>;
|
|
80
|
+
changed: Array<ContractChange & { affectedCallers?: AffectedCaller[] }>;
|
|
81
|
+
/** True when the affected-callers section is non-empty for at least one row. */
|
|
82
|
+
affectedCallersAvailable: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Open a bundle's payload as a read-only Store (decompressed to a temp file).
|
|
87
|
+
* Returns the Store plus a cleanup() function the caller MUST invoke.
|
|
88
|
+
*/
|
|
89
|
+
async function openBundleStore(bundlePath: string): Promise<{
|
|
90
|
+
store: Store; manifest: BundleManifest; cleanup: () => void;
|
|
91
|
+
}> {
|
|
92
|
+
if (!fs.existsSync(bundlePath)) {
|
|
93
|
+
throw new Error(`No bundle at ${bundlePath}`);
|
|
94
|
+
}
|
|
95
|
+
const fileBuf = fs.readFileSync(bundlePath);
|
|
96
|
+
if (fileBuf.length < 12 || !fileBuf.subarray(0, 4).equals(BUNDLE_MAGIC)) {
|
|
97
|
+
throw new Error(`Not a Seer bundle: ${bundlePath} (bad magic)`);
|
|
98
|
+
}
|
|
99
|
+
const formatVersion = fileBuf.readUInt32BE(4);
|
|
100
|
+
if (formatVersion > BUNDLE_FORMAT_VERSION) {
|
|
101
|
+
throw new Error(`Bundle format v${formatVersion} is newer than this build (v${BUNDLE_FORMAT_VERSION}). Upgrade Seer.`);
|
|
102
|
+
}
|
|
103
|
+
const manifestLen = fileBuf.readUInt32BE(8);
|
|
104
|
+
const manifestEnd = 12 + manifestLen;
|
|
105
|
+
if (manifestLen <= 0 || manifestEnd > fileBuf.length) {
|
|
106
|
+
throw new Error(`Bundle truncated: ${bundlePath} (manifest length ${manifestLen} exceeds file size)`);
|
|
107
|
+
}
|
|
108
|
+
const manifest = JSON.parse(fileBuf.slice(12, manifestEnd).toString('utf-8')) as BundleManifest;
|
|
109
|
+
const dbBuf = zlib.gunzipSync(fileBuf.slice(manifestEnd));
|
|
110
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'seer-contractdiff-'));
|
|
111
|
+
const dbPath = path.join(tmpDir, 'bundle.db');
|
|
112
|
+
fs.writeFileSync(dbPath, dbBuf);
|
|
113
|
+
const store = Store.openReadOnly(dbPath);
|
|
114
|
+
return {
|
|
115
|
+
store, manifest,
|
|
116
|
+
cleanup: () => {
|
|
117
|
+
try { store.close(); } catch { /* */ }
|
|
118
|
+
try { fs.unlinkSync(dbPath); fs.rmdirSync(tmpDir); } catch { /* */ }
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the "contract surface" — one entry per route — from a bundle Store.
|
|
125
|
+
* Stable key:
|
|
126
|
+
* - HTTP → `http|${method}|${path}`
|
|
127
|
+
* - tRPC → `trpc|${operation}`
|
|
128
|
+
* - GraphQL → `graphql|${operation}`
|
|
129
|
+
* - gRPC → `grpc|${service}.${operation}`
|
|
130
|
+
* - Messaging → `${protocol}|${topic|queue|exchange}`
|
|
131
|
+
*/
|
|
132
|
+
export function collectContractSurface(store: Store): Map<string, ContractEndpoint> {
|
|
133
|
+
const surface = new Map<string, ContractEndpoint>();
|
|
134
|
+
const rows = store.listRoutes({ limit: 1_000_000 });
|
|
135
|
+
for (const r of rows) {
|
|
136
|
+
const protocol = r.protocol ?? 'http';
|
|
137
|
+
const key = endpointKey(protocol, r);
|
|
138
|
+
if (key == null) continue;
|
|
139
|
+
surface.set(key, {
|
|
140
|
+
protocol,
|
|
141
|
+
key,
|
|
142
|
+
method: r.method ?? null,
|
|
143
|
+
path: r.path ?? null,
|
|
144
|
+
framework: r.framework ?? null,
|
|
145
|
+
handlerName: r.handlerName ?? null,
|
|
146
|
+
operation: r.operation ?? null,
|
|
147
|
+
topic: r.topic ?? null,
|
|
148
|
+
queue: r.queue ?? null,
|
|
149
|
+
exchange: r.exchange ?? null,
|
|
150
|
+
service: r.service ?? null,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return surface;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function endpointKey(protocol: string, r: {
|
|
157
|
+
method?: string | null; path?: string | null;
|
|
158
|
+
operation?: string | null; topic?: string | null;
|
|
159
|
+
queue?: string | null; exchange?: string | null;
|
|
160
|
+
service?: string | null;
|
|
161
|
+
}): string | null {
|
|
162
|
+
if (protocol === 'http') {
|
|
163
|
+
if (!r.path) return null;
|
|
164
|
+
return `http|${(r.method ?? 'ANY').toUpperCase()}|${r.path}`;
|
|
165
|
+
}
|
|
166
|
+
if (protocol === 'trpc') {
|
|
167
|
+
if (!r.operation) return null;
|
|
168
|
+
return `trpc|${r.operation}`;
|
|
169
|
+
}
|
|
170
|
+
if (protocol === 'graphql') {
|
|
171
|
+
if (!r.operation) return null;
|
|
172
|
+
return `graphql|${r.operation}`;
|
|
173
|
+
}
|
|
174
|
+
if (protocol === 'grpc') {
|
|
175
|
+
if (!r.operation) return null;
|
|
176
|
+
const svc = r.service ?? '';
|
|
177
|
+
return `grpc|${svc}.${r.operation}`;
|
|
178
|
+
}
|
|
179
|
+
if (protocol === 'kafka' || protocol === 'redis_pubsub' || protocol === 'nats' || protocol === 'sns') {
|
|
180
|
+
if (!r.topic) return null;
|
|
181
|
+
return `${protocol}|${r.topic}`;
|
|
182
|
+
}
|
|
183
|
+
if (protocol === 'sqs' || protocol === 'rabbitmq') {
|
|
184
|
+
const key = r.queue ?? r.exchange;
|
|
185
|
+
if (!key) return null;
|
|
186
|
+
return `${protocol}|${key}`;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Diff two bundles' contract surfaces. Both bundles are opened read-only,
|
|
193
|
+
* the surfaces are collected, and the diff is computed without importing
|
|
194
|
+
* either bundle into a workspace.
|
|
195
|
+
*/
|
|
196
|
+
export async function contractDiff(
|
|
197
|
+
oldBundlePath: string,
|
|
198
|
+
newBundlePath: string,
|
|
199
|
+
options: { includeAffectedCallers?: boolean } = {},
|
|
200
|
+
): Promise<ContractDiff> {
|
|
201
|
+
const oldH = await openBundleStore(oldBundlePath);
|
|
202
|
+
const newH = await openBundleStore(newBundlePath);
|
|
203
|
+
try {
|
|
204
|
+
const oldSurface = collectContractSurface(oldH.store);
|
|
205
|
+
const newSurface = collectContractSurface(newH.store);
|
|
206
|
+
|
|
207
|
+
const added: ContractEndpoint[] = [];
|
|
208
|
+
const removed: ContractEndpoint[] = [];
|
|
209
|
+
const changed: ContractChange[] = [];
|
|
210
|
+
|
|
211
|
+
for (const [key, before] of oldSurface) {
|
|
212
|
+
const after = newSurface.get(key);
|
|
213
|
+
if (!after) { removed.push(before); continue; }
|
|
214
|
+
const fields = diffEndpointFields(before, after);
|
|
215
|
+
if (fields.length > 0) {
|
|
216
|
+
changed.push({ protocol: before.protocol, key, before, after, changedFields: fields });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const [key, after] of newSurface) {
|
|
220
|
+
if (!oldSurface.has(key)) added.push(after);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
sortByProtocolKey(added);
|
|
224
|
+
sortByProtocolKey(removed);
|
|
225
|
+
changed.sort((a, b) =>
|
|
226
|
+
a.protocol < b.protocol ? -1 :
|
|
227
|
+
a.protocol > b.protocol ? 1 :
|
|
228
|
+
a.key < b.key ? -1 :
|
|
229
|
+
a.key > b.key ? 1 : 0);
|
|
230
|
+
|
|
231
|
+
let affectedCallersAvailable = false;
|
|
232
|
+
let augmented = false;
|
|
233
|
+
if (options.includeAffectedCallers) {
|
|
234
|
+
augmented = true;
|
|
235
|
+
const oldCallersByRouteKey = collectCallersByRouteKey(oldH.store);
|
|
236
|
+
const newCallersByRouteKey = collectCallersByRouteKey(newH.store);
|
|
237
|
+
|
|
238
|
+
const attach = (item: ContractEndpoint & { affectedCallers?: AffectedCaller[] },
|
|
239
|
+
side: 'old' | 'new'): void => {
|
|
240
|
+
const src = side === 'old' ? oldCallersByRouteKey : newCallersByRouteKey;
|
|
241
|
+
const callers = src.get(item.key);
|
|
242
|
+
if (callers && callers.length > 0) {
|
|
243
|
+
item.affectedCallers = callers.map(c => ({ ...c, source: side }));
|
|
244
|
+
affectedCallersAvailable = true;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
for (const item of added) attach(item as any, 'new');
|
|
248
|
+
for (const item of removed) attach(item as any, 'old');
|
|
249
|
+
for (const item of changed) {
|
|
250
|
+
const sideOld = oldCallersByRouteKey.get(item.key);
|
|
251
|
+
const sideNew = newCallersByRouteKey.get(item.key);
|
|
252
|
+
const callers: AffectedCaller[] = [];
|
|
253
|
+
if (sideOld) for (const c of sideOld) callers.push({ ...c, source: 'old' });
|
|
254
|
+
if (sideNew) for (const c of sideNew) callers.push({ ...c, source: 'new' });
|
|
255
|
+
if (callers.length > 0) {
|
|
256
|
+
(item as any).affectedCallers = callers;
|
|
257
|
+
affectedCallersAvailable = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
void augmented;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
oldBundle: {
|
|
265
|
+
path: path.resolve(oldBundlePath),
|
|
266
|
+
gitHead: oldH.manifest.source.gitHead,
|
|
267
|
+
rosterHash: oldH.manifest.source.rosterHash,
|
|
268
|
+
},
|
|
269
|
+
newBundle: {
|
|
270
|
+
path: path.resolve(newBundlePath),
|
|
271
|
+
gitHead: newH.manifest.source.gitHead,
|
|
272
|
+
rosterHash: newH.manifest.source.rosterHash,
|
|
273
|
+
},
|
|
274
|
+
totals: {
|
|
275
|
+
old: oldSurface.size,
|
|
276
|
+
new: newSurface.size,
|
|
277
|
+
added: added.length,
|
|
278
|
+
removed: removed.length,
|
|
279
|
+
changed: changed.length,
|
|
280
|
+
},
|
|
281
|
+
added,
|
|
282
|
+
removed,
|
|
283
|
+
changed,
|
|
284
|
+
affectedCallersAvailable,
|
|
285
|
+
};
|
|
286
|
+
} finally {
|
|
287
|
+
oldH.cleanup();
|
|
288
|
+
newH.cleanup();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function sortByProtocolKey(items: ContractEndpoint[]): void {
|
|
293
|
+
items.sort((a, b) =>
|
|
294
|
+
a.protocol < b.protocol ? -1 :
|
|
295
|
+
a.protocol > b.protocol ? 1 :
|
|
296
|
+
a.key < b.key ? -1 :
|
|
297
|
+
a.key > b.key ? 1 : 0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function diffEndpointFields(a: ContractEndpoint, b: ContractEndpoint): string[] {
|
|
301
|
+
const fields: string[] = [];
|
|
302
|
+
const keys: Array<keyof ContractEndpoint> = [
|
|
303
|
+
'method', 'path', 'framework', 'handlerName',
|
|
304
|
+
'operation', 'topic', 'queue', 'exchange', 'service',
|
|
305
|
+
];
|
|
306
|
+
for (const k of keys) {
|
|
307
|
+
if ((a as any)[k] !== (b as any)[k]) fields.push(k);
|
|
308
|
+
}
|
|
309
|
+
return fields;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* For each route in the bundle, build a list of caller previews using
|
|
314
|
+
* service_links + service_calls when present. Lets the diff report
|
|
315
|
+
* affectedCallers without importing the bundle.
|
|
316
|
+
*/
|
|
317
|
+
function collectCallersByRouteKey(store: Store): Map<string, AffectedCaller[]> {
|
|
318
|
+
const out = new Map<string, AffectedCaller[]>();
|
|
319
|
+
try {
|
|
320
|
+
const links = store.listServiceLinks({ limit: 1_000_000 });
|
|
321
|
+
const routesById = new Map<number, ContractEndpoint>();
|
|
322
|
+
for (const r of store.listRoutes({ limit: 1_000_000 })) {
|
|
323
|
+
const protocol = r.protocol ?? 'http';
|
|
324
|
+
const key = endpointKey(protocol, r);
|
|
325
|
+
if (!key) continue;
|
|
326
|
+
routesById.set(r.id, {
|
|
327
|
+
protocol, key,
|
|
328
|
+
method: r.method, path: r.path, framework: r.framework,
|
|
329
|
+
handlerName: r.handlerName,
|
|
330
|
+
operation: r.operation ?? null, topic: r.topic ?? null,
|
|
331
|
+
queue: r.queue ?? null, exchange: r.exchange ?? null,
|
|
332
|
+
service: r.service ?? null,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
for (const l of links) {
|
|
336
|
+
if (l.routeId == null) continue;
|
|
337
|
+
const ep = routesById.get(l.routeId);
|
|
338
|
+
if (!ep) continue;
|
|
339
|
+
const list = out.get(ep.key) ?? [];
|
|
340
|
+
list.push({
|
|
341
|
+
callerQualifiedName: l.callerQualifiedName ?? l.callerName ?? null,
|
|
342
|
+
callerFile: l.callerFile,
|
|
343
|
+
callerLine: l.callerLine ?? null,
|
|
344
|
+
source: 'old',
|
|
345
|
+
});
|
|
346
|
+
out.set(ep.key, list);
|
|
347
|
+
}
|
|
348
|
+
} catch { /* */ }
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Convenience: format the diff as a compact human-readable table.
|
|
354
|
+
* Deterministic ordering by protocol + key.
|
|
355
|
+
*/
|
|
356
|
+
export function formatContractDiffTable(diff: ContractDiff): string {
|
|
357
|
+
const lines: string[] = [];
|
|
358
|
+
lines.push(`Contract diff ${diff.oldBundle.path} → ${diff.newBundle.path}`);
|
|
359
|
+
lines.push(` old endpoints: ${diff.totals.old} new: ${diff.totals.new} ` +
|
|
360
|
+
`added: ${diff.totals.added} removed: ${diff.totals.removed} changed: ${diff.totals.changed}`);
|
|
361
|
+
if (diff.added.length > 0) {
|
|
362
|
+
lines.push('');
|
|
363
|
+
lines.push(' Added:');
|
|
364
|
+
for (const a of diff.added) lines.push(` + [${a.protocol}] ${labelEndpoint(a)}`);
|
|
365
|
+
}
|
|
366
|
+
if (diff.removed.length > 0) {
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push(' Removed:');
|
|
369
|
+
for (const r of diff.removed) lines.push(` - [${r.protocol}] ${labelEndpoint(r)}`);
|
|
370
|
+
}
|
|
371
|
+
if (diff.changed.length > 0) {
|
|
372
|
+
lines.push('');
|
|
373
|
+
lines.push(' Changed:');
|
|
374
|
+
for (const c of diff.changed) {
|
|
375
|
+
lines.push(` ~ [${c.protocol}] ${labelEndpoint(c.before)} (${c.changedFields.join(', ')})`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return lines.join('\n') + '\n';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function labelEndpoint(e: ContractEndpoint): string {
|
|
382
|
+
if (e.protocol === 'http') return `${e.method ?? 'ANY'} ${e.path ?? ''} (${e.framework ?? '?'})`;
|
|
383
|
+
if (e.protocol === 'trpc') return `${e.operation ?? ''}`;
|
|
384
|
+
if (e.protocol === 'graphql')return `${e.operation ?? ''}`;
|
|
385
|
+
if (e.protocol === 'grpc') return `${e.service ?? ''}.${e.operation ?? ''}`;
|
|
386
|
+
return e.topic ?? e.queue ?? e.exchange ?? '?';
|
|
387
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import zlib from 'zlib';
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
import { Store } from '../db/store.js';
|
|
8
|
+
import {
|
|
9
|
+
BUNDLE_MAGIC, BUNDLE_FORMAT_VERSION, BundleManifest,
|
|
10
|
+
} from './format.js';
|
|
11
|
+
|
|
12
|
+
export interface ExportOptions {
|
|
13
|
+
/** Where to write the bundle. Defaults to `<repoRoot>/.seer/index.seerbundle`. */
|
|
14
|
+
out?: string;
|
|
15
|
+
/** Gzip level (0-9). Defaults to 6 (zlib's standard balance). */
|
|
16
|
+
compressionLevel?: number;
|
|
17
|
+
/** Logger; defaults to no-op. */
|
|
18
|
+
log?: (msg: string) => void;
|
|
19
|
+
/** Override the gitHead recorded in the manifest (CI sometimes wants this). */
|
|
20
|
+
gitHead?: string;
|
|
21
|
+
/** Override the gitBranch recorded in the manifest. */
|
|
22
|
+
gitBranch?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Pin the manifest's `builtAt` (Unix-millis). Defaults to `Date.now()`,
|
|
25
|
+
* which makes successive exports of the same DB byte-different. Override
|
|
26
|
+
* to a stable value (e.g. the source repo's HEAD commit time) when you
|
|
27
|
+
* need reproducible bundle bytes for build-cache keys.
|
|
28
|
+
*/
|
|
29
|
+
builtAt?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ExportResult {
|
|
33
|
+
bundlePath: string;
|
|
34
|
+
bytes: number;
|
|
35
|
+
manifest: BundleManifest;
|
|
36
|
+
elapsedMs: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Bundle an existing `.seer/graph.db` into a portable single-file artifact.
|
|
41
|
+
*
|
|
42
|
+
* The DB is first VACUUM INTO'd to a temp file so we ship a tightly packed
|
|
43
|
+
* copy (no WAL, no free pages). The packed DB is hashed, then gzip-compressed
|
|
44
|
+
* and concatenated after the manifest header. The result is deterministic for
|
|
45
|
+
* a given DB content + manifest input.
|
|
46
|
+
*/
|
|
47
|
+
export async function exportBundle(
|
|
48
|
+
dbPath: string, repoRoot: string, options: ExportOptions = {},
|
|
49
|
+
): Promise<ExportResult> {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
const log = options.log ?? (() => { /* */ });
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(dbPath)) {
|
|
54
|
+
throw new Error(`No index at ${dbPath} — run \`seer index\` first.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Open the source DB read-only and harvest the manifest data BEFORE we
|
|
58
|
+
// start the vacuum (vacuum closes implicit locks on its own connection but
|
|
59
|
+
// the source connection stays compatible since we re-open it here).
|
|
60
|
+
const srcStore = Store.openReadOnly(dbPath);
|
|
61
|
+
let manifest: BundleManifest;
|
|
62
|
+
try {
|
|
63
|
+
const stats = srcStore.getStats();
|
|
64
|
+
const scipImports = srcStore.listScipImports();
|
|
65
|
+
const files = srcStore.listFiles();
|
|
66
|
+
const roster = files
|
|
67
|
+
.map(f => ({ relPath: f.relPath, hash: f.hash }))
|
|
68
|
+
.sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0);
|
|
69
|
+
const rosterHash = crypto.createHash('sha256')
|
|
70
|
+
.update(roster.map(r => `${r.relPath}\t${r.hash}`).join('\n'))
|
|
71
|
+
.digest('hex');
|
|
72
|
+
const schemaVersion = srcStore.schemaInfo().dbVersion;
|
|
73
|
+
|
|
74
|
+
manifest = {
|
|
75
|
+
bundleFormatVersion: BUNDLE_FORMAT_VERSION,
|
|
76
|
+
schemaVersion,
|
|
77
|
+
builtAt: options.builtAt ?? Date.now(),
|
|
78
|
+
builtBy: `seer/${BUNDLE_FORMAT_VERSION}`,
|
|
79
|
+
source: {
|
|
80
|
+
repoRoot: path.resolve(repoRoot),
|
|
81
|
+
gitHead: options.gitHead ?? detectGitHead(repoRoot),
|
|
82
|
+
gitBranch: options.gitBranch ?? detectGitBranch(repoRoot),
|
|
83
|
+
rosterHash,
|
|
84
|
+
fileCount: files.length,
|
|
85
|
+
},
|
|
86
|
+
index: {
|
|
87
|
+
symbols: stats.symbols,
|
|
88
|
+
edges: stats.edges,
|
|
89
|
+
resolvedEdges: stats.resolvedEdges,
|
|
90
|
+
modules: stats.modules ?? 0,
|
|
91
|
+
routes: stats.routes ?? 0,
|
|
92
|
+
externalDependencies: stats.externalDependencies ?? 0,
|
|
93
|
+
configKeys: stats.configKeys ?? 0,
|
|
94
|
+
languages: stats.languages,
|
|
95
|
+
provenance: stats.provenance,
|
|
96
|
+
},
|
|
97
|
+
scipImports: scipImports.map(s => ({
|
|
98
|
+
path: s.path, sha256: s.sha256, tool: s.tool,
|
|
99
|
+
symbolCount: s.symbolCount, refCount: s.refCount,
|
|
100
|
+
})),
|
|
101
|
+
dbSha256: '', // filled in after we hash the packed DB
|
|
102
|
+
dbBytes: 0,
|
|
103
|
+
};
|
|
104
|
+
} finally {
|
|
105
|
+
srcStore.close();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// VACUUM INTO produces a fresh, tightly packed DB — fast on small repos,
|
|
109
|
+
// O(rows) on huge ones. We write to a temp file so the source DB stays
|
|
110
|
+
// untouched and aborted exports leave nothing behind.
|
|
111
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'seer-bundle-'));
|
|
112
|
+
const packedDb = path.join(tmpDir, 'graph.packed.db');
|
|
113
|
+
log(`Vacuuming index into ${packedDb}...`);
|
|
114
|
+
const packStore = new Store(dbPath);
|
|
115
|
+
try {
|
|
116
|
+
packStore.rawDb().exec(`VACUUM INTO '${packedDb.replace(/'/g, "''")}'`);
|
|
117
|
+
} finally {
|
|
118
|
+
packStore.close();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dbBuf = fs.readFileSync(packedDb);
|
|
122
|
+
const dbSha = crypto.createHash('sha256').update(dbBuf).digest('hex');
|
|
123
|
+
manifest.dbSha256 = dbSha;
|
|
124
|
+
manifest.dbBytes = dbBuf.length;
|
|
125
|
+
log(`Packed DB: ${dbBuf.length} bytes, sha256 ${dbSha.slice(0, 12)}...`);
|
|
126
|
+
|
|
127
|
+
const manifestJson = Buffer.from(JSON.stringify(manifest, null, 2), 'utf-8');
|
|
128
|
+
// Gzip the DB. Level 6 is the zlib default; bumpable for CI artifacts.
|
|
129
|
+
const compressed = zlib.gzipSync(dbBuf, {
|
|
130
|
+
level: options.compressionLevel ?? 6,
|
|
131
|
+
});
|
|
132
|
+
log(`Compressed DB: ${compressed.length} bytes (${(compressed.length / dbBuf.length * 100).toFixed(1)}% of raw)`);
|
|
133
|
+
|
|
134
|
+
const header = Buffer.alloc(4 + 4 + 4);
|
|
135
|
+
BUNDLE_MAGIC.copy(header, 0);
|
|
136
|
+
header.writeUInt32BE(BUNDLE_FORMAT_VERSION, 4);
|
|
137
|
+
header.writeUInt32BE(manifestJson.length, 8);
|
|
138
|
+
|
|
139
|
+
const outPath = options.out ?? path.join(repoRoot, '.seer', 'index.seerbundle');
|
|
140
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
141
|
+
const fd = fs.openSync(outPath, 'w');
|
|
142
|
+
try {
|
|
143
|
+
fs.writeSync(fd, header);
|
|
144
|
+
fs.writeSync(fd, manifestJson);
|
|
145
|
+
fs.writeSync(fd, compressed);
|
|
146
|
+
} finally {
|
|
147
|
+
fs.closeSync(fd);
|
|
148
|
+
}
|
|
149
|
+
try { fs.unlinkSync(packedDb); fs.rmdirSync(tmpDir); } catch { /* */ }
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
bundlePath: outPath,
|
|
153
|
+
bytes: header.length + manifestJson.length + compressed.length,
|
|
154
|
+
manifest,
|
|
155
|
+
elapsedMs: Date.now() - start,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectGitHead(repoRoot: string): string | null {
|
|
160
|
+
try {
|
|
161
|
+
const sha = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], {
|
|
162
|
+
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
163
|
+
}).trim();
|
|
164
|
+
return sha.length > 0 ? sha : null;
|
|
165
|
+
} catch { return null; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function detectGitBranch(repoRoot: string): string | null {
|
|
169
|
+
try {
|
|
170
|
+
const branch = execFileSync('git', ['-C', repoRoot, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
171
|
+
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
172
|
+
}).trim();
|
|
173
|
+
return branch.length > 0 && branch !== 'HEAD' ? branch : null;
|
|
174
|
+
} catch { return null; }
|
|
175
|
+
}
|