@danypops/lector 0.1.6 → 0.1.8

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.
Files changed (35) hide show
  1. package/README.md +9 -7
  2. package/package.json +5 -1
  3. package/src/adapters/fallback-code-intelligence-index.ts +73 -0
  4. package/src/adapters/find-source-files.ts +1 -1
  5. package/src/adapters/git-repo-fetcher.ts +154 -37
  6. package/src/adapters/lsp/json-rpc-stream.ts +52 -2
  7. package/src/adapters/lsp/language-server-process.ts +54 -10
  8. package/src/adapters/lsp/lsp-symbol-index.ts +133 -32
  9. package/src/adapters/normalize-npm-repository.ts +77 -0
  10. package/src/adapters/npm-lockfile-version-resolver.ts +519 -0
  11. package/src/adapters/npm-package-source-resolver.ts +322 -0
  12. package/src/adapters/npm-registry-client.ts +304 -0
  13. package/src/adapters/sqlite-symbol-graph.ts +47 -3
  14. package/src/adapters/tree-sitter/typescript-tree-sitter-symbol-index.ts +61 -11
  15. package/src/adapters/typescript-compiler-symbol-index.ts +129 -0
  16. package/src/cli.ts +87 -23
  17. package/src/daemon.ts +4 -0
  18. package/src/domain/find-workspace-symbols.ts +4 -3
  19. package/src/domain/installed-package-version.ts +66 -0
  20. package/src/domain/intelligence-provenance.ts +19 -0
  21. package/src/domain/language-server-descriptor.ts +18 -0
  22. package/src/domain/npm-package-metadata.ts +26 -0
  23. package/src/domain/package-source.ts +143 -0
  24. package/src/domain/repo-fetch-result.ts +33 -1
  25. package/src/domain/resolve-package-source.ts +204 -0
  26. package/src/domain/symbol-graph-generation.ts +3 -0
  27. package/src/domain/symbol-query.ts +16 -0
  28. package/src/domain/workspace-symbol.ts +8 -0
  29. package/src/index.ts +61 -4
  30. package/src/ports/installed-package-version-resolver-port.ts +5 -0
  31. package/src/ports/npm-registry-port.ts +5 -0
  32. package/src/ports/package-source-resolver-port.ts +5 -0
  33. package/src/ports/repo-fetcher-port.ts +2 -2
  34. package/src/ports/symbol-index-port.ts +4 -2
  35. package/src/service.ts +89 -25
@@ -0,0 +1,66 @@
1
+ export type JavaScriptPackageManager = "npm" | "pnpm" | "yarn" | "bun";
2
+
3
+ export interface InstalledPackageVersionRequest {
4
+ readonly projectRoot: string;
5
+ readonly packageName: string;
6
+ readonly requestedVersion: string | null;
7
+ }
8
+
9
+ export interface InstalledPackageVersionBounds {
10
+ readonly maxManifestBytes: number;
11
+ readonly maxManifestEntries: number;
12
+ readonly maxManifestNesting: number;
13
+ readonly maxWorkspaces: number;
14
+ readonly maxDiagnostics: number;
15
+ readonly maxCandidates: number;
16
+ readonly maxEvidencePerVersion: number;
17
+ }
18
+
19
+ export interface InstalledPackageEvidence {
20
+ readonly manager: JavaScriptPackageManager;
21
+ readonly lockfile: string;
22
+ readonly locator: string;
23
+ readonly integrity: string | null;
24
+ readonly workspace: boolean;
25
+ }
26
+
27
+ export interface ResolvedInstalledPackageVersion {
28
+ readonly status: "resolved";
29
+ readonly packageName: string;
30
+ readonly requestedVersion: string | null;
31
+ readonly version: string;
32
+ readonly evidence: readonly InstalledPackageEvidence[];
33
+ readonly evidenceTruncated: boolean;
34
+ }
35
+
36
+ export interface InstalledPackageVersionCandidate {
37
+ readonly version: string;
38
+ readonly evidence: readonly InstalledPackageEvidence[];
39
+ readonly evidenceTruncated: boolean;
40
+ }
41
+
42
+ export interface AmbiguousInstalledPackageVersion {
43
+ readonly status: "ambiguous";
44
+ readonly packageName: string;
45
+ readonly requestedVersion: null;
46
+ readonly candidates: readonly InstalledPackageVersionCandidate[];
47
+ readonly truncated: boolean;
48
+ }
49
+
50
+ export interface UnavailableInstalledPackageVersion {
51
+ readonly status: "unavailable";
52
+ readonly code: "lockfile-not-found" | "package-not-found" | "version-not-found" | "unsupported-lockfile" | "corrupt-lockfile";
53
+ readonly lockfile?: string;
54
+ }
55
+
56
+ export interface OversizedInstalledPackageVersion {
57
+ readonly status: "oversized";
58
+ readonly resource: "manifest-bytes" | "manifest-entries" | "manifest-nesting" | "workspaces" | "diagnostics";
59
+ readonly limit: number;
60
+ }
61
+
62
+ export type InstalledPackageVersionOutcome =
63
+ | ResolvedInstalledPackageVersion
64
+ | AmbiguousInstalledPackageVersion
65
+ | UnavailableInstalledPackageVersion
66
+ | OversizedInstalledPackageVersion;
@@ -0,0 +1,19 @@
1
+ export type IntelligenceFidelity = "semantic" | "structural";
2
+
3
+ export interface IntelligenceProvenance {
4
+ readonly fidelity: IntelligenceFidelity;
5
+ readonly backend: string;
6
+ readonly languageId: string;
7
+ readonly authority: "language-server" | "parser" | "compiler";
8
+ readonly freshness: "live-process" | "content-hash" | "filesystem-snapshot";
9
+ readonly limitations: readonly string[];
10
+ }
11
+
12
+ export interface SymbolSearchBounds {
13
+ readonly maxResults: number;
14
+ }
15
+
16
+ export interface ProvenancedResult<T> {
17
+ readonly result: T;
18
+ readonly provenance: IntelligenceProvenance;
19
+ }
@@ -13,7 +13,10 @@ export type LanguageServerLaunch = { readonly kind: "npm-module"; readonly entry
13
13
 
14
14
  export interface LanguageServerDescriptor {
15
15
  readonly languageId: string;
16
+ readonly backendId: string;
16
17
  readonly extensions: readonly string[];
18
+ /** Per-extension document language ids when one server owns a language family. */
19
+ readonly documentLanguageIds?: Readonly<Record<string, string>>;
17
20
  readonly launch: LanguageServerLaunch;
18
21
  readonly args: readonly string[];
19
22
  /** Checked nearest-first; closest match wins over a more distant one -- a monorepo subproject with its own root marker resolves to itself, not the outer repo. */
@@ -30,7 +33,16 @@ export const DEFAULT_SETTLE_MS = 1000;
30
33
 
31
34
  export const TYPESCRIPT_DESCRIPTOR: LanguageServerDescriptor = {
32
35
  languageId: "typescript",
36
+ backendId: "typescript-language-server",
33
37
  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
38
+ documentLanguageIds: {
39
+ ".ts": "typescript",
40
+ ".tsx": "typescriptreact",
41
+ ".js": "javascript",
42
+ ".jsx": "javascriptreact",
43
+ ".mjs": "javascript",
44
+ ".cjs": "javascript",
45
+ },
34
46
  launch: { kind: "npm-module", entryModule: "typescript-language-server/lib/cli.mjs" },
35
47
  args: ["--stdio"],
36
48
  rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
@@ -43,6 +55,7 @@ export const TYPESCRIPT_DESCRIPTOR: LanguageServerDescriptor = {
43
55
 
44
56
  export const PYTHON_DESCRIPTOR: LanguageServerDescriptor = {
45
57
  languageId: "python",
58
+ backendId: "pyright",
46
59
  extensions: [".py", ".pyi"],
47
60
  launch: { kind: "npm-module", entryModule: "pyright/langserver.index.js" },
48
61
  args: ["--stdio"],
@@ -52,6 +65,7 @@ export const PYTHON_DESCRIPTOR: LanguageServerDescriptor = {
52
65
 
53
66
  export const GO_DESCRIPTOR: LanguageServerDescriptor = {
54
67
  languageId: "go",
68
+ backendId: "gopls",
55
69
  extensions: [".go"],
56
70
  // gopls ships via `go install`, not npm.
57
71
  launch: { kind: "system-binary", command: "gopls" },
@@ -62,6 +76,7 @@ export const GO_DESCRIPTOR: LanguageServerDescriptor = {
62
76
 
63
77
  export const RUST_DESCRIPTOR: LanguageServerDescriptor = {
64
78
  languageId: "rust",
79
+ backendId: "rust-analyzer",
65
80
  extensions: [".rs"],
66
81
  // rust-analyzer ships via rustup, not npm.
67
82
  launch: { kind: "system-binary", command: "rust-analyzer" },
@@ -73,6 +88,7 @@ export const RUST_DESCRIPTOR: LanguageServerDescriptor = {
73
88
 
74
89
  export const CPP_DESCRIPTOR: LanguageServerDescriptor = {
75
90
  languageId: "cpp",
91
+ backendId: "clangd",
76
92
  extensions: [".c", ".h", ".cc", ".cpp", ".cxx", ".hh", ".hpp", ".hxx"],
77
93
  // clangd ships via LLVM's system packaging, not npm.
78
94
  launch: { kind: "system-binary", command: "clangd" },
@@ -83,6 +99,7 @@ export const CPP_DESCRIPTOR: LanguageServerDescriptor = {
83
99
 
84
100
  export const BASH_DESCRIPTOR: LanguageServerDescriptor = {
85
101
  languageId: "shellscript",
102
+ backendId: "bash-language-server",
86
103
  extensions: [".sh", ".bash"],
87
104
  launch: { kind: "npm-module", entryModule: "bash-language-server/out/cli.js" },
88
105
  args: ["start"],
@@ -92,6 +109,7 @@ export const BASH_DESCRIPTOR: LanguageServerDescriptor = {
92
109
 
93
110
  export const YAML_DESCRIPTOR: LanguageServerDescriptor = {
94
111
  languageId: "yaml",
112
+ backendId: "yaml-language-server",
95
113
  extensions: [".yaml", ".yml"],
96
114
  launch: { kind: "npm-module", entryModule: "yaml-language-server/bin/yaml-language-server" },
97
115
  args: ["--stdio"],
@@ -0,0 +1,26 @@
1
+ export interface NpmRegistryVersionRequest {
2
+ readonly registry: string;
3
+ readonly name: string;
4
+ readonly version: string;
5
+ }
6
+
7
+ export interface NpmRegistryBounds {
8
+ readonly maxResponseBytes: number;
9
+ readonly maxRedirects: number;
10
+ readonly maxRetries: number;
11
+ readonly timeoutMs: number;
12
+ }
13
+
14
+ export interface NpmRepositoryMetadata {
15
+ readonly type: string | null;
16
+ readonly url: string;
17
+ readonly directory: string | null;
18
+ }
19
+
20
+ export interface NpmPackageVersionMetadata {
21
+ readonly name: string;
22
+ readonly version: string;
23
+ readonly repository: NpmRepositoryMetadata | null;
24
+ readonly gitHead: string | null;
25
+ readonly integrity: string | null;
26
+ }
@@ -0,0 +1,143 @@
1
+ export type PackageEcosystem = "npm" | "pypi" | "cargo" | "go" | "maven" | "conan" | "vcpkg" | "nuget" | "swiftpm";
2
+
3
+ export interface PackageCoordinateRequest {
4
+ readonly ecosystem: PackageEcosystem;
5
+ readonly registry: string | null;
6
+ readonly name: string;
7
+ readonly requestedVersion: string | null;
8
+ }
9
+
10
+ export interface PackageSourceRequest {
11
+ readonly projectRoot: string;
12
+ readonly coordinate: PackageCoordinateRequest;
13
+ }
14
+
15
+ export interface PackageSourceBounds {
16
+ readonly maxManifestBytes: number;
17
+ readonly maxManifestEntries: number;
18
+ readonly maxManifestNesting: number;
19
+ readonly maxWorkspaces: number;
20
+ readonly maxDiagnostics: number;
21
+ readonly maxRegistryResponseBytes: number;
22
+ readonly maxRedirects: number;
23
+ readonly maxRetries: number;
24
+ readonly maxCloneBytes: number;
25
+ readonly maxCacheBytes: number;
26
+ readonly maxCandidates: number;
27
+ readonly timeoutMs: number;
28
+ }
29
+
30
+ export interface ResolvedPackageCoordinate extends PackageCoordinateRequest {
31
+ readonly resolvedVersion: string;
32
+ }
33
+
34
+ export interface PackageRepositoryIdentity {
35
+ readonly url: string | null;
36
+ readonly requestedRef: string | null;
37
+ readonly resolvedRef: string | null;
38
+ readonly commit: string | null;
39
+ }
40
+
41
+ export interface PackageSourceWorkspace {
42
+ readonly cachePath: string;
43
+ readonly origin: "local" | "fetched";
44
+ readonly readOnly: true;
45
+ }
46
+
47
+ export type PackageSourceVerificationMethod = "lockfile-vcs-pin" | "registry-metadata-and-commit" | "source-artifact-checksum" | "local-content-digest";
48
+
49
+ export interface PackageSourceVerification {
50
+ readonly status: "verified";
51
+ readonly method: PackageSourceVerificationMethod;
52
+ readonly integrity: string;
53
+ }
54
+
55
+ export interface VerifiedPackageSource {
56
+ readonly status: "verified";
57
+ readonly coordinate: ResolvedPackageCoordinate;
58
+ readonly repository: PackageRepositoryIdentity;
59
+ readonly workspace: PackageSourceWorkspace;
60
+ readonly verification: PackageSourceVerification;
61
+ }
62
+
63
+ export type PackageSourceUnavailableCode =
64
+ | "package-not-found"
65
+ | "version-not-found"
66
+ | "source-metadata-missing"
67
+ | "unsupported-ecosystem"
68
+ | "unsupported-manifest"
69
+ | "unverifiable-source";
70
+
71
+ export interface UnavailablePackageSource {
72
+ readonly status: "unavailable";
73
+ readonly code: PackageSourceUnavailableCode;
74
+ }
75
+
76
+ export interface PackageSourceCandidate {
77
+ readonly version: string;
78
+ readonly source: string;
79
+ }
80
+
81
+ export interface AmbiguousPackageSource {
82
+ readonly status: "ambiguous";
83
+ readonly code: "multiple-installed-versions" | "multiple-source-candidates";
84
+ readonly candidates: readonly PackageSourceCandidate[];
85
+ readonly truncated: boolean;
86
+ }
87
+
88
+ export interface UnauthenticatedPackageSource {
89
+ readonly status: "unauthenticated";
90
+ readonly code: "registry-authentication-required" | "repository-authentication-required";
91
+ readonly requiredCredentialNames: readonly string[];
92
+ }
93
+
94
+ export interface OversizedPackageSource {
95
+ readonly status: "oversized";
96
+ readonly code: "manifest-limit-exceeded" | "registry-response-limit-exceeded" | "clone-limit-exceeded" | "cache-limit-exceeded";
97
+ readonly resource:
98
+ | "manifest-bytes"
99
+ | "manifest-entries"
100
+ | "manifest-nesting"
101
+ | "workspaces"
102
+ | "diagnostics"
103
+ | "registry-response-bytes"
104
+ | "clone-bytes"
105
+ | "cache-bytes";
106
+ readonly limit: number;
107
+ readonly observed: number | null;
108
+ }
109
+
110
+ export interface MismatchedPackageSource {
111
+ readonly status: "mismatched";
112
+ readonly code: "coordinate-mismatch" | "repository-ref-mismatch" | "repository-commit-mismatch" | "integrity-mismatch";
113
+ readonly expected: string;
114
+ readonly actual: string;
115
+ }
116
+
117
+ export type PackageSourceOutcome =
118
+ | VerifiedPackageSource
119
+ | UnavailablePackageSource
120
+ | AmbiguousPackageSource
121
+ | UnauthenticatedPackageSource
122
+ | OversizedPackageSource
123
+ | MismatchedPackageSource;
124
+
125
+ export interface PackageSourceOperationResult {
126
+ readonly outcome: PackageSourceOutcome;
127
+ readonly workspaceId: string | null;
128
+ }
129
+
130
+ export const DEFAULT_PACKAGE_SOURCE_BOUNDS: PackageSourceBounds = {
131
+ maxManifestBytes: 16 * 1024 * 1024,
132
+ maxManifestEntries: 100_000,
133
+ maxManifestNesting: 128,
134
+ maxWorkspaces: 10_000,
135
+ maxDiagnostics: 100,
136
+ maxRegistryResponseBytes: 8 * 1024 * 1024,
137
+ maxRedirects: 5,
138
+ maxRetries: 2,
139
+ maxCloneBytes: 512 * 1024 * 1024,
140
+ maxCacheBytes: 5 * 1024 * 1024 * 1024,
141
+ maxCandidates: 20,
142
+ timeoutMs: 60_000,
143
+ };
@@ -5,9 +5,41 @@ export interface RepoFetchResult {
5
5
  /** The ref actually checked out -- may differ from the requested ref if it didn't exist and cloning fell back to the default branch. */
6
6
  readonly resolvedRef: string;
7
7
  readonly refFallbackOccurred: boolean;
8
+ readonly commit: string;
8
9
  }
9
10
 
10
- /** Raised when neither the requested ref nor a default-branch fallback could be cloned. */
11
+ export interface RepoFetchPolicy {
12
+ readonly exactRef?: boolean;
13
+ readonly maxCloneBytes?: number;
14
+ readonly maxCacheBytes?: number;
15
+ readonly timeoutMs?: number;
16
+ }
17
+
18
+ export class RepoFetchCapacityExceeded extends Error {
19
+ readonly maxQueued: number;
20
+
21
+ constructor(maxQueued: number) {
22
+ super(`repository fetch queue is full (${maxQueued} queued)`);
23
+ this.name = "RepoFetchCapacityExceeded";
24
+ this.maxQueued = maxQueued;
25
+ }
26
+ }
27
+
28
+ export class RepoFetchLimitExceeded extends Error {
29
+ readonly resource: "clone-bytes" | "cache-bytes";
30
+ readonly limit: number;
31
+ readonly observed: number;
32
+
33
+ constructor(resource: RepoFetchLimitExceeded["resource"], limit: number, observed: number) {
34
+ super(`repository ${resource} ${observed} exceeded limit ${limit}`);
35
+ this.name = "RepoFetchLimitExceeded";
36
+ this.resource = resource;
37
+ this.limit = limit;
38
+ this.observed = observed;
39
+ }
40
+ }
41
+
42
+ /** Raised when the requested ref cannot be cloned under the caller's fallback policy. */
11
43
  export class RepoFetchFailed extends Error {
12
44
  constructor(host: string, owner: string, repo: string, ref: string | null, cause: unknown) {
13
45
  const target = `${host}/${owner}/${repo}${ref ? `@${ref}` : ""}`;
@@ -0,0 +1,204 @@
1
+ import type { PackageSourceResolverPort } from "../ports/package-source-resolver-port.ts";
2
+ import type {
3
+ AmbiguousPackageSource,
4
+ MismatchedPackageSource,
5
+ OversizedPackageSource,
6
+ PackageSourceBounds,
7
+ PackageSourceOutcome,
8
+ PackageSourceRequest,
9
+ UnauthenticatedPackageSource,
10
+ VerifiedPackageSource,
11
+ } from "./package-source.ts";
12
+
13
+ const COMMIT_HASH = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
14
+ const CREDENTIAL_NAME = /^[A-Z][A-Z0-9_]{0,127}$/;
15
+ const ECOSYSTEMS = ["npm", "pypi", "cargo", "go", "maven", "conan", "vcpkg", "nuget", "swiftpm"] as const;
16
+ const VERIFICATION_METHODS = ["lockfile-vcs-pin", "registry-metadata-and-commit", "source-artifact-checksum", "local-content-digest"] as const;
17
+ const UNAVAILABLE_CODES = [
18
+ "package-not-found",
19
+ "version-not-found",
20
+ "source-metadata-missing",
21
+ "unsupported-ecosystem",
22
+ "unsupported-manifest",
23
+ "unverifiable-source",
24
+ ] as const;
25
+ const AMBIGUOUS_CODES = ["multiple-installed-versions", "multiple-source-candidates"] as const;
26
+ const UNAUTHENTICATED_CODES = ["registry-authentication-required", "repository-authentication-required"] as const;
27
+ const OVERSIZED_CODES = ["manifest-limit-exceeded", "registry-response-limit-exceeded", "clone-limit-exceeded", "cache-limit-exceeded"] as const;
28
+ const OVERSIZED_RESOURCES = [
29
+ "manifest-bytes",
30
+ "manifest-entries",
31
+ "manifest-nesting",
32
+ "workspaces",
33
+ "diagnostics",
34
+ "registry-response-bytes",
35
+ "clone-bytes",
36
+ "cache-bytes",
37
+ ] as const;
38
+ const MISMATCHED_CODES = ["coordinate-mismatch", "repository-ref-mismatch", "repository-commit-mismatch", "integrity-mismatch"] as const;
39
+
40
+ export class InvalidPackageSourceContract extends Error {
41
+ constructor(reason: string) {
42
+ super(`invalid package-source contract: ${reason}`);
43
+ this.name = "InvalidPackageSourceContract";
44
+ }
45
+ }
46
+
47
+ function containsControlCharacter(value: string): boolean {
48
+ for (let index = 0; index < value.length; index++) {
49
+ const code = value.charCodeAt(index);
50
+ if (code <= 31 || code === 127) return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function assertText(value: string, name: string, maxLength: number): void {
56
+ if (value.length === 0 || value.length > maxLength || containsControlCharacter(value)) throw new InvalidPackageSourceContract(name);
57
+ }
58
+
59
+ function assertOptionalText(value: string | null, name: string, maxLength: number): void {
60
+ if (value !== null) assertText(value, name, maxLength);
61
+ }
62
+
63
+ function assertAllowed(value: string, allowed: readonly string[], name: string): void {
64
+ if (!allowed.includes(value)) throw new InvalidPackageSourceContract(name);
65
+ }
66
+
67
+ function assertPositiveSafeInteger(value: number, name: string): void {
68
+ if (!Number.isSafeInteger(value) || value < 1) throw new InvalidPackageSourceContract(name);
69
+ }
70
+
71
+ function assertNonNegativeSafeInteger(value: number, name: string): void {
72
+ if (!Number.isSafeInteger(value) || value < 0) throw new InvalidPackageSourceContract(name);
73
+ }
74
+
75
+ function validateRequest(request: PackageSourceRequest): void {
76
+ assertText(request.projectRoot, "projectRoot", 4096);
77
+ assertAllowed(request.coordinate.ecosystem, ECOSYSTEMS, "coordinate.ecosystem");
78
+ assertText(request.coordinate.name, "coordinate.name", 512);
79
+ assertOptionalText(request.coordinate.registry, "coordinate.registry", 2048);
80
+ assertOptionalText(request.coordinate.requestedVersion, "coordinate.requestedVersion", 256);
81
+ }
82
+
83
+ function validateBounds(bounds: PackageSourceBounds): void {
84
+ assertPositiveSafeInteger(bounds.maxManifestBytes, "maxManifestBytes");
85
+ assertPositiveSafeInteger(bounds.maxManifestEntries, "maxManifestEntries");
86
+ assertPositiveSafeInteger(bounds.maxManifestNesting, "maxManifestNesting");
87
+ assertPositiveSafeInteger(bounds.maxWorkspaces, "maxWorkspaces");
88
+ assertPositiveSafeInteger(bounds.maxDiagnostics, "maxDiagnostics");
89
+ assertPositiveSafeInteger(bounds.maxRegistryResponseBytes, "maxRegistryResponseBytes");
90
+ assertNonNegativeSafeInteger(bounds.maxRedirects, "maxRedirects");
91
+ assertNonNegativeSafeInteger(bounds.maxRetries, "maxRetries");
92
+ assertPositiveSafeInteger(bounds.maxCloneBytes, "maxCloneBytes");
93
+ assertPositiveSafeInteger(bounds.maxCacheBytes, "maxCacheBytes");
94
+ assertPositiveSafeInteger(bounds.maxCandidates, "maxCandidates");
95
+ assertPositiveSafeInteger(bounds.timeoutMs, "timeoutMs");
96
+ }
97
+
98
+ function validateVerified(outcome: VerifiedPackageSource, request: PackageSourceRequest): void {
99
+ if (outcome.coordinate.ecosystem !== request.coordinate.ecosystem || outcome.coordinate.name !== request.coordinate.name) {
100
+ throw new InvalidPackageSourceContract("resolved coordinate differs from request");
101
+ }
102
+ if (outcome.coordinate.requestedVersion !== request.coordinate.requestedVersion || outcome.coordinate.registry !== request.coordinate.registry) {
103
+ throw new InvalidPackageSourceContract("resolved request identity differs from request");
104
+ }
105
+ assertText(outcome.coordinate.resolvedVersion, "coordinate.resolvedVersion", 256);
106
+ assertOptionalText(outcome.coordinate.registry, "coordinate.registry", 2048);
107
+ assertOptionalText(outcome.repository.url, "repository.url", 4096);
108
+ assertOptionalText(outcome.repository.requestedRef, "repository.requestedRef", 512);
109
+ assertOptionalText(outcome.repository.resolvedRef, "repository.resolvedRef", 512);
110
+ if (outcome.repository.requestedRef !== null && outcome.repository.resolvedRef !== outcome.repository.requestedRef) {
111
+ throw new InvalidPackageSourceContract("verified source cannot fall back from requestedRef");
112
+ }
113
+ assertAllowed(outcome.workspace.origin, ["local", "fetched"], "workspace.origin");
114
+ if (outcome.workspace.origin === "fetched") {
115
+ if (outcome.repository.commit === null || !COMMIT_HASH.test(outcome.repository.commit)) {
116
+ throw new InvalidPackageSourceContract("fetched source requires an exact commit");
117
+ }
118
+ if (outcome.repository.url === null || outcome.repository.resolvedRef === null) {
119
+ throw new InvalidPackageSourceContract("fetched source requires repository identity");
120
+ }
121
+ } else if (outcome.repository.commit !== null && !COMMIT_HASH.test(outcome.repository.commit)) {
122
+ throw new InvalidPackageSourceContract("repository.commit");
123
+ }
124
+ if (outcome.workspace.readOnly !== true) throw new InvalidPackageSourceContract("workspace must be read-only");
125
+ assertText(outcome.workspace.cachePath, "workspace.cachePath", 4096);
126
+ if (outcome.verification.status !== "verified") throw new InvalidPackageSourceContract("verification.status");
127
+ assertAllowed(outcome.verification.method, VERIFICATION_METHODS, "verification.method");
128
+ assertText(outcome.verification.integrity, "verification.integrity", 1024);
129
+ }
130
+
131
+ function validateAmbiguous(outcome: AmbiguousPackageSource, bounds: PackageSourceBounds): void {
132
+ assertAllowed(outcome.code, AMBIGUOUS_CODES, "ambiguous.code");
133
+ if (outcome.candidates.length === 0 || outcome.candidates.length > bounds.maxCandidates) {
134
+ throw new InvalidPackageSourceContract("ambiguous candidates exceed bounds");
135
+ }
136
+ for (const candidate of outcome.candidates) {
137
+ assertText(candidate.version, "candidate.version", 256);
138
+ assertText(candidate.source, "candidate.source", 4096);
139
+ }
140
+ }
141
+
142
+ function validateUnauthenticated(outcome: UnauthenticatedPackageSource, bounds: PackageSourceBounds): void {
143
+ assertAllowed(outcome.code, UNAUTHENTICATED_CODES, "unauthenticated.code");
144
+ if (outcome.requiredCredentialNames.length === 0 || outcome.requiredCredentialNames.length > bounds.maxCandidates) {
145
+ throw new InvalidPackageSourceContract("requiredCredentialNames exceed bounds");
146
+ }
147
+ for (const name of outcome.requiredCredentialNames) {
148
+ if (!CREDENTIAL_NAME.test(name)) throw new InvalidPackageSourceContract("credential names must be environment-variable names");
149
+ }
150
+ }
151
+
152
+ function validateOversized(outcome: OversizedPackageSource): void {
153
+ assertAllowed(outcome.code, OVERSIZED_CODES, "oversized.code");
154
+ assertAllowed(outcome.resource, OVERSIZED_RESOURCES, "oversized.resource");
155
+ assertPositiveSafeInteger(outcome.limit, "oversized.limit");
156
+ if (outcome.observed !== null) {
157
+ assertPositiveSafeInteger(outcome.observed, "oversized.observed");
158
+ if (outcome.observed <= outcome.limit) throw new InvalidPackageSourceContract("oversized observation does not exceed limit");
159
+ }
160
+ }
161
+
162
+ function validateMismatched(outcome: MismatchedPackageSource): void {
163
+ assertAllowed(outcome.code, MISMATCHED_CODES, "mismatched.code");
164
+ assertText(outcome.expected, "mismatch.expected", 4096);
165
+ assertText(outcome.actual, "mismatch.actual", 4096);
166
+ if (outcome.expected === outcome.actual) throw new InvalidPackageSourceContract("mismatch values are equal");
167
+ }
168
+
169
+ function validateOutcome(outcome: PackageSourceOutcome, request: PackageSourceRequest, bounds: PackageSourceBounds): void {
170
+ switch (outcome.status) {
171
+ case "verified":
172
+ validateVerified(outcome, request);
173
+ return;
174
+ case "unavailable":
175
+ assertAllowed(outcome.code, UNAVAILABLE_CODES, "unavailable.code");
176
+ return;
177
+ case "ambiguous":
178
+ validateAmbiguous(outcome, bounds);
179
+ return;
180
+ case "unauthenticated":
181
+ validateUnauthenticated(outcome, bounds);
182
+ return;
183
+ case "oversized":
184
+ validateOversized(outcome);
185
+ return;
186
+ case "mismatched":
187
+ validateMismatched(outcome);
188
+ return;
189
+ default:
190
+ throw new InvalidPackageSourceContract("outcome.status");
191
+ }
192
+ }
193
+
194
+ export async function resolvePackageSource(
195
+ resolver: PackageSourceResolverPort,
196
+ request: PackageSourceRequest,
197
+ bounds: PackageSourceBounds,
198
+ ): Promise<PackageSourceOutcome> {
199
+ validateRequest(request);
200
+ validateBounds(bounds);
201
+ const outcome = await resolver.resolve(request, bounds);
202
+ validateOutcome(outcome, request, bounds);
203
+ return outcome;
204
+ }
@@ -1,3 +1,4 @@
1
+ import type { IntelligenceProvenance } from "./intelligence-provenance.ts";
1
2
  import type { PopulateSymbolGraphResult } from "./populate-symbol-graph.ts";
2
3
 
3
4
  export interface SymbolGraphGeneration {
@@ -5,6 +6,8 @@ export interface SymbolGraphGeneration {
5
6
  readonly maxFiles: number;
6
7
  readonly maxSymbolsPerFile: number;
7
8
  readonly completedAt: number;
9
+ /** Absent only for generations persisted before provenance was recorded. */
10
+ readonly provenance?: IntelligenceProvenance;
8
11
  readonly result: PopulateSymbolGraphResult;
9
12
  }
10
13
 
@@ -0,0 +1,16 @@
1
+ export const MAX_SYMBOL_QUERY_BYTES = 4_096;
2
+
3
+ export class InvalidSymbolQuery extends Error {
4
+ constructor(
5
+ readonly maxBytes: number,
6
+ readonly observedBytes: number,
7
+ ) {
8
+ super(`symbol query exceeds ${maxBytes} UTF-8 bytes (observed ${observedBytes})`);
9
+ this.name = "InvalidSymbolQuery";
10
+ }
11
+ }
12
+
13
+ export function assertBoundedSymbolQuery(query: string): void {
14
+ const observedBytes = Buffer.byteLength(query, "utf-8");
15
+ if (observedBytes > MAX_SYMBOL_QUERY_BYTES) throw new InvalidSymbolQuery(MAX_SYMBOL_QUERY_BYTES, observedBytes);
16
+ }
@@ -1,3 +1,5 @@
1
+ import type { IntelligenceProvenance } from "./intelligence-provenance.ts";
2
+
1
3
  /** A location within a workspace file: 1-indexed line and character, matching how humans and CLIs present positions. */
2
4
  export interface WorkspaceLocation {
3
5
  readonly path: string;
@@ -12,3 +14,9 @@ export interface WorkspaceSymbol {
12
14
  readonly location: WorkspaceLocation;
13
15
  readonly containerName?: string;
14
16
  }
17
+
18
+ export interface SymbolSearchResult {
19
+ readonly symbols: readonly WorkspaceSymbol[];
20
+ readonly truncated: boolean;
21
+ readonly provenance: IntelligenceProvenance;
22
+ }