@oscharko-dev/keiko-workspace 0.2.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.
Files changed (83) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/binaryDetect.d.ts +6 -0
  3. package/dist/binaryDetect.d.ts.map +1 -0
  4. package/dist/binaryDetect.js +20 -0
  5. package/dist/contextPack.d.ts +24 -0
  6. package/dist/contextPack.d.ts.map +1 -0
  7. package/dist/contextPack.js +118 -0
  8. package/dist/detect.d.ts +5 -0
  9. package/dist/detect.d.ts.map +1 -0
  10. package/dist/detect.js +144 -0
  11. package/dist/discovery.d.ts +10 -0
  12. package/dist/discovery.d.ts.map +1 -0
  13. package/dist/discovery.js +199 -0
  14. package/dist/document-extraction.d.ts +44 -0
  15. package/dist/document-extraction.d.ts.map +1 -0
  16. package/dist/document-extraction.js +372 -0
  17. package/dist/errors.d.ts +3 -0
  18. package/dist/errors.d.ts.map +1 -0
  19. package/dist/errors.js +4 -0
  20. package/dist/fs.d.ts +25 -0
  21. package/dist/fs.d.ts.map +1 -0
  22. package/dist/fs.js +69 -0
  23. package/dist/gitHistory.d.ts +3 -0
  24. package/dist/gitHistory.d.ts.map +1 -0
  25. package/dist/gitHistory.js +317 -0
  26. package/dist/ignore.d.ts +15 -0
  27. package/dist/ignore.d.ts.map +1 -0
  28. package/dist/ignore.js +248 -0
  29. package/dist/importGraph.d.ts +3 -0
  30. package/dist/importGraph.d.ts.map +1 -0
  31. package/dist/importGraph.js +131 -0
  32. package/dist/index.d.ts +27 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +25 -0
  35. package/dist/paths.d.ts +3 -0
  36. package/dist/paths.d.ts.map +1 -0
  37. package/dist/paths.js +38 -0
  38. package/dist/realpath.d.ts +9 -0
  39. package/dist/realpath.d.ts.map +1 -0
  40. package/dist/realpath.js +72 -0
  41. package/dist/repoSearch.d.ts +46 -0
  42. package/dist/repoSearch.d.ts.map +1 -0
  43. package/dist/repoSearch.js +350 -0
  44. package/dist/repoSearchEntries.d.ts +15 -0
  45. package/dist/repoSearchEntries.d.ts.map +1 -0
  46. package/dist/repoSearchEntries.js +106 -0
  47. package/dist/repoSearchLineSelection.d.ts +18 -0
  48. package/dist/repoSearchLineSelection.d.ts.map +1 -0
  49. package/dist/repoSearchLineSelection.js +43 -0
  50. package/dist/repoSearchMatchers.d.ts +8 -0
  51. package/dist/repoSearchMatchers.d.ts.map +1 -0
  52. package/dist/repoSearchMatchers.js +414 -0
  53. package/dist/repoSearchPolicy.d.ts +34 -0
  54. package/dist/repoSearchPolicy.d.ts.map +1 -0
  55. package/dist/repoSearchPolicy.js +342 -0
  56. package/dist/repoSearchRegexSafety.d.ts +2 -0
  57. package/dist/repoSearchRegexSafety.d.ts.map +1 -0
  58. package/dist/repoSearchRegexSafety.js +15 -0
  59. package/dist/repoSearchScan.d.ts +62 -0
  60. package/dist/repoSearchScan.d.ts.map +1 -0
  61. package/dist/repoSearchScan.js +292 -0
  62. package/dist/retrieval.d.ts +10 -0
  63. package/dist/retrieval.d.ts.map +1 -0
  64. package/dist/retrieval.js +74 -0
  65. package/dist/stableId.d.ts +4 -0
  66. package/dist/stableId.d.ts.map +1 -0
  67. package/dist/stableId.js +49 -0
  68. package/dist/structuralAdapters.d.ts +27 -0
  69. package/dist/structuralAdapters.d.ts.map +1 -0
  70. package/dist/structuralAdapters.js +87 -0
  71. package/dist/summary.d.ts +4 -0
  72. package/dist/summary.d.ts.map +1 -0
  73. package/dist/summary.js +54 -0
  74. package/dist/testSourcePairing.d.ts +3 -0
  75. package/dist/testSourcePairing.d.ts.map +1 -0
  76. package/dist/testSourcePairing.js +179 -0
  77. package/dist/types.d.ts +3 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +4 -0
  80. package/dist/version.d.ts +2 -0
  81. package/dist/version.d.ts.map +1 -0
  82. package/dist/version.js +4 -0
  83. package/package.json +35 -0
@@ -0,0 +1,131 @@
1
+ // Import-graph adapter (Epic #177, Issue #180). Pure-JS regex extractor: scans discovered
2
+ // files for ESM imports/re-exports and CJS requires, emits an EvidenceAtom for each file
3
+ // whose specifier matches the query text. Fixed, anchored, non-nested-quantifier regexes
4
+ // keep the scan ReDoS-safe; the binary probe and read cap come from the shared workspace
5
+ // primitives. Stays within ADR-0019 rule 3b: imports only @oscharko-dev/keiko-contracts,
6
+ // sibling workspace modules, and Node stdlib (node:crypto).
7
+ import { createHash } from "node:crypto";
8
+ import { looksBinary } from "./binaryDetect.js";
9
+ import { readWorkspaceFile } from "./discovery.js";
10
+ import { RepoSearchInvalidQueryError } from "./errors.js";
11
+ import { resolveWithinWorkspace } from "./paths.js";
12
+ import { assertContainedRealPath } from "./realpath.js";
13
+ import { buildAtom, gatherCandidates } from "./repoSearchScan.js";
14
+ function queryFingerprint(query) {
15
+ const canonical = JSON.stringify({ kind: query.kind, text: query.text });
16
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
17
+ }
18
+ // ESM static imports: import X from "y"; import * as X from "y"; import "y"; import { X } from "y".
19
+ // [ \t]+\S+ tokens in the import clause avoid ReDoS: [ \t] and \S are complementary so the engine
20
+ // never tries alternative splits. [ \t] (not \s) prevents the clause from crossing newlines.
21
+ const ESM_IMPORT = /^\s*import(?:[ \t]+\S+(?:[ \t]+\S+)*[ \t]+from)?\s+["']([^"'\n]+)["']/gm;
22
+ // ESM re-exports: export * from "y"; export { X } from "y".
23
+ const ESM_REEXPORT = /^\s*export\s+(?:\*|\{[^}]*\})\s+from\s+["']([^"'\n]+)["']/gm;
24
+ // CommonJS: require("y").
25
+ const CJS_REQUIRE = /\brequire\s*\(\s*["']([^"'\n]+)["']\s*\)/g;
26
+ const BINARY_PROBE_BYTES = 512;
27
+ async function probeBinary(fs, abs, size) {
28
+ const cap = Math.min(BINARY_PROBE_BYTES, size);
29
+ if (cap === 0) {
30
+ return false;
31
+ }
32
+ if (fs.readFileBytes !== undefined) {
33
+ return looksBinary(await fs.readFileBytes(abs, cap));
34
+ }
35
+ const text = fs.readFileUtf8(abs);
36
+ return looksBinary(new TextEncoder().encode(text.slice(0, cap)));
37
+ }
38
+ function specifierMatches(specifier, query) {
39
+ if (query.kind === "exact-symbol") {
40
+ return specifier === query.text;
41
+ }
42
+ return specifier.toLowerCase().includes(query.text.toLowerCase());
43
+ }
44
+ function scoreFor(specifier, query) {
45
+ return specifier === query.text ? 1.0 : 0.7;
46
+ }
47
+ function lineNumberOf(text, charIndex) {
48
+ let line = 1;
49
+ for (let i = 0; i < charIndex && i < text.length; i += 1) {
50
+ if (text.charCodeAt(i) === 10) {
51
+ line += 1;
52
+ }
53
+ }
54
+ return line;
55
+ }
56
+ function collectHits(text, query) {
57
+ const hits = [];
58
+ for (const regex of [ESM_IMPORT, ESM_REEXPORT, CJS_REQUIRE]) {
59
+ regex.lastIndex = 0;
60
+ let m = regex.exec(text);
61
+ while (m !== null) {
62
+ const specifier = m[1] ?? "";
63
+ if (specifierMatches(specifier, query)) {
64
+ hits.push({ specifier, line: lineNumberOf(text, m.index) });
65
+ }
66
+ m = regex.exec(text);
67
+ }
68
+ }
69
+ return hits;
70
+ }
71
+ function emitHitAtom(ctx, relativePath, hit) {
72
+ return buildAtom({
73
+ scopeId: ctx.scope.scopeId,
74
+ scopePath: relativePath,
75
+ lineRange: { startLine: hit.line, endLine: hit.line },
76
+ provenanceKind: "structural",
77
+ tool: "import-graph",
78
+ queryFingerprint: ctx.fingerprint,
79
+ score: scoreFor(hit.specifier, ctx.query),
80
+ emittedAtMs: ctx.nowMs(),
81
+ });
82
+ }
83
+ function elapsedOver(ctx) {
84
+ return ctx.nowMs() - ctx.startMs > ctx.limits.elapsedMsMax;
85
+ }
86
+ async function scanFileForImports(ctx, relativePath, atoms) {
87
+ const abs = resolveWithinWorkspace(ctx.scope.workspace.root, relativePath);
88
+ const containedAbs = assertContainedRealPath(ctx.fs, ctx.scope.workspace.root, abs, "scope");
89
+ const stat = ctx.fs.stat(containedAbs);
90
+ if (stat.hardLinkCount !== undefined && stat.hardLinkCount > 1) {
91
+ return;
92
+ }
93
+ if (await probeBinary(ctx.fs, containedAbs, stat.size)) {
94
+ return;
95
+ }
96
+ const content = readWorkspaceFile(ctx.scope.workspace, relativePath, { maxBytes: ctx.limits.maxBytesPerFileScanned }, ctx.fs);
97
+ const hits = collectHits(content.text, ctx.query);
98
+ for (const hit of hits) {
99
+ if (atoms.length >= ctx.limits.maxMatchesReturned) {
100
+ return;
101
+ }
102
+ atoms.push(emitHitAtom(ctx, relativePath, hit));
103
+ }
104
+ }
105
+ export const importGraphAdapter = {
106
+ name: "import-graph",
107
+ isAvailable: () => Promise.resolve(true),
108
+ lookup: async (scope, query, limits, fs, deps) => {
109
+ if (query.kind !== "natural-language" && query.kind !== "exact-symbol") {
110
+ throw new RepoSearchInvalidQueryError(`import-graph adapter does not accept query kind: ${query.kind}`);
111
+ }
112
+ const ctx = {
113
+ scope,
114
+ fs,
115
+ limits,
116
+ query,
117
+ fingerprint: queryFingerprint(query),
118
+ startMs: (deps?.nowMs ?? Date.now)(),
119
+ nowMs: deps?.nowMs ?? Date.now,
120
+ };
121
+ const candidateSet = gatherCandidates(scope, limits, fs);
122
+ const atoms = [];
123
+ for (const file of candidateSet.files) {
124
+ if (atoms.length >= limits.maxMatchesReturned || elapsedOver(ctx)) {
125
+ break;
126
+ }
127
+ await scanFileForImports(ctx, file.relativePath, atoms);
128
+ }
129
+ return atoms;
130
+ },
131
+ };
@@ -0,0 +1,27 @@
1
+ export type { AuditEntry, AuditSummary, ContextEntry, ContextEntrySummary, ContextPack, ContextPackSummary, ContextRequest, DiscoveredFile, DiscoveryOptions, DiscoveryStats, FileContent, ReadOptions, SelectionReason, TestFramework, WorkspaceInfo, WorkspaceLanguage, WorkspaceSummary, } from "./types.js";
2
+ export { DEFAULT_CONTEXT_REQUEST, DEFAULT_DISCOVERY_OPTIONS, DEFAULT_READ_OPTIONS, SELECTION_REASON_PRIORITY, } from "./types.js";
3
+ export { FileTooLargeError, PathDeniedError, PathEscapeError, RepoSearchInvalidQueryError, RepoSearchInvalidRangeError, RepoSearchUnsupportedFileError, WORKSPACE_CODES, WorkspaceError, WorkspaceNotFoundError, WorkspaceReadError, type WorkspaceCode, } from "./errors.js";
4
+ export { type WorkspaceDirEntry, type WorkspaceFs, type WorkspaceStat } from "./fs.js";
5
+ export { isWithinWorkspace, resolveWithinWorkspace } from "./paths.js";
6
+ export { assertContainedRealPath, containedRealPathInfo } from "./realpath.js";
7
+ export { compileIgnore, DEFAULT_DENY_PATTERNS, isDenied, isIgnored, type IgnoreMatcher, } from "./ignore.js";
8
+ export { detectWorkspace, detectWorkspaceAt } from "./detect.js";
9
+ export { discoverFiles, discoverWithStats, readWorkspaceFile, type DiscoveryResult, } from "./discovery.js";
10
+ export { lexicalRetrievalStrategy, type RankedFile, type RetrievalStrategy } from "./retrieval.js";
11
+ export { buildContextPack, buildContextPackFromFiles, selectScoredTextByByteBudget, type ContextPackDeps, type ScoredTextBudgetResult, type ScoredTextBudgetSelection, } from "./contextPack.js";
12
+ export { buildWorkspaceSummary, summarizeForAudit } from "./summary.js";
13
+ export type { SearchScope, SearchLimits, SearchResult, ReadExcerptRequest, ReadExcerptResult, } from "./repoSearch.js";
14
+ export { DEFAULT_SEARCH_LIMITS, searchText, findFiles, readExcerpt } from "./repoSearch.js";
15
+ export type { CandidateBucket, SearchDiagnostics, SearchHints, SearchIntent, SearchPolicy, SearchPolicyMode, } from "./repoSearchPolicy.js";
16
+ export { looksBinary, DEFAULT_BINARY_PROBE } from "./binaryDetect.js";
17
+ export type { BinaryProbeOptions } from "./binaryDetect.js";
18
+ export { evidenceAtomStableId, connectedContextPackStableId } from "./stableId.js";
19
+ export type { AdapterError, RunAllResult, StructuralAdapter, StructuralAdapterDeps, StructuralAdapterRegistry, } from "./structuralAdapters.js";
20
+ export { createDefaultStructuralRegistry, runStructuralAdapters } from "./structuralAdapters.js";
21
+ export { testSourcePairingAdapter } from "./testSourcePairing.js";
22
+ export { importGraphAdapter } from "./importGraph.js";
23
+ export { gitHistoryAdapter } from "./gitHistory.js";
24
+ export { KEIKO_WORKSPACE_VERSION } from "./version.js";
25
+ export type { DocumentExtractionBudget, DocumentExtractionFailure, DocumentExtractionResult, ExtractedDocumentContext, } from "./document-extraction.js";
26
+ export { MAX_EXTRACTED_BYTES, MAX_TOTAL_EXTRACTED_BYTES, SUPPORTED_MIME_LITERALS, SUPPORTED_MIME_PREFIXES, extractDocumentContext, } from "./document-extraction.js";
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ // Public barrel for the repository-context & workspace-access layer (ADR-0005). The only
2
+ // boundary-checked file-read seam is `readWorkspaceFile` (lexical containment + symlink
3
+ // realpath gate + size cap + redaction). The Node-backed `nodeWorkspaceFs` adapter is kept on
4
+ // the package's internal subpath so the public barrel exposes safe operations and injectable port
5
+ // types, not a parallel raw read path.
6
+ export { DEFAULT_CONTEXT_REQUEST, DEFAULT_DISCOVERY_OPTIONS, DEFAULT_READ_OPTIONS, SELECTION_REASON_PRIORITY, } from "./types.js";
7
+ export { FileTooLargeError, PathDeniedError, PathEscapeError, RepoSearchInvalidQueryError, RepoSearchInvalidRangeError, RepoSearchUnsupportedFileError, WORKSPACE_CODES, WorkspaceError, WorkspaceNotFoundError, WorkspaceReadError, } from "./errors.js";
8
+ export {} from "./fs.js";
9
+ export { isWithinWorkspace, resolveWithinWorkspace } from "./paths.js";
10
+ export { assertContainedRealPath, containedRealPathInfo } from "./realpath.js";
11
+ export { compileIgnore, DEFAULT_DENY_PATTERNS, isDenied, isIgnored, } from "./ignore.js";
12
+ export { detectWorkspace, detectWorkspaceAt } from "./detect.js";
13
+ export { discoverFiles, discoverWithStats, readWorkspaceFile, } from "./discovery.js";
14
+ export { lexicalRetrievalStrategy } from "./retrieval.js";
15
+ export { buildContextPack, buildContextPackFromFiles, selectScoredTextByByteBudget, } from "./contextPack.js";
16
+ export { buildWorkspaceSummary, summarizeForAudit } from "./summary.js";
17
+ export { DEFAULT_SEARCH_LIMITS, searchText, findFiles, readExcerpt } from "./repoSearch.js";
18
+ export { looksBinary, DEFAULT_BINARY_PROBE } from "./binaryDetect.js";
19
+ export { evidenceAtomStableId, connectedContextPackStableId } from "./stableId.js";
20
+ export { createDefaultStructuralRegistry, runStructuralAdapters } from "./structuralAdapters.js";
21
+ export { testSourcePairingAdapter } from "./testSourcePairing.js";
22
+ export { importGraphAdapter } from "./importGraph.js";
23
+ export { gitHistoryAdapter } from "./gitHistory.js";
24
+ export { KEIKO_WORKSPACE_VERSION } from "./version.js";
25
+ export { MAX_EXTRACTED_BYTES, MAX_TOTAL_EXTRACTED_BYTES, SUPPORTED_MIME_LITERALS, SUPPORTED_MIME_PREFIXES, extractDocumentContext, } from "./document-extraction.js";
@@ -0,0 +1,3 @@
1
+ export declare function resolveWithinWorkspace(root: string, candidate: string): string;
2
+ export declare function isWithinWorkspace(root: string, candidate: string): boolean;
3
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAeA,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAgB9E;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAO1E"}
package/dist/paths.js ADDED
@@ -0,0 +1,38 @@
1
+ // PURE, security-critical lexical path containment. This module performs NO filesystem
2
+ // access: it decides whether a candidate path lexically resolves inside a workspace root.
3
+ // Symlink/realpath containment (which DOES touch the filesystem) is enforced separately at
4
+ // the IO edge in discovery.ts — see ADR-0005 D2 for the split.
5
+ import { isAbsolute, relative, resolve, sep } from "node:path";
6
+ import { PathEscapeError } from "./errors.js";
7
+ function hasNul(value) {
8
+ return value.includes("\u0000");
9
+ }
10
+ // Returns the normalized absolute path of `candidate` inside `root`, or throws
11
+ // PathEscapeError. The returned value is the ONLY path that downstream IO should read, so
12
+ // a static analyser's path sanitizer sits on this boundary.
13
+ export function resolveWithinWorkspace(root, candidate) {
14
+ if (hasNul(root) || hasNul(candidate)) {
15
+ throw new PathEscapeError("path contains a NUL byte", candidate);
16
+ }
17
+ const absoluteRoot = resolve(root);
18
+ const absoluteCandidate = isAbsolute(candidate)
19
+ ? resolve(candidate)
20
+ : resolve(absoluteRoot, candidate);
21
+ const rel = relative(absoluteRoot, absoluteCandidate);
22
+ if (rel === "") {
23
+ return absoluteRoot;
24
+ }
25
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
26
+ throw new PathEscapeError(`path escapes the workspace boundary: ${candidate}`, candidate);
27
+ }
28
+ return absoluteCandidate;
29
+ }
30
+ export function isWithinWorkspace(root, candidate) {
31
+ try {
32
+ resolveWithinWorkspace(root, candidate);
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
@@ -0,0 +1,9 @@
1
+ import type { WorkspaceFs } from "./fs.js";
2
+ export interface ContainedRealPathInfo {
3
+ readonly path: string;
4
+ readonly realRelative: string;
5
+ readonly realBase: string;
6
+ }
7
+ export declare function containedRealPathInfo(fs: WorkspaceFs, root: string, absolutePath: string): ContainedRealPathInfo;
8
+ export declare function assertContainedRealPath(fs: WorkspaceFs, root: string, absolutePath: string, _label: string): string;
9
+ //# sourceMappingURL=realpath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realpath.d.ts","sourceRoot":"","sources":["../src/realpath.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAqC3C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAI9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,WAAW,EACf,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,qBAAqB,CAwBvB;AAQD,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,WAAW,EACf,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,MAAM,CAGR"}
@@ -0,0 +1,72 @@
1
+ // PURE-at-the-port symlink containment. After lexical resolveWithinWorkspace has proven a path is
2
+ // lexically inside the root, this gate defends against the symlink class of escape: a path whose
3
+ // real (symlink-followed) location is outside the root, or a not-yet-existing create target whose
4
+ // nearest existing parent escapes via a symlink. Every filesystem touch goes through the injected
5
+ // WorkspaceFs port (realPath only) so the logic stays testable with an in-memory fake and all real
6
+ // IO is auditable in one place (ADR-0005 D2, ADR-0006 D2). The read path (discovery.ts) and the
7
+ // write/cwd paths (tools/patch.ts, tools/exec.ts) share this single primitive — no duplicated logic.
8
+ import { dirname } from "node:path";
9
+ import { isWithinWorkspace } from "./paths.js";
10
+ import { PathEscapeError } from "./errors.js";
11
+ // Resolves `root` through any platform symlinks (e.g. macOS /var -> /private/var) so the
12
+ // containment comparison is symlink-consistent on both sides. Falls back to the lexical root.
13
+ function realRoot(fs, root) {
14
+ try {
15
+ return fs.realPath(root);
16
+ }
17
+ catch {
18
+ return root;
19
+ }
20
+ }
21
+ // Walks up from `absolutePath` to the nearest ancestor that exists on disk and returns its real
22
+ // path. A create target does not exist yet, so we must realpath the deepest existing parent to
23
+ // catch a symlinked parent directory (e.g. `link/evil` where `link` -> /outside). Bounded by the
24
+ // path depth; terminates at the filesystem root where dirname is a fixpoint.
25
+ function realNearestExisting(fs, absolutePath) {
26
+ let current = absolutePath;
27
+ for (;;) {
28
+ try {
29
+ return fs.realPath(current);
30
+ }
31
+ catch {
32
+ const parent = dirname(current);
33
+ if (parent === current) {
34
+ return absolutePath; // reached the root with nothing resolvable; lexical check stands
35
+ }
36
+ current = parent;
37
+ }
38
+ }
39
+ }
40
+ function toRelative(root, absolutePath) {
41
+ return absolutePath.slice(root.length).replace(/^[/\\]/, "");
42
+ }
43
+ export function containedRealPathInfo(fs, root, absolutePath) {
44
+ const realBase = realRoot(fs, root);
45
+ try {
46
+ const target = fs.realPath(absolutePath);
47
+ if (!isWithinWorkspace(realBase, target)) {
48
+ throw new PathEscapeError(`path escapes the workspace boundary via symlink: ${absolutePath}`, absolutePath);
49
+ }
50
+ return { path: target, realRelative: toRelative(realBase, target), realBase };
51
+ }
52
+ catch (error) {
53
+ if (error instanceof PathEscapeError) {
54
+ throw error;
55
+ }
56
+ const parentReal = realNearestExisting(fs, absolutePath);
57
+ if (!isWithinWorkspace(realBase, parentReal)) {
58
+ throw new PathEscapeError(`path escapes the workspace boundary via symlink: ${absolutePath}`, absolutePath);
59
+ }
60
+ return { path: absolutePath, realRelative: toRelative(realBase, parentReal), realBase };
61
+ }
62
+ }
63
+ // Asserts that `absolutePath` (already lexically contained) does not escape `root` via a symlink.
64
+ // For an existing target, the target's own realpath must stay within the real root. For a
65
+ // not-yet-existing target (create), the nearest existing ancestor's realpath must stay within it,
66
+ // which blocks `create through a symlinked directory` (the S-H1 .git/hooks escalation).
67
+ // Returns the canonical real path to hand to IO (existing case) or the lexically-resolved path
68
+ // (pure-create case where the target itself has no realpath yet).
69
+ export function assertContainedRealPath(fs, root, absolutePath, _label) {
70
+ const info = containedRealPathInfo(fs, root, absolutePath);
71
+ return info.path;
72
+ }
@@ -0,0 +1,46 @@
1
+ import type { CandidateFile, EvidenceAtom, RetrievalQuery } from "@oscharko-dev/keiko-contracts/connected-context";
2
+ import { type WorkspaceFs } from "./fs.js";
3
+ import { type SearchDiagnostics, type SearchHints } from "./repoSearchPolicy.js";
4
+ import type { WorkspaceInfo } from "./types.js";
5
+ export interface SearchScope {
6
+ readonly workspace: WorkspaceInfo;
7
+ readonly scopeId: string;
8
+ readonly relativePaths: readonly string[];
9
+ }
10
+ export interface SearchLimits {
11
+ readonly maxFilesScanned: number;
12
+ readonly maxMatchesReturned: number;
13
+ readonly maxBytesPerFileScanned: number;
14
+ readonly elapsedMsMax: number;
15
+ }
16
+ export declare const DEFAULT_SEARCH_LIMITS: SearchLimits;
17
+ export interface SearchResult {
18
+ readonly atoms: readonly EvidenceAtom[];
19
+ readonly candidates: readonly CandidateFile[];
20
+ readonly filesScanned: number;
21
+ readonly elapsedMs: number;
22
+ readonly truncated: boolean;
23
+ readonly diagnostics: SearchDiagnostics | undefined;
24
+ }
25
+ export interface ReadExcerptRequest {
26
+ readonly scopePath: string;
27
+ readonly startLine: number;
28
+ readonly endLine: number;
29
+ readonly maxBytes: number;
30
+ }
31
+ export interface ReadExcerptResult {
32
+ readonly atom: EvidenceAtom;
33
+ readonly content: string;
34
+ readonly truncated: boolean;
35
+ }
36
+ interface FacadeDeps {
37
+ readonly fs?: WorkspaceFs;
38
+ readonly nowMs?: () => number;
39
+ readonly searchHints?: SearchHints | undefined;
40
+ readonly signal?: AbortSignal;
41
+ }
42
+ export declare function searchText(scope: SearchScope, query: RetrievalQuery, limits?: SearchLimits, deps?: FacadeDeps): Promise<SearchResult>;
43
+ export declare function findFiles(scope: SearchScope, query: RetrievalQuery, limits?: SearchLimits, deps?: FacadeDeps): Promise<SearchResult>;
44
+ export declare function readExcerpt(scope: SearchScope, request: ReadExcerptRequest, deps?: FacadeDeps): Promise<ReadExcerptResult>;
45
+ export {};
46
+ //# sourceMappingURL=repoSearch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repoSearch.d.ts","sourceRoot":"","sources":["../src/repoSearch.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,iDAAiD,CAAC;AAWzD,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAmB5D,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,eAAO,MAAM,qBAAqB,EAAE,YAK1B,CAAC;AAWX,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;IAC9C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,iBAAiB,GAAG,SAAS,CAAC;CACrD;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,UAAU,UAAU;IAClB,QAAQ,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAqGD,wBAAsB,UAAU,CAC9B,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,cAAc,EACrB,MAAM,GAAE,YAAoC,EAC5C,IAAI,GAAE,UAAe,GACpB,OAAO,CAAC,YAAY,CAAC,CAsCvB;AA+HD,wBAAsB,SAAS,CAC7B,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,cAAc,EACrB,MAAM,GAAE,YAAoC,EAC5C,IAAI,GAAE,UAAe,GACpB,OAAO,CAAC,YAAY,CAAC,CAWvB;AAmHD,wBAAsB,WAAW,CAC/B,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE,UAAe,GACpB,OAAO,CAAC,iBAAiB,CAAC,CA+C5B"}