sweet-search 2.5.2 → 2.5.3
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/core/cli.js +24 -3
- package/core/graph/graph-expansion.js +215 -36
- package/core/graph/graph-extractor.js +196 -11
- package/core/graph/graph-search.js +395 -92
- package/core/graph/hcgs-generator.js +2 -1
- package/core/graph/index.js +2 -0
- package/core/graph/repo-map.js +28 -6
- package/core/graph/structural-answer-cues.js +168 -0
- package/core/graph/structural-callsite-hints.js +40 -0
- package/core/graph/structural-context-format.js +40 -0
- package/core/graph/structural-context.js +450 -0
- package/core/graph/structural-forward-push.js +156 -0
- package/core/graph/structural-header-context.js +19 -0
- package/core/graph/structural-importance.js +148 -0
- package/core/graph/structural-pagerank.js +197 -0
- package/core/graph/summary-manager.js +13 -9
- package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
- package/core/incremental-indexing/application/file-watcher.mjs +197 -0
- package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
- package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
- package/core/incremental-indexing/application/operator-cli.mjs +554 -0
- package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
- package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
- package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
- package/core/incremental-indexing/application/reconciler.mjs +477 -0
- package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
- package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
- package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
- package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
- package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
- package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
- package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
- package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
- package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
- package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
- package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
- package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
- package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
- package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
- package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
- package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
- package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
- package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
- package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
- package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
- package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
- package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
- package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
- package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
- package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
- package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
- package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
- package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
- package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
- package/core/indexing/admission-policy.js +139 -0
- package/core/indexing/artifact-builder.js +29 -12
- package/core/indexing/ast-chunker.js +107 -30
- package/core/indexing/dedup/exemplar-selector.js +19 -1
- package/core/indexing/gitignore-filter.js +223 -0
- package/core/indexing/incremental-tracker.js +99 -30
- package/core/indexing/index-codebase-v21.js +6 -5
- package/core/indexing/index-maintainer.mjs +698 -6
- package/core/indexing/indexer-ann.js +99 -15
- package/core/indexing/indexer-build.js +158 -45
- package/core/indexing/indexer-empty-baseline.js +80 -0
- package/core/indexing/indexer-manifest.js +66 -0
- package/core/indexing/indexer-phases.js +56 -23
- package/core/indexing/indexer-sparse-gram.js +54 -13
- package/core/indexing/indexer-utils.js +26 -208
- package/core/indexing/indexing-file-policy.js +32 -7
- package/core/indexing/maintainer-launcher.mjs +137 -0
- package/core/indexing/merkle-tracker.js +251 -244
- package/core/indexing/model-pool.js +46 -5
- package/core/infrastructure/code-graph-repository.js +758 -6
- package/core/infrastructure/code-graph-visibility.js +157 -0
- package/core/infrastructure/codebase-repository.js +100 -13
- package/core/infrastructure/config/search.js +1 -1
- package/core/infrastructure/db-utils.js +118 -0
- package/core/infrastructure/dedup-hashing.js +10 -13
- package/core/infrastructure/hardware-capability.js +17 -7
- package/core/infrastructure/index.js +8 -2
- package/core/infrastructure/language-patterns/maps.js +4 -1
- package/core/infrastructure/language-patterns/registry-core.js +56 -17
- package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
- package/core/infrastructure/language-patterns.js +69 -0
- package/core/infrastructure/model-registry.js +20 -0
- package/core/infrastructure/native-inference.js +7 -12
- package/core/infrastructure/native-resolver.js +52 -37
- package/core/infrastructure/native-sparse-gram.js +261 -20
- package/core/infrastructure/native-tokenizer.js +6 -15
- package/core/infrastructure/simd-distance.js +10 -16
- package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
- package/core/infrastructure/structural-alias-resolver.js +122 -0
- package/core/infrastructure/structural-candidate-ranker.js +34 -0
- package/core/infrastructure/structural-context-repository.js +472 -0
- package/core/infrastructure/structural-context-utils.js +51 -0
- package/core/infrastructure/structural-graph-signals.js +121 -0
- package/core/infrastructure/structural-qualified-resolution.js +15 -0
- package/core/infrastructure/structural-source-definitions.js +100 -0
- package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
- package/core/infrastructure/tree-sitter-provider.js +811 -37
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
- package/core/query/query-router.js +55 -5
- package/core/ranking/file-kind-ranking.js +2192 -15
- package/core/ranking/late-interaction-index.js +87 -12
- package/core/search/cli-decoration.js +290 -0
- package/core/search/context-expander.js +988 -78
- package/core/search/index.js +1 -0
- package/core/search/output-policy.js +275 -0
- package/core/search/search-anchor.js +499 -0
- package/core/search/search-boost.js +93 -1
- package/core/search/search-cli.js +61 -204
- package/core/search/search-hybrid.js +250 -10
- package/core/search/search-pattern-chunks.js +57 -8
- package/core/search/search-pattern-planner.js +68 -9
- package/core/search/search-pattern-prefilter.js +30 -10
- package/core/search/search-pattern-ripgrep.js +40 -4
- package/core/search/search-pattern-sparse-overlay.js +256 -0
- package/core/search/search-pattern.js +117 -29
- package/core/search/search-postprocess.js +479 -5
- package/core/search/search-read-semantic.js +260 -23
- package/core/search/search-read.js +82 -64
- package/core/search/search-reader-pin.js +71 -0
- package/core/search/search-rrf.js +279 -0
- package/core/search/search-semantic.js +110 -5
- package/core/search/search-server.js +130 -57
- package/core/search/search-trace.js +107 -0
- package/core/search/server-identity.js +93 -0
- package/core/search/session-daemon-prewarm.mjs +33 -10
- package/core/search/sweet-search.js +399 -7
- package/core/skills/sweet-index/SKILL.md +8 -6
- package/core/vector-store/binary-hnsw-index.js +194 -30
- package/core/vector-store/float-vector-store.js +96 -6
- package/core/vector-store/hnsw-index.js +220 -49
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
- package/eval/agent-read-workflows/bin/ss-find +15 -0
- package/eval/agent-read-workflows/bin/ss-grep +12 -0
- package/eval/agent-read-workflows/bin/ss-read +14 -0
- package/eval/agent-read-workflows/bin/ss-search +18 -0
- package/eval/agent-read-workflows/bin/ss-semantic +12 -0
- package/eval/agent-read-workflows/bin/ss-trace +11 -0
- package/mcp/read-tool.js +109 -0
- package/mcp/server.js +55 -15
- package/mcp/tool-handlers.js +14 -124
- package/mcp/trace-tool.js +81 -0
- package/package.json +25 -10
- package/scripts/hooks/intercept-read.mjs +55 -0
- package/scripts/hooks/remind-tools.mjs +40 -0
- package/scripts/init.js +698 -54
- package/scripts/inject-agent-instructions.js +431 -0
- package/scripts/install-prompt-reminders.js +188 -0
- package/scripts/install-tool-enforcement.js +220 -0
- package/scripts/smoke-test.js +12 -9
- package/scripts/uninstall.js +276 -18
- package/scripts/write-claude-rules.js +110 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { readManifest } from '../infrastructure/manifest.mjs';
|
|
5
|
+
import { baselineStatus, WAITING_FOR_INITIAL_INDEX } from '../infrastructure/baseline-readiness.mjs';
|
|
6
|
+
import { canonicaliseInsideRoot } from '../infrastructure/dirty-set.mjs';
|
|
7
|
+
import { contentHashSync } from '../infrastructure/hashing.mjs';
|
|
8
|
+
import {
|
|
9
|
+
reconcileEnablement,
|
|
10
|
+
startupInterval,
|
|
11
|
+
tierForHardware,
|
|
12
|
+
} from '../domain/interval-autotune.mjs';
|
|
13
|
+
import { detectHardwareCapability } from '../../infrastructure/hardware-capability.js';
|
|
14
|
+
import {
|
|
15
|
+
DEAD_LETTER_FILENAME,
|
|
16
|
+
QUEUE_FILENAME,
|
|
17
|
+
enqueueMaintenanceJob,
|
|
18
|
+
readMaintenanceQueue,
|
|
19
|
+
} from './maintenance-worker.mjs';
|
|
20
|
+
|
|
21
|
+
const MAINTAINER_LOCK = 'index-maintainer.lock';
|
|
22
|
+
|
|
23
|
+
const DIRTY_QUEUE = 'index-maintainer-queue.jsonl';
|
|
24
|
+
const PROCESSING_QUEUE = 'index-maintainer-queue.processing.jsonl';
|
|
25
|
+
const METRICS_FILE = 'reconcile-metrics.jsonl';
|
|
26
|
+
const MERKLE_STATE = 'merkle-state.json';
|
|
27
|
+
const PAUSE_FILE = 'reconcile-pause.json';
|
|
28
|
+
|
|
29
|
+
const REBUILD_TIERS = new Map([
|
|
30
|
+
['hnsw', 'float_hnsw'],
|
|
31
|
+
['float_hnsw', 'float_hnsw'],
|
|
32
|
+
['float-hnsw', 'float_hnsw'],
|
|
33
|
+
['binary_hnsw', 'binary_hnsw'],
|
|
34
|
+
['binary-hnsw', 'binary_hnsw'],
|
|
35
|
+
['li', 'li_segment'],
|
|
36
|
+
['li_segment', 'li_segment'],
|
|
37
|
+
['li-segment', 'li_segment'],
|
|
38
|
+
['sparse', 'sparse_gram'],
|
|
39
|
+
['sparse_gram', 'sparse_gram'],
|
|
40
|
+
['sparse-gram', 'sparse_gram'],
|
|
41
|
+
['fts5', 'fts5'],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function parseOptions(args) {
|
|
45
|
+
const positional = [];
|
|
46
|
+
const opts = { json: false };
|
|
47
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
48
|
+
const arg = args[i];
|
|
49
|
+
if (arg === '--json') {
|
|
50
|
+
opts.json = true;
|
|
51
|
+
} else if (arg === '--project-root') {
|
|
52
|
+
opts.projectRoot = args[++i];
|
|
53
|
+
} else if (arg.startsWith('--project-root=')) {
|
|
54
|
+
opts.projectRoot = arg.slice('--project-root='.length);
|
|
55
|
+
} else if (arg === '--state-dir') {
|
|
56
|
+
opts.stateDir = args[++i];
|
|
57
|
+
} else if (arg.startsWith('--state-dir=')) {
|
|
58
|
+
opts.stateDir = arg.slice('--state-dir='.length);
|
|
59
|
+
} else if (arg === '--add') {
|
|
60
|
+
opts.add = args[++i];
|
|
61
|
+
} else if (arg.startsWith('--add=')) {
|
|
62
|
+
opts.add = arg.slice('--add='.length);
|
|
63
|
+
} else {
|
|
64
|
+
positional.push(arg);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { positional, opts };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function context(opts) {
|
|
71
|
+
const requestedProjectRoot = path.resolve(
|
|
72
|
+
opts.projectRoot || process.env.SWEET_SEARCH_PROJECT_ROOT || process.cwd(),
|
|
73
|
+
);
|
|
74
|
+
const projectRoot = fs.existsSync(requestedProjectRoot)
|
|
75
|
+
? fs.realpathSync.native(requestedProjectRoot)
|
|
76
|
+
: requestedProjectRoot;
|
|
77
|
+
const stateDir = path.resolve(
|
|
78
|
+
opts.stateDir || process.env.SWEET_SEARCH_STATE_DIR || path.join(projectRoot, '.sweet-search'),
|
|
79
|
+
);
|
|
80
|
+
return { projectRoot, stateDir };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readJson(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readJsonl(filePath) {
|
|
92
|
+
if (!fs.existsSync(filePath)) return { items: [], malformed: 0 };
|
|
93
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter((line) => line.trim());
|
|
94
|
+
const items = [];
|
|
95
|
+
let malformed = 0;
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
try {
|
|
98
|
+
items.push(JSON.parse(line));
|
|
99
|
+
} catch {
|
|
100
|
+
malformed += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { items, malformed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function lastJsonl(filePath, n = 5) {
|
|
107
|
+
const { items } = readJsonl(filePath);
|
|
108
|
+
return items.slice(-n);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function dirtySnapshot(stateDir) {
|
|
112
|
+
const pending = readJsonl(path.join(stateDir, DIRTY_QUEUE));
|
|
113
|
+
const processing = readJsonl(path.join(stateDir, PROCESSING_QUEUE));
|
|
114
|
+
return {
|
|
115
|
+
pending: pending.items.length,
|
|
116
|
+
processing: processing.items.length,
|
|
117
|
+
malformed: pending.malformed + processing.malformed,
|
|
118
|
+
entries: [...pending.items, ...processing.items],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function deadLetterSnapshot(stateDir) {
|
|
123
|
+
return readJsonl(path.join(stateDir, DEAD_LETTER_FILENAME));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether a process id is currently alive. EPERM (a live process owned by
|
|
128
|
+
* another user) counts as alive; ESRCH (no such process) counts as dead.
|
|
129
|
+
*/
|
|
130
|
+
function pidAlive(pid) {
|
|
131
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
132
|
+
try {
|
|
133
|
+
process.kill(pid, 0);
|
|
134
|
+
return true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return err.code === 'EPERM';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reconcile-v2 enablement + resolved startup interval, for the operator
|
|
142
|
+
* `status` surface. Reads the same domain policy + hardware-tier logic the
|
|
143
|
+
* daemon uses so `status` reflects what a fresh daemon would actually do.
|
|
144
|
+
*/
|
|
145
|
+
function reconcileConfigSnapshot(env = process.env) {
|
|
146
|
+
const enablement = reconcileEnablement(env);
|
|
147
|
+
let hardware = null;
|
|
148
|
+
try {
|
|
149
|
+
hardware = detectHardwareCapability();
|
|
150
|
+
} catch {
|
|
151
|
+
hardware = null;
|
|
152
|
+
}
|
|
153
|
+
const tier = hardware ? tierForHardware(hardware) : null;
|
|
154
|
+
const interval = startupInterval({ tier: tier || undefined, env, hardware });
|
|
155
|
+
return {
|
|
156
|
+
enabled: enablement.enabled,
|
|
157
|
+
source: enablement.source,
|
|
158
|
+
disabledReason: enablement.enabled
|
|
159
|
+
? null
|
|
160
|
+
: `SWEET_SEARCH_RECONCILE_V2=${enablement.raw}`,
|
|
161
|
+
interval: {
|
|
162
|
+
ms: interval.intervalMs,
|
|
163
|
+
source: interval.source,
|
|
164
|
+
tier,
|
|
165
|
+
pinned: interval.pinned,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Maintainer lock state for the state dir: whether a daemon currently holds
|
|
172
|
+
* the lock, its pid, and whether that pid is alive (a present-but-dead lock
|
|
173
|
+
* is stale and will be cleared on the next daemon start).
|
|
174
|
+
*/
|
|
175
|
+
function lockSnapshot(stateDir) {
|
|
176
|
+
const lockFile = path.join(stateDir, MAINTAINER_LOCK);
|
|
177
|
+
const payload = readJson(lockFile);
|
|
178
|
+
if (!payload || !Number.isInteger(payload.pid)) {
|
|
179
|
+
return { present: false, filePath: lockFile };
|
|
180
|
+
}
|
|
181
|
+
const alive = pidAlive(payload.pid);
|
|
182
|
+
return {
|
|
183
|
+
present: true,
|
|
184
|
+
filePath: lockFile,
|
|
185
|
+
pid: payload.pid,
|
|
186
|
+
alive,
|
|
187
|
+
stale: !alive,
|
|
188
|
+
startedAt: payload.timestamp ? new Date(payload.timestamp).toISOString() : null,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function statusSnapshot(ctx) {
|
|
193
|
+
const manifest = readManifest(ctx.stateDir);
|
|
194
|
+
const dirty = dirtySnapshot(ctx.stateDir);
|
|
195
|
+
const rebuildJobs = readMaintenanceQueue(ctx.stateDir);
|
|
196
|
+
const dead = deadLetterSnapshot(ctx.stateDir);
|
|
197
|
+
const lastTicks = lastJsonl(path.join(ctx.stateDir, METRICS_FILE), 5);
|
|
198
|
+
const pause = pauseSnapshot(ctx);
|
|
199
|
+
const byTier = {};
|
|
200
|
+
for (const job of rebuildJobs) {
|
|
201
|
+
const tier = job.tier || 'unknown';
|
|
202
|
+
byTier[tier] = (byTier[tier] || 0) + 1;
|
|
203
|
+
}
|
|
204
|
+
const lastTick = lastTicks.at(-1) ?? null;
|
|
205
|
+
const baseline = baselineStatus(ctx.stateDir);
|
|
206
|
+
return {
|
|
207
|
+
projectRoot: ctx.projectRoot,
|
|
208
|
+
stateDir: ctx.stateDir,
|
|
209
|
+
reconcile: reconcileConfigSnapshot(),
|
|
210
|
+
baseline,
|
|
211
|
+
lock: lockSnapshot(ctx.stateDir),
|
|
212
|
+
manifest: manifest
|
|
213
|
+
? { present: true, epoch: manifest.epoch ?? 0, publishedAt: manifest.publishedAt ?? null }
|
|
214
|
+
: { present: false, epoch: 0, publishedAt: null },
|
|
215
|
+
dirty: {
|
|
216
|
+
pending: dirty.pending,
|
|
217
|
+
processing: dirty.processing,
|
|
218
|
+
malformed: dirty.malformed,
|
|
219
|
+
},
|
|
220
|
+
pause,
|
|
221
|
+
rebuild: {
|
|
222
|
+
pending: rebuildJobs.length,
|
|
223
|
+
deadLetters: dead.items.length,
|
|
224
|
+
malformedDeadLetters: dead.malformed,
|
|
225
|
+
byTier,
|
|
226
|
+
},
|
|
227
|
+
metrics: {
|
|
228
|
+
lastTicks,
|
|
229
|
+
lastTickAt: lastTick?.published_at ?? lastTick?.publishedAt ?? lastTick?.ts ?? null,
|
|
230
|
+
lastError: lastTick?.last_error ?? null,
|
|
231
|
+
lastMaintenance: lastTick?.last_maintenance ?? null,
|
|
232
|
+
watermarks: lastTick?.watermarks ?? null,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function pauseSnapshot(ctx) {
|
|
238
|
+
const filePath = path.join(ctx.stateDir, PAUSE_FILE);
|
|
239
|
+
const payload = readJson(filePath);
|
|
240
|
+
if (!payload) return { paused: false, filePath };
|
|
241
|
+
return {
|
|
242
|
+
paused: true,
|
|
243
|
+
filePath,
|
|
244
|
+
pausedAt: payload.pausedAt ?? null,
|
|
245
|
+
reason: payload.reason ?? null,
|
|
246
|
+
pid: payload.pid ?? null,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function normalizeRel(p) {
|
|
251
|
+
return p.replace(/\\/g, '/');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function queueMatches(entries, absPath, relPath) {
|
|
255
|
+
const candidates = new Set([normalizeRel(absPath), normalizeRel(relPath)]);
|
|
256
|
+
return entries.filter((entry) => {
|
|
257
|
+
const value = entry.file_path || entry.path || entry.filePath;
|
|
258
|
+
return value && candidates.has(normalizeRel(value));
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function fileStatTuple(filePath) {
|
|
263
|
+
try {
|
|
264
|
+
const stat = fs.statSync(filePath, { bigint: true });
|
|
265
|
+
return {
|
|
266
|
+
size: stat.size.toString(),
|
|
267
|
+
mtime_ns: stat.mtimeNs.toString(),
|
|
268
|
+
inode: stat.ino.toString(),
|
|
269
|
+
};
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function statDiff(stored, current) {
|
|
276
|
+
if (!stored || !current) return null;
|
|
277
|
+
return {
|
|
278
|
+
size: String(stored.size ?? '') !== current.size,
|
|
279
|
+
mtime_ns: String(stored.mtime_ns ?? '') !== current.mtime_ns,
|
|
280
|
+
inode: stored.inode == null ? true : String(stored.inode) !== current.inode,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function inspectPath(ctx, inputPath) {
|
|
285
|
+
if (!inputPath) throw new Error('reconcile inspect requires a path');
|
|
286
|
+
const canonicalPath = canonicaliseInsideRoot(ctx.projectRoot, inputPath);
|
|
287
|
+
if (!canonicalPath) throw new Error(`path is outside project root: ${inputPath}`);
|
|
288
|
+
const relPath = normalizeRel(path.relative(ctx.projectRoot, canonicalPath));
|
|
289
|
+
const dirty = dirtySnapshot(ctx.stateDir);
|
|
290
|
+
const manifest = readManifest(ctx.stateDir);
|
|
291
|
+
const merkle = readJson(path.join(ctx.stateDir, MERKLE_STATE)) || {};
|
|
292
|
+
const stored = merkle.files?.[relPath] || merkle.files?.[canonicalPath] || null;
|
|
293
|
+
const currentStat = fileStatTuple(canonicalPath);
|
|
294
|
+
let currentHash = null;
|
|
295
|
+
if (currentStat && fs.existsSync(canonicalPath)) {
|
|
296
|
+
currentHash = contentHashSync(fs.readFileSync(canonicalPath));
|
|
297
|
+
}
|
|
298
|
+
const queued = queueMatches(dirty.entries, canonicalPath, relPath);
|
|
299
|
+
const reasons = [];
|
|
300
|
+
if (queued.length > 0) reasons.push('queued');
|
|
301
|
+
if (!stored) reasons.push('not_tracked');
|
|
302
|
+
if (!currentStat) reasons.push('deleted');
|
|
303
|
+
if (stored?.hash && currentHash && stored.hash !== currentHash) reasons.push('hash_diff');
|
|
304
|
+
const tupleDiff = statDiff(stored, currentStat);
|
|
305
|
+
if (tupleDiff && Object.values(tupleDiff).some(Boolean)) reasons.push('stat_tuple_diff');
|
|
306
|
+
if (reasons.length === 0) reasons.push('clean');
|
|
307
|
+
return {
|
|
308
|
+
inputPath,
|
|
309
|
+
canonicalPath,
|
|
310
|
+
relativePath: relPath,
|
|
311
|
+
manifestEpoch: manifest?.epoch ?? 0,
|
|
312
|
+
lastIndex: merkle.lastIndex ?? null,
|
|
313
|
+
queued: queued.map((entry) => ({
|
|
314
|
+
file_path: entry.file_path || entry.path || entry.filePath,
|
|
315
|
+
timestamp: entry.timestamp ?? null,
|
|
316
|
+
queued_at: entry.queued_at ?? null,
|
|
317
|
+
retry: entry.retry ?? null,
|
|
318
|
+
})),
|
|
319
|
+
state: {
|
|
320
|
+
tracked: Boolean(stored),
|
|
321
|
+
storedHash: stored?.hash ?? null,
|
|
322
|
+
currentHash,
|
|
323
|
+
hashDiff: Boolean(stored?.hash && currentHash && stored.hash !== currentHash),
|
|
324
|
+
storedStat: stored ? {
|
|
325
|
+
size: stored.size ?? null,
|
|
326
|
+
mtime_ns: stored.mtime_ns ?? null,
|
|
327
|
+
inode: stored.inode ?? null,
|
|
328
|
+
} : null,
|
|
329
|
+
currentStat,
|
|
330
|
+
statDiff: tupleDiff,
|
|
331
|
+
},
|
|
332
|
+
reasons,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function resetDirtySet(ctx) {
|
|
337
|
+
const files = [DIRTY_QUEUE, PROCESSING_QUEUE].map((name) => path.join(ctx.stateDir, name));
|
|
338
|
+
let removed = 0;
|
|
339
|
+
for (const file of files) {
|
|
340
|
+
try {
|
|
341
|
+
fs.unlinkSync(file);
|
|
342
|
+
removed += 1;
|
|
343
|
+
} catch (err) {
|
|
344
|
+
if (err.code !== 'ENOENT') throw err;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { stateDir: ctx.stateDir, removedQueueFiles: removed };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function pauseReconcile(ctx) {
|
|
351
|
+
fs.mkdirSync(ctx.stateDir, { recursive: true });
|
|
352
|
+
const filePath = path.join(ctx.stateDir, PAUSE_FILE);
|
|
353
|
+
const payload = {
|
|
354
|
+
paused: true,
|
|
355
|
+
pausedAt: new Date().toISOString(),
|
|
356
|
+
pid: process.pid,
|
|
357
|
+
};
|
|
358
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
359
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
360
|
+
fs.renameSync(tmp, filePath);
|
|
361
|
+
return { stateDir: ctx.stateDir, pause: pauseSnapshot(ctx) };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function resumeReconcile(ctx) {
|
|
365
|
+
const filePath = path.join(ctx.stateDir, PAUSE_FILE);
|
|
366
|
+
let removed = false;
|
|
367
|
+
try {
|
|
368
|
+
fs.unlinkSync(filePath);
|
|
369
|
+
removed = true;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (err.code !== 'ENOENT') throw err;
|
|
372
|
+
}
|
|
373
|
+
return { stateDir: ctx.stateDir, removed, pause: pauseSnapshot(ctx) };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function addDirtyHint(ctx, inputPath) {
|
|
377
|
+
if (!inputPath) throw new Error('index --add requires a path');
|
|
378
|
+
const canonicalPath = canonicaliseInsideRoot(ctx.projectRoot, inputPath);
|
|
379
|
+
if (!canonicalPath) throw new Error(`path is outside project root: ${inputPath}`);
|
|
380
|
+
const relativePath = normalizeRel(path.relative(ctx.projectRoot, canonicalPath));
|
|
381
|
+
fs.mkdirSync(ctx.stateDir, { recursive: true });
|
|
382
|
+
const queueFile = path.join(ctx.stateDir, DIRTY_QUEUE);
|
|
383
|
+
const entry = {
|
|
384
|
+
file_path: relativePath,
|
|
385
|
+
timestamp: Date.now(),
|
|
386
|
+
queued_at: new Date().toISOString(),
|
|
387
|
+
source: 'cli',
|
|
388
|
+
};
|
|
389
|
+
fs.appendFileSync(queueFile, `${JSON.stringify(entry)}\n`);
|
|
390
|
+
return {
|
|
391
|
+
stateDir: ctx.stateDir,
|
|
392
|
+
queueFile,
|
|
393
|
+
inputPath,
|
|
394
|
+
canonicalPath,
|
|
395
|
+
relativePath,
|
|
396
|
+
entry,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function preserveJsonStdout(json, fn) {
|
|
401
|
+
if (!json) return fn();
|
|
402
|
+
const originalLog = console.log;
|
|
403
|
+
console.log = (...args) => console.error(...args);
|
|
404
|
+
try {
|
|
405
|
+
return await fn();
|
|
406
|
+
} finally {
|
|
407
|
+
console.log = originalLog;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function print(payload, json) {
|
|
412
|
+
if (json) {
|
|
413
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (payload.kind === 'status') {
|
|
417
|
+
if (payload.reconcile) {
|
|
418
|
+
const r = payload.reconcile;
|
|
419
|
+
console.log(`reconcile v2: ${r.enabled ? 'enabled' : 'disabled'} (${r.source}) interval: ${r.interval.ms}ms (${r.interval.source})`);
|
|
420
|
+
if (!r.enabled && r.disabledReason) console.log(`disabled reason: ${r.disabledReason}`);
|
|
421
|
+
}
|
|
422
|
+
if (payload.baseline && !payload.baseline.ready) {
|
|
423
|
+
console.log(`baseline: ${WAITING_FOR_INITIAL_INDEX} (${payload.baseline.reason}) — run "sweet-search index" first`);
|
|
424
|
+
}
|
|
425
|
+
console.log(`index epoch: ${payload.manifest.epoch} dirty files: ${payload.dirty.pending + payload.dirty.processing} rebuild backlog: ${payload.rebuild.pending}`);
|
|
426
|
+
if (payload.lock?.present) {
|
|
427
|
+
console.log(`maintainer lock: pid ${payload.lock.pid} (${payload.lock.alive ? 'alive' : 'stale'})`);
|
|
428
|
+
}
|
|
429
|
+
if (payload.pause?.paused) console.log(`reconcile paused since ${payload.pause.pausedAt || 'unknown'}`);
|
|
430
|
+
if (payload.rebuild.deadLetters > 0) console.log(`dead letters: ${payload.rebuild.deadLetters}`);
|
|
431
|
+
if (payload.metrics.lastError) console.log(`last error: ${payload.metrics.lastError}`);
|
|
432
|
+
if (payload.metrics.lastTicks.length > 0) console.log(`last ticks: ${payload.metrics.lastTicks.length}`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (payload.kind === 'inspect') {
|
|
436
|
+
console.log(`${payload.relativePath}: ${payload.reasons.join(', ')}`);
|
|
437
|
+
console.log(`manifest epoch: ${payload.manifestEpoch}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (payload.kind === 'reset') {
|
|
441
|
+
console.log(`reset dirty set (${payload.removedQueueFiles} queue file(s) removed)`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (payload.kind === 'pause') {
|
|
445
|
+
console.log(`reconcile paused (${payload.stateDir})`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (payload.kind === 'resume') {
|
|
449
|
+
console.log(payload.removed ? 'reconcile resumed' : 'reconcile was not paused');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (payload.kind === 'index-add') {
|
|
453
|
+
console.log(`queued ${payload.relativePath}`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (payload.kind === 'tick') {
|
|
457
|
+
if (payload.skipped) {
|
|
458
|
+
console.log(`reconcile tick skipped: ${WAITING_FOR_INITIAL_INDEX} (${payload.baseline?.reason ?? 'no-baseline'}) — run "sweet-search index" first`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
console.log(`reconcile tick epoch ${payload.counters?.epoch ?? 'unknown'} (${payload.counters?.files_processed ?? 0} file(s) processed)`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (payload.kind === 'rebuild-status') {
|
|
465
|
+
console.log(`rebuild backlog: ${payload.rebuild.pending} dead letters: ${payload.rebuild.deadLetters}`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (payload.kind === 'rebuild-force') {
|
|
469
|
+
console.log(`queued ${payload.job.tier} maintenance (${payload.job.reason})`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function handleIncrementalCli(command, args) {
|
|
475
|
+
const { positional, opts } = parseOptions(args);
|
|
476
|
+
const ctx = context(opts);
|
|
477
|
+
const sub = positional[0];
|
|
478
|
+
|
|
479
|
+
if (command === 'reconcile') {
|
|
480
|
+
if (sub === 'status') {
|
|
481
|
+
return print({ kind: 'status', ...statusSnapshot(ctx) }, opts.json);
|
|
482
|
+
}
|
|
483
|
+
if (sub === 'inspect') {
|
|
484
|
+
return print({ kind: 'inspect', ...inspectPath(ctx, positional[1]) }, opts.json);
|
|
485
|
+
}
|
|
486
|
+
if (sub === 'reset') {
|
|
487
|
+
return print({ kind: 'reset', ...resetDirtySet(ctx) }, opts.json);
|
|
488
|
+
}
|
|
489
|
+
if (sub === 'pause') {
|
|
490
|
+
return print({ kind: 'pause', ...pauseReconcile(ctx) }, opts.json);
|
|
491
|
+
}
|
|
492
|
+
if (sub === 'resume') {
|
|
493
|
+
return print({ kind: 'resume', ...resumeReconcile(ctx) }, opts.json);
|
|
494
|
+
}
|
|
495
|
+
if (sub === 'tick') {
|
|
496
|
+
// Baseline gate: a reconcile tick must not be the first index builder.
|
|
497
|
+
// Refuse (without touching artifacts) until `sweet-search index` lands a
|
|
498
|
+
// complete baseline — mirrors the default-on maintainer's dormancy.
|
|
499
|
+
const baseline = baselineStatus(ctx.stateDir);
|
|
500
|
+
if (!baseline.ready) {
|
|
501
|
+
return print({
|
|
502
|
+
kind: 'tick',
|
|
503
|
+
ok: false,
|
|
504
|
+
skipped: true,
|
|
505
|
+
reason: WAITING_FOR_INITIAL_INDEX,
|
|
506
|
+
baseline,
|
|
507
|
+
projectRoot: ctx.projectRoot,
|
|
508
|
+
stateDir: ctx.stateDir,
|
|
509
|
+
}, opts.json);
|
|
510
|
+
}
|
|
511
|
+
const counters = await preserveJsonStdout(opts.json, async () => {
|
|
512
|
+
const { runProductionReconcileTick } = await import('./production-reconciler.mjs');
|
|
513
|
+
return runProductionReconcileTick({
|
|
514
|
+
projectRoot: ctx.projectRoot,
|
|
515
|
+
stateDir: ctx.stateDir,
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
return print({ kind: 'tick', ok: true, projectRoot: ctx.projectRoot, stateDir: ctx.stateDir, counters }, opts.json);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (command === 'rebuild') {
|
|
523
|
+
if (sub === 'status') {
|
|
524
|
+
return print({ kind: 'rebuild-status', ...statusSnapshot(ctx) }, opts.json);
|
|
525
|
+
}
|
|
526
|
+
if (sub === 'force') {
|
|
527
|
+
const tier = REBUILD_TIERS.get(positional[1] || '');
|
|
528
|
+
if (!tier) throw new Error(`unknown rebuild tier "${positional[1] || ''}"`);
|
|
529
|
+
const manifest = readManifest(ctx.stateDir);
|
|
530
|
+
const job = {
|
|
531
|
+
tier,
|
|
532
|
+
reason: 'operator_force',
|
|
533
|
+
epoch: manifest?.epoch ?? 0,
|
|
534
|
+
payload: {},
|
|
535
|
+
};
|
|
536
|
+
enqueueMaintenanceJob(ctx.stateDir, job);
|
|
537
|
+
return print({ kind: 'rebuild-force', stateDir: ctx.stateDir, job }, opts.json);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
throw new Error(`unknown ${command} command "${sub || ''}"`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function handleIndexAddCli(args) {
|
|
545
|
+
const { opts } = parseOptions(args);
|
|
546
|
+
const ctx = context(opts);
|
|
547
|
+
return print({ kind: 'index-add', ...addDirtyHint(ctx, opts.add) }, opts.json);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export const __testing = {
|
|
551
|
+
PAUSE_FILE,
|
|
552
|
+
addDirtyHint,
|
|
553
|
+
pauseSnapshot,
|
|
554
|
+
};
|