knodin 0.10.3 → 0.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +99 -7
- package/dist/src/engine/candidate-database.js +68 -0
- package/dist/src/engine/index.js +76 -5
- package/dist/src/engine/text-matches.js +309 -0
- package/dist/src/init.js +24 -3
- package/dist/src/lifecycle-health.js +36 -0
- package/dist/src/node-runtime.js +33 -0
- package/dist/src/tools/knodin-tools.js +36 -7
- package/dist/src/wait-for-fresh.js +43 -1
- package/docs/releases/0.10.4.md +161 -0
- package/docs/releases/0.10.5.md +172 -0
- package/package.json +3 -1
package/dist/bin/cli.js
CHANGED
|
@@ -77,8 +77,17 @@ function integrationAgents(repo) {
|
|
|
77
77
|
const repositoryDetected = inspectRepositoryIntegrationStatus(repo)?.agents ?? [];
|
|
78
78
|
return [...new Set([...detectSupportedAgents(), ...previous, ...repositoryDetected])];
|
|
79
79
|
}
|
|
80
|
-
function formatInitHuman(result) {
|
|
81
|
-
|
|
80
|
+
export function formatInitHuman(result) {
|
|
81
|
+
// `configured` is what THIS run wrote, not what exists on the machine. The
|
|
82
|
+
// old wording called an empty list "no supported coding agents detected",
|
|
83
|
+
// asserting a detection result init never computed — and it read as a flat
|
|
84
|
+
// contradiction of the `status` line seconds later, which lists the agents
|
|
85
|
+
// that genuinely are configured (EASFDC-8499).
|
|
86
|
+
//
|
|
87
|
+
// Deliberately not calling the detector here: a formatter that reads the
|
|
88
|
+
// machine cannot be tested deterministically, and the honest fix is to stop
|
|
89
|
+
// claiming more than this value supports rather than to go find more.
|
|
90
|
+
let agents = `${result.paths.scope} — no agents configured by this run`;
|
|
82
91
|
if (result.paths.scope === "cli-only")
|
|
83
92
|
agents = "CLI-only — AI agents are not configured to discover knodin";
|
|
84
93
|
else if (result.paths.agentIntegration.configured.length > 0)
|
|
@@ -165,7 +174,13 @@ function formatIndexHuman(result) {
|
|
|
165
174
|
function formatIndexVerificationError(result) {
|
|
166
175
|
const firstIssue = result.verification.missing.files[0] ?? result.verification.missing.records[0];
|
|
167
176
|
const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
|
|
168
|
-
|
|
177
|
+
// Describes whichever condition actually gated. Printing the issue count
|
|
178
|
+
// unconditionally produced "0 graph issue(s) remain" as a failure reason —
|
|
179
|
+
// a sentence that reports no problem and then recommends repair (KNODIN-7).
|
|
180
|
+
const condition = result.verification.issueCount > 0
|
|
181
|
+
? `${result.verification.issueCount.toLocaleString()} graph issue(s) remain`
|
|
182
|
+
: `graph verification reported status "${result.verification.status}"`;
|
|
183
|
+
return `knodin index: requested work completed, but ${condition}.${detail} Run \`knodin repair\`.\n`;
|
|
169
184
|
}
|
|
170
185
|
/**
|
|
171
186
|
* The hooks/lifecycle line of `status`.
|
|
@@ -2247,11 +2262,39 @@ async function main() {
|
|
|
2247
2262
|
result = indexResult;
|
|
2248
2263
|
const indexedCheck = checkIndexed([...indexResult.indexed, ...indexResult.unchanged], plan.repo);
|
|
2249
2264
|
if (!indexedCheck.ok) {
|
|
2265
|
+
// Same obligation as the verification gate below: `--json` must mean
|
|
2266
|
+
// parseable output on every exit path, and this one wrote zero bytes
|
|
2267
|
+
// to stdout and a bare sentence to stderr (KNODIN-5).
|
|
2268
|
+
if (jsonOutput)
|
|
2269
|
+
process.stdout.write(`${JSON.stringify({
|
|
2270
|
+
available: false,
|
|
2271
|
+
status: "unavailable",
|
|
2272
|
+
state: "unknown",
|
|
2273
|
+
error: indexedCheck.error,
|
|
2274
|
+
})}\n`);
|
|
2250
2275
|
process.stderr.write(`${indexedCheck.error}\n`);
|
|
2251
2276
|
await engine.close();
|
|
2252
2277
|
process.exit(1);
|
|
2253
2278
|
}
|
|
2254
|
-
|
|
2279
|
+
// Gated on damage, not on `status !== "healthy"`. That older condition
|
|
2280
|
+
// rejected `stale`, and a freshly promoted graph is routinely stale
|
|
2281
|
+
// because freshness has not been probed since the index wrote — so the
|
|
2282
|
+
// FIRST clean index of any repository exited 1 while its own progress
|
|
2283
|
+
// output said "Promoted graph has 0 issue(s)" and its error said "0
|
|
2284
|
+
// graph issue(s) remain. Run `knodin repair`", recommending repair for
|
|
2285
|
+
// a graph it had just verified as undamaged (KNODIN-7).
|
|
2286
|
+
//
|
|
2287
|
+
// `repair-needed` and a non-zero issue count keep every real failure.
|
|
2288
|
+
// Staleness is a freshness fact that `status` reports and ordinary use
|
|
2289
|
+
// reconciles; it is not a verification result, and treating it as one
|
|
2290
|
+
// made a new repository's first command fail for doing nothing wrong.
|
|
2291
|
+
if (indexResult.verification.status === "repair-needed" ||
|
|
2292
|
+
indexResult.verification.issueCount > 0) {
|
|
2293
|
+
// Emitted before exiting so `--json` is not silently empty on this
|
|
2294
|
+
// path: it previously wrote zero bytes to stdout and a human
|
|
2295
|
+
// sentence to stderr, which is the same defect as KNODIN-5.
|
|
2296
|
+
if (jsonOutput)
|
|
2297
|
+
process.stdout.write(`${JSON.stringify(indexResult)}\n`);
|
|
2255
2298
|
process.stderr.write(formatIndexVerificationError(indexResult));
|
|
2256
2299
|
await engine.close();
|
|
2257
2300
|
process.exit(1);
|
|
@@ -2475,14 +2518,38 @@ async function main() {
|
|
|
2475
2518
|
stripEmbeddings: !rest.includes("--keep-embeddings"),
|
|
2476
2519
|
includeExcluded: rest.includes("--include-excluded"),
|
|
2477
2520
|
});
|
|
2521
|
+
// `ok: false` and exit 0 are contradictory claims from one command, and
|
|
2522
|
+
// the exit code is what every shell idiom reads. `knodin seal --output X
|
|
2523
|
+
// && upload X` ran the upload after a refusal (KNODIN-4).
|
|
2524
|
+
//
|
|
2525
|
+
// seal already fails closed on content — it writes no file when it
|
|
2526
|
+
// refuses — so this closes the last gap rather than papering over one.
|
|
2527
|
+
if (result.ok === false)
|
|
2528
|
+
process.exitCode = 1;
|
|
2478
2529
|
break;
|
|
2479
2530
|
}
|
|
2480
2531
|
case "sealed": {
|
|
2481
|
-
|
|
2532
|
+
// Positionals are extracted rather than read off `rest` by index.
|
|
2533
|
+
// `rest[1]` took whatever token followed the artifact, so
|
|
2534
|
+
// `sealed <artifact> --tokens 200` explained a symbol literally named
|
|
2535
|
+
// "--tokens" — and at that budget the fabricated empty explain block
|
|
2536
|
+
// evicted commit, ref and sealedAt, which are the fields an
|
|
2537
|
+
// inspect-only call exists to return (KNODIN-3).
|
|
2538
|
+
//
|
|
2539
|
+
// `extractPositionals` already knows which flags consume the token
|
|
2540
|
+
// after them, so `200` is not mistaken for a symbol either.
|
|
2541
|
+
const positionals = extractPositionals(rest);
|
|
2542
|
+
if (!positionals[0])
|
|
2482
2543
|
throw new Error("knodin sealed requires an artifact path");
|
|
2483
|
-
result = await runSealedQuery(
|
|
2544
|
+
result = await runSealedQuery(positionals[0], positionals[1], {
|
|
2484
2545
|
strictCompat: rest.includes("--strict-compat"),
|
|
2485
2546
|
});
|
|
2547
|
+
// Same contradiction as `seal`, found while testing the fix for it:
|
|
2548
|
+
// `sealed <missing-artifact>` reported ok: false and exited 0, so a
|
|
2549
|
+
// caller checking $? saw success (KNODIN-4 R2 — audit the other
|
|
2550
|
+
// commands emitting an `ok` field).
|
|
2551
|
+
if (result.ok === false)
|
|
2552
|
+
process.exitCode = 1;
|
|
2486
2553
|
break;
|
|
2487
2554
|
}
|
|
2488
2555
|
case "pack": {
|
|
@@ -3029,7 +3096,32 @@ catch (err) {
|
|
|
3029
3096
|
error: err,
|
|
3030
3097
|
});
|
|
3031
3098
|
const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
|
|
3032
|
-
|
|
3099
|
+
const message = describeThrown(err);
|
|
3100
|
+
// `--json` has to mean parseable output on EVERY exit path, not only the
|
|
3101
|
+
// successful one. A bare string here makes a consumer's parser throw a
|
|
3102
|
+
// decode error, which gets triaged as a malformed response or a version
|
|
3103
|
+
// mismatch — while the string it failed to parse already named the cause
|
|
3104
|
+
// (KNODIN-5, found as "attempt to write a readonly database" from
|
|
3105
|
+
// `knodin --json sealed` against a read-only artifact).
|
|
3106
|
+
//
|
|
3107
|
+
// Shaped like the unavailability envelope the graph-backed paths already
|
|
3108
|
+
// return for the same class of condition, so a caller can handle both
|
|
3109
|
+
// surfaces with one branch. Written to stdout because that is where a
|
|
3110
|
+
// caller reading `--json` is looking; the human path keeps stderr.
|
|
3111
|
+
// The envelope is ADDITIONAL, not a replacement. stdout is the JSON
|
|
3112
|
+
// channel and stderr is the human one, so writing the envelope instead of
|
|
3113
|
+
// the message silently emptied stderr for every existing caller that reads
|
|
3114
|
+
// diagnostics there — `configure --json` asserts its refusal on stderr, and
|
|
3115
|
+
// it went blank.
|
|
3116
|
+
if (argv.includes("--json"))
|
|
3117
|
+
process.stdout.write(`${JSON.stringify({
|
|
3118
|
+
available: false,
|
|
3119
|
+
status: "unavailable",
|
|
3120
|
+
state: "unknown",
|
|
3121
|
+
error: message,
|
|
3122
|
+
...(diagnostic.recorded ? { diagnosticId: diagnostic.correlationId } : {}),
|
|
3123
|
+
})}\n`);
|
|
3124
|
+
console.error(`${message}${correlation}`);
|
|
3033
3125
|
process.exit(1);
|
|
3034
3126
|
}
|
|
3035
3127
|
}
|
|
@@ -200,6 +200,30 @@ export function promoteCandidateFile(repo, candidate) {
|
|
|
200
200
|
fsyncDirectory(stateDir);
|
|
201
201
|
fs.rmSync(marker, { force: true });
|
|
202
202
|
fsyncDirectory(stateDir);
|
|
203
|
+
// The rename above moves the database out and nothing else, and the
|
|
204
|
+
// candidate directory is NOT empty at this point: the clean-index audit
|
|
205
|
+
// writes `status-audit-v1.json` beside the candidate database. So every
|
|
206
|
+
// SUCCESSFUL promotion left a directory behind — and `listCandidates`
|
|
207
|
+
// skips directories without a database, so the only cleanup path could
|
|
208
|
+
// never see them (KNODIN-2). Twelve had accumulated in this repository
|
|
209
|
+
// since 11 Aug and thirty in a reporting checkout, where they were
|
|
210
|
+
// reasonably read as promotions that had failed.
|
|
211
|
+
//
|
|
212
|
+
// Removed after the marker, so an interruption before this point still
|
|
213
|
+
// recovers through the marker rather than losing a candidate it needed.
|
|
214
|
+
//
|
|
215
|
+
// Swallowed rather than left to the enclosing `catch`: the promotion is
|
|
216
|
+
// already committed here — the marker is gone and the new database is
|
|
217
|
+
// fsynced into place — so a failure to delete leftover residue (a locked
|
|
218
|
+
// file, EPERM on a read-only parent) must not reach the rollback below,
|
|
219
|
+
// which would move the just-promoted database back into the candidate
|
|
220
|
+
// directory and restore the old graph over a promotion that succeeded.
|
|
221
|
+
try {
|
|
222
|
+
fs.rmSync(path.dirname(candidate.databasePath), { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Residue only. `sweepAbandonedCandidates` collects it on the next run.
|
|
226
|
+
}
|
|
203
227
|
return fs.existsSync(backupPath) ? backupPath : null;
|
|
204
228
|
}
|
|
205
229
|
catch (error) {
|
|
@@ -214,6 +238,50 @@ export function promoteCandidateFile(repo, candidate) {
|
|
|
214
238
|
throw error;
|
|
215
239
|
}
|
|
216
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Remove candidate directories that hold no database, and report how many.
|
|
243
|
+
*
|
|
244
|
+
* `listCandidates` deliberately answers "what could be resumed", so it skips a
|
|
245
|
+
* directory with no `db.sqlite`. Cleanup used to run only over that list, which
|
|
246
|
+
* meant the one kind of directory guaranteed to have no database — the residue a
|
|
247
|
+
* successful promotion leaves once the database is renamed out — was the one
|
|
248
|
+
* kind nothing could ever delete (KNODIN-2).
|
|
249
|
+
*
|
|
250
|
+
* Promotion now removes its own directory, so this exists for two other cases:
|
|
251
|
+
* residue already accumulated by earlier versions, and directories left by a
|
|
252
|
+
* process that died between `allocateCandidate` and writing a database.
|
|
253
|
+
*
|
|
254
|
+
* Returns the count rather than nothing so callers can say what they swept
|
|
255
|
+
* instead of tidying up silently.
|
|
256
|
+
*/
|
|
257
|
+
export function sweepAbandonedCandidates(repo) {
|
|
258
|
+
const resolvedRepo = path.resolve(repo);
|
|
259
|
+
const root = candidateRoot(resolvedRepo);
|
|
260
|
+
let entries;
|
|
261
|
+
try {
|
|
262
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
let removed = 0;
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
if (!entry.isDirectory() || !entry.name.startsWith("candidate-"))
|
|
270
|
+
continue;
|
|
271
|
+
const directory = path.join(root, entry.name);
|
|
272
|
+
if (fs.existsSync(path.join(directory, "db.sqlite")))
|
|
273
|
+
continue;
|
|
274
|
+
try {
|
|
275
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
276
|
+
removed++;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Best effort. Failing to remove residue must not stop the caller's
|
|
280
|
+
// actual work, which is what this is being cleaned up alongside.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return removed;
|
|
284
|
+
}
|
|
217
285
|
export function discardCandidateFiles(repo, candidate) {
|
|
218
286
|
assertCandidate(repo, candidate);
|
|
219
287
|
fs.rmSync(path.dirname(candidate.databasePath), { recursive: true, force: true });
|
package/dist/src/engine/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import { contentFingerprint, writeStructuralSnapshot, } from "../structural-snap
|
|
|
28
28
|
import { acquireLifecycleCoordination } from "../update-coordination.js";
|
|
29
29
|
import { KNODIN_VERSION } from "../version.js";
|
|
30
30
|
import * as ann from "./ann-hnsw.js";
|
|
31
|
-
import { allocateCandidate, assertCandidate, discardCandidateFiles, listCandidates, promoteCandidateFile, recoverInterruptedPromotion, } from "./candidate-database.js";
|
|
31
|
+
import { allocateCandidate, assertCandidate, discardCandidateFiles, listCandidates, promoteCandidateFile, recoverInterruptedPromotion, sweepAbandonedCandidates, } from "./candidate-database.js";
|
|
32
32
|
import { computeSimilarity, generateEmbedding, generateEmbeddings, } from "./embeddings.js";
|
|
33
33
|
import { walkRepoFiles } from "./file-walker.js";
|
|
34
34
|
import { clearGitHistorySignalCache, collectGitHistorySignals, } from "./git-history.js";
|
|
@@ -41,11 +41,12 @@ import { isIndexablePath, makeWatchIgnorePredicate } from "./prune.js";
|
|
|
41
41
|
import { reflinkCopyFile } from "./reflink-copy.js";
|
|
42
42
|
import { readSarifLog, SARIF_DEFAULT_LIMITS, } from "./sarif-import.js";
|
|
43
43
|
import { readScipIndex, SCIP_DEFAULT_LIMITS, } from "./scip-import.js";
|
|
44
|
-
import { listSealedFiles, readSealedSource } from "./seal.js";
|
|
44
|
+
import { isSealedDatabase, listSealedFiles, readSealedSource } from "./seal.js";
|
|
45
45
|
import { isIndexableSourcePath } from "./source-policy.js";
|
|
46
46
|
import { Database } from "./sqlite.js";
|
|
47
47
|
import { lookupMirror, mayWriteToRepository, resolveDbPath, resolveStateDir, } from "./state-paths.js";
|
|
48
48
|
import { deleteAllSymbols, deleteSymbolsForFile, deleteSymbolsMatchingPath, ORPHANED_EMBEDDING_PREDICATE, purgeOrphanEmbeddings, } from "./symbol-delete.js";
|
|
49
|
+
import { searchRepoText } from "./text-matches.js";
|
|
49
50
|
// ES Module resolution
|
|
50
51
|
const __filename = fileURLToPath(import.meta.url);
|
|
51
52
|
const __dirname = path.dirname(__filename);
|
|
@@ -7272,6 +7273,12 @@ function sumCounts(counts) {
|
|
|
7272
7273
|
*/
|
|
7273
7274
|
function findResumableCandidate(repoPath) {
|
|
7274
7275
|
const head = gitHead(repoPath) ?? "";
|
|
7276
|
+
// Swept first, because the loop below can only discard what `listCandidates`
|
|
7277
|
+
// returns and that deliberately excludes database-less directories — which is
|
|
7278
|
+
// exactly what a promoted candidate leaves behind (KNODIN-2). Without this,
|
|
7279
|
+
// the comment above about not accumulating abandoned copies was false for the
|
|
7280
|
+
// most common case.
|
|
7281
|
+
sweepAbandonedCandidates(repoPath);
|
|
7275
7282
|
let resumable = null;
|
|
7276
7283
|
for (const candidate of listCandidates(repoPath)) {
|
|
7277
7284
|
let usable = false;
|
|
@@ -7682,8 +7689,54 @@ function recordIndexState(db, repoPath, relPath) {
|
|
|
7682
7689
|
return 0;
|
|
7683
7690
|
}
|
|
7684
7691
|
}
|
|
7692
|
+
/**
|
|
7693
|
+
* Whether this database is a sealed artifact — one carrying its own source.
|
|
7694
|
+
*
|
|
7695
|
+
* Decided from the seal attestation, which is what `seal` writes to declare an
|
|
7696
|
+
* artifact sealed, rather than from a row count in `sealed_file`: an artifact of
|
|
7697
|
+
* a repository that indexed no files carries no `sealed_file` rows and is still
|
|
7698
|
+
* sealed, and a count answers "does it embed source for anything" instead of
|
|
7699
|
+
* "is this an artifact".
|
|
7700
|
+
*
|
|
7701
|
+
* Cached per database: this is asked once per file during reconciliation, and
|
|
7702
|
+
* the answer cannot change for the life of a connection.
|
|
7703
|
+
*/
|
|
7704
|
+
const sealedArtifactDatabases = new WeakMap();
|
|
7705
|
+
function databaseIsSealedArtifact(db) {
|
|
7706
|
+
const known = sealedArtifactDatabases.get(db);
|
|
7707
|
+
if (known !== undefined)
|
|
7708
|
+
return known;
|
|
7709
|
+
// `isSealedDatabase` returns false for an ordinary working graph, including
|
|
7710
|
+
// one with no `meta` table at all, so no extra guard is needed here.
|
|
7711
|
+
const sealed = isSealedDatabase(db);
|
|
7712
|
+
sealedArtifactDatabases.set(db, sealed);
|
|
7713
|
+
return sealed;
|
|
7714
|
+
}
|
|
7715
|
+
/**
|
|
7716
|
+
* Whether a file missing from disk should be purged from this graph.
|
|
7717
|
+
*
|
|
7718
|
+
* For a working checkout, purging is right — the file really was deleted. For an
|
|
7719
|
+
* artifact whose source was materialized from `sealed_source`, a missing file
|
|
7720
|
+
* means the extraction was incomplete, not that the repository changed, and
|
|
7721
|
+
* purging silently rewrote the graph to match a truncated tree: 858 index_state
|
|
7722
|
+
* rows fell to 627 on one query while `sealed_file` still claimed 858, leaving
|
|
7723
|
+
* the artifact contradicting itself and answering confidently from the smaller
|
|
7724
|
+
* graph (KNODIN-6).
|
|
7725
|
+
*
|
|
7726
|
+
* Keeping every file-owned row makes the discrepancy visible instead: health
|
|
7727
|
+
* reports the file as indexed-but-missing, which is the truth, and a symbol
|
|
7728
|
+
* query against it can be told apart from a symbol that genuinely has no
|
|
7729
|
+
* callers. Guarding only the `index_state` row would be worse than not guarding
|
|
7730
|
+
* at all — the symbols, references and dependencies would still be gone, and
|
|
7731
|
+
* `index_state` would then claim coverage the graph no longer has.
|
|
7732
|
+
*/
|
|
7733
|
+
function shouldPurgeMissingFile(db) {
|
|
7734
|
+
return !databaseIsSealedArtifact(db);
|
|
7735
|
+
}
|
|
7685
7736
|
/** Drop a file's `index_state` row (used when a file is deleted). */
|
|
7686
7737
|
function removeIndexState(db, relPath) {
|
|
7738
|
+
if (!shouldPurgeMissingFile(db))
|
|
7739
|
+
return;
|
|
7687
7740
|
try {
|
|
7688
7741
|
db.run("DELETE FROM index_state WHERE filePath = ?", [relPath]);
|
|
7689
7742
|
}
|
|
@@ -7853,7 +7906,7 @@ async function reconcileIndex(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
7853
7906
|
reindexedPaths.push(rel);
|
|
7854
7907
|
}
|
|
7855
7908
|
}
|
|
7856
|
-
else {
|
|
7909
|
+
else if (shouldPurgeMissingFile(db)) {
|
|
7857
7910
|
// Deletion or source-policy retirement — purge every file-owned row.
|
|
7858
7911
|
db.run("BEGIN TRANSACTION;");
|
|
7859
7912
|
try {
|
|
@@ -8523,6 +8576,8 @@ function startFileWatcher(repoPath, db, watcherFileLimit = MAX_RECURSIVE_WATCH_F
|
|
|
8523
8576
|
};
|
|
8524
8577
|
watchQueues.set(resolvedRepoPath, queue);
|
|
8525
8578
|
const purgeFile = (relativePath) => {
|
|
8579
|
+
if (!shouldPurgeMissingFile(db))
|
|
8580
|
+
return;
|
|
8526
8581
|
db.run("BEGIN TRANSACTION;");
|
|
8527
8582
|
try {
|
|
8528
8583
|
deleteSymbolsForFile(db, relativePath);
|
|
@@ -11555,6 +11610,13 @@ function withStaleness(engine, claimRepository, openDb, openPolicy) {
|
|
|
11555
11610
|
row.staleness = staleness;
|
|
11556
11611
|
return page;
|
|
11557
11612
|
},
|
|
11613
|
+
searchText(term, repoPath, options) {
|
|
11614
|
+
claimRepository(repoPath);
|
|
11615
|
+
// No staleness annotation: this reads the working tree directly rather
|
|
11616
|
+
// than the graph, so its answers are current by construction and
|
|
11617
|
+
// stamping them with the index's freshness would misreport them.
|
|
11618
|
+
return engine.searchText(term, repoPath, options);
|
|
11619
|
+
},
|
|
11558
11620
|
async query(pattern, target, repoPath, to, limit, depth, detailLevel, selector, impactOptions, options) {
|
|
11559
11621
|
claimRepository(repoPath);
|
|
11560
11622
|
const result = await engine.query(pattern, target, repoPath, to, limit, depth, detailLevel, selector, impactOptions, options);
|
|
@@ -11893,7 +11955,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
11893
11955
|
await indexFile(absolute, file, resolved, db);
|
|
11894
11956
|
recordIndexState(db, resolved, file);
|
|
11895
11957
|
}
|
|
11896
|
-
else {
|
|
11958
|
+
else if (shouldPurgeMissingFile(db)) {
|
|
11897
11959
|
db.run("BEGIN TRANSACTION;");
|
|
11898
11960
|
try {
|
|
11899
11961
|
deleteSymbolsForFile(db, file);
|
|
@@ -13858,7 +13920,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13858
13920
|
await indexFile(absPath, relativePath, repoPath, db);
|
|
13859
13921
|
recordIndexState(db, repoPath, relativePath);
|
|
13860
13922
|
}
|
|
13861
|
-
else {
|
|
13923
|
+
else if (shouldPurgeMissingFile(db)) {
|
|
13862
13924
|
// File was deleted
|
|
13863
13925
|
db.run("BEGIN TRANSACTION;");
|
|
13864
13926
|
try {
|
|
@@ -14073,6 +14135,15 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14073
14135
|
indexCoordination.release();
|
|
14074
14136
|
}
|
|
14075
14137
|
},
|
|
14138
|
+
async searchText(term, repoPath, options = {}) {
|
|
14139
|
+
if (term.length === 0)
|
|
14140
|
+
throw new Error("knodin searchText: term must not be empty");
|
|
14141
|
+
// Reads the working tree, not the graph, so it needs no open database
|
|
14142
|
+
// and no freshness check. `getLanguageForFile` is the same grammar
|
|
14143
|
+
// loader indexing uses, which is what makes the classification
|
|
14144
|
+
// evidence rather than a heuristic over file extensions.
|
|
14145
|
+
return searchRepoText(path.resolve(repoPath), term, getLanguageForFile, () => new Parser(), options);
|
|
14146
|
+
},
|
|
14076
14147
|
async search(query, repoPath, limit, options = {}) {
|
|
14077
14148
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
14078
14149
|
const offset = options.offset ?? 0;
|