@karmaniverous/jeeves-watcher 0.9.4 → 0.9.6
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/config.schema.json +29 -15
- package/dist/cli/jeeves-watcher/index.js +121 -16
- package/dist/index.d.ts +11 -0
- package/dist/index.js +122 -17
- package/package.json +1 -1
package/config.schema.json
CHANGED
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"description": "Named Qdrant filter patterns for skill-activated behaviors.",
|
|
190
190
|
"allOf": [
|
|
191
191
|
{
|
|
192
|
-
"$ref": "#/definitions/
|
|
192
|
+
"$ref": "#/definitions/__schema83"
|
|
193
193
|
}
|
|
194
194
|
]
|
|
195
195
|
},
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
"description": "Search configuration including score thresholds and hybrid search.",
|
|
198
198
|
"allOf": [
|
|
199
199
|
{
|
|
200
|
-
"$ref": "#/definitions/
|
|
200
|
+
"$ref": "#/definitions/__schema84"
|
|
201
201
|
}
|
|
202
202
|
]
|
|
203
203
|
},
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
"description": "Logging configuration.",
|
|
206
206
|
"allOf": [
|
|
207
207
|
{
|
|
208
|
-
"$ref": "#/definitions/
|
|
208
|
+
"$ref": "#/definitions/__schema85"
|
|
209
209
|
}
|
|
210
210
|
]
|
|
211
211
|
},
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"description": "Timeout in milliseconds for graceful shutdown.",
|
|
214
214
|
"allOf": [
|
|
215
215
|
{
|
|
216
|
-
"$ref": "#/definitions/
|
|
216
|
+
"$ref": "#/definitions/__schema88"
|
|
217
217
|
}
|
|
218
218
|
]
|
|
219
219
|
},
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
"description": "Maximum consecutive system-level failures before triggering fatal error. Default: Infinity.",
|
|
222
222
|
"allOf": [
|
|
223
223
|
{
|
|
224
|
-
"$ref": "#/definitions/
|
|
224
|
+
"$ref": "#/definitions/__schema89"
|
|
225
225
|
}
|
|
226
226
|
]
|
|
227
227
|
},
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
"description": "Maximum backoff delay in milliseconds for system errors. Default: 60000.",
|
|
230
230
|
"allOf": [
|
|
231
231
|
{
|
|
232
|
-
"$ref": "#/definitions/
|
|
232
|
+
"$ref": "#/definitions/__schema90"
|
|
233
233
|
}
|
|
234
234
|
]
|
|
235
235
|
}
|
|
@@ -807,7 +807,7 @@
|
|
|
807
807
|
"items": {
|
|
808
808
|
"$ref": "#/definitions/__schema66"
|
|
809
809
|
},
|
|
810
|
-
"description": "Keys
|
|
810
|
+
"description": "Keys or glob patterns to include as YAML frontmatter. Supports picomatch globs (e.g. \"*\") and \"!\"-prefixed exclusion patterns (e.g. \"!_*\"). Explicit names preserve declaration order; glob-matched keys are sorted alphabetically."
|
|
811
811
|
},
|
|
812
812
|
"body": {
|
|
813
813
|
"type": "array",
|
|
@@ -1046,17 +1046,31 @@
|
|
|
1046
1046
|
"callbackUrl": {
|
|
1047
1047
|
"type": "string",
|
|
1048
1048
|
"format": "uri"
|
|
1049
|
+
},
|
|
1050
|
+
"concurrency": {
|
|
1051
|
+
"default": 50,
|
|
1052
|
+
"description": "Maximum concurrent file operations during reindex (default 50).",
|
|
1053
|
+
"allOf": [
|
|
1054
|
+
{
|
|
1055
|
+
"$ref": "#/definitions/__schema82"
|
|
1056
|
+
}
|
|
1057
|
+
]
|
|
1049
1058
|
}
|
|
1050
1059
|
}
|
|
1051
1060
|
},
|
|
1052
1061
|
"__schema82": {
|
|
1062
|
+
"type": "integer",
|
|
1063
|
+
"minimum": 1,
|
|
1064
|
+
"maximum": 9007199254740991
|
|
1065
|
+
},
|
|
1066
|
+
"__schema83": {
|
|
1053
1067
|
"type": "object",
|
|
1054
1068
|
"propertyNames": {
|
|
1055
1069
|
"type": "string"
|
|
1056
1070
|
},
|
|
1057
1071
|
"additionalProperties": {}
|
|
1058
1072
|
},
|
|
1059
|
-
"
|
|
1073
|
+
"__schema84": {
|
|
1060
1074
|
"type": "object",
|
|
1061
1075
|
"properties": {
|
|
1062
1076
|
"scoreThresholds": {
|
|
@@ -1101,14 +1115,14 @@
|
|
|
1101
1115
|
}
|
|
1102
1116
|
}
|
|
1103
1117
|
},
|
|
1104
|
-
"
|
|
1118
|
+
"__schema85": {
|
|
1105
1119
|
"type": "object",
|
|
1106
1120
|
"properties": {
|
|
1107
1121
|
"level": {
|
|
1108
1122
|
"description": "Logging level (trace, debug, info, warn, error, fatal).",
|
|
1109
1123
|
"allOf": [
|
|
1110
1124
|
{
|
|
1111
|
-
"$ref": "#/definitions/
|
|
1125
|
+
"$ref": "#/definitions/__schema86"
|
|
1112
1126
|
}
|
|
1113
1127
|
]
|
|
1114
1128
|
},
|
|
@@ -1116,26 +1130,26 @@
|
|
|
1116
1130
|
"description": "Path to log file (logs to stdout if omitted).",
|
|
1117
1131
|
"allOf": [
|
|
1118
1132
|
{
|
|
1119
|
-
"$ref": "#/definitions/
|
|
1133
|
+
"$ref": "#/definitions/__schema87"
|
|
1120
1134
|
}
|
|
1121
1135
|
]
|
|
1122
1136
|
}
|
|
1123
1137
|
}
|
|
1124
1138
|
},
|
|
1125
|
-
"__schema85": {
|
|
1126
|
-
"type": "string"
|
|
1127
|
-
},
|
|
1128
1139
|
"__schema86": {
|
|
1129
1140
|
"type": "string"
|
|
1130
1141
|
},
|
|
1131
1142
|
"__schema87": {
|
|
1132
|
-
"type": "
|
|
1143
|
+
"type": "string"
|
|
1133
1144
|
},
|
|
1134
1145
|
"__schema88": {
|
|
1135
1146
|
"type": "number"
|
|
1136
1147
|
},
|
|
1137
1148
|
"__schema89": {
|
|
1138
1149
|
"type": "number"
|
|
1150
|
+
},
|
|
1151
|
+
"__schema90": {
|
|
1152
|
+
"type": "number"
|
|
1139
1153
|
}
|
|
1140
1154
|
}
|
|
1141
1155
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { Command } from '@commander-js/extra-typings';
|
|
4
|
+
import { parallel, capitalize, title, camel, snake, dash, isEqual, get, omit } from 'radash';
|
|
4
5
|
import { readdir, stat, writeFile, rm, readFile, mkdir } from 'node:fs/promises';
|
|
5
6
|
import { resolve, dirname, join, extname, basename, isAbsolute, relative } from 'node:path';
|
|
6
7
|
import picomatch from 'picomatch';
|
|
@@ -17,7 +18,6 @@ import dayjs from 'dayjs';
|
|
|
17
18
|
import { toMdast } from 'hast-util-to-mdast';
|
|
18
19
|
import { fromADF } from 'mdast-util-from-adf';
|
|
19
20
|
import { toMarkdown } from 'mdast-util-to-markdown';
|
|
20
|
-
import { capitalize, title, camel, snake, dash, isEqual, get, omit } from 'radash';
|
|
21
21
|
import rehypeParse from 'rehype-parse';
|
|
22
22
|
import { unified } from 'unified';
|
|
23
23
|
import yaml from 'js-yaml';
|
|
@@ -225,6 +225,8 @@ async function listFilesFromGlobs(patterns, ignored = []) {
|
|
|
225
225
|
*
|
|
226
226
|
* Shared helper for processing all files matching configured globs.
|
|
227
227
|
*/
|
|
228
|
+
/** Default concurrency limit for reindex operations. */
|
|
229
|
+
const DEFAULT_REINDEX_CONCURRENCY = 50;
|
|
228
230
|
/**
|
|
229
231
|
* Process all files from globs using the specified processor method.
|
|
230
232
|
*
|
|
@@ -232,15 +234,17 @@ async function listFilesFromGlobs(patterns, ignored = []) {
|
|
|
232
234
|
* @param ignoredPaths - The glob patterns to ignore.
|
|
233
235
|
* @param processor - The document processor instance.
|
|
234
236
|
* @param method - The processor method to call ('processFile' or 'processRulesUpdate').
|
|
237
|
+
* @param concurrency - Maximum concurrent file operations (default 50).
|
|
238
|
+
* @param callbacks - Optional progress tracking callbacks.
|
|
235
239
|
* @returns The number of files processed.
|
|
236
240
|
*/
|
|
237
|
-
async function processAllFiles(watchPaths, ignoredPaths, processor, method) {
|
|
241
|
+
async function processAllFiles(watchPaths, ignoredPaths, processor, method, concurrency = DEFAULT_REINDEX_CONCURRENCY, callbacks) {
|
|
238
242
|
const files = await listFilesFromGlobs(watchPaths, ignoredPaths);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// Queue integration can come later.
|
|
243
|
+
callbacks?.onTotal?.(files.length);
|
|
244
|
+
await parallel(concurrency, files, async (file) => {
|
|
242
245
|
await processor[method](file);
|
|
243
|
-
|
|
246
|
+
callbacks?.onFileProcessed?.();
|
|
247
|
+
});
|
|
244
248
|
return files.length;
|
|
245
249
|
}
|
|
246
250
|
|
|
@@ -289,8 +293,8 @@ async function executeReindex(deps, scope) {
|
|
|
289
293
|
// Reprocess only files with issues
|
|
290
294
|
const issues = deps.issuesManager.getAll();
|
|
291
295
|
const issuePaths = Object.keys(issues);
|
|
292
|
-
|
|
293
|
-
|
|
296
|
+
const concurrency = config.reindex?.concurrency ?? 50;
|
|
297
|
+
await parallel(concurrency, issuePaths, async (filePath) => {
|
|
294
298
|
try {
|
|
295
299
|
await processor.processFile(filePath);
|
|
296
300
|
filesProcessed++;
|
|
@@ -299,11 +303,15 @@ async function executeReindex(deps, scope) {
|
|
|
299
303
|
errors++;
|
|
300
304
|
logger.warn({ filePath, err: normalizeError(error) }, 'Failed to reprocess issue file');
|
|
301
305
|
}
|
|
302
|
-
}
|
|
306
|
+
});
|
|
303
307
|
}
|
|
304
308
|
else {
|
|
305
309
|
// Full reindex - process all watched files
|
|
306
|
-
|
|
310
|
+
const concurrency = config.reindex?.concurrency ?? 50;
|
|
311
|
+
filesProcessed = await processAllFiles(config.watch.paths, config.watch.ignored, processor, 'processFile', concurrency, {
|
|
312
|
+
onTotal: (total) => reindexTracker?.setTotal(total),
|
|
313
|
+
onFileProcessed: () => reindexTracker?.incrementProcessed(),
|
|
314
|
+
});
|
|
307
315
|
}
|
|
308
316
|
const durationMs = Date.now() - startTime;
|
|
309
317
|
logger.info({ scope, filesProcessed, durationMs }, `Reindex (${scope}) completed`);
|
|
@@ -1049,6 +1057,72 @@ function rebaseHeadings(markdown, baseHeading) {
|
|
|
1049
1057
|
return rebased.join('\n');
|
|
1050
1058
|
}
|
|
1051
1059
|
|
|
1060
|
+
/**
|
|
1061
|
+
* @module templates/resolveFrontmatterKeys
|
|
1062
|
+
* Resolves frontmatter key patterns (with glob/negation support) against
|
|
1063
|
+
* available context keys. Patterns prefixed with `!` are exclusions.
|
|
1064
|
+
* Supports picomatch glob syntax (e.g. `*`, `_*`, `chunk_*`).
|
|
1065
|
+
*/
|
|
1066
|
+
/**
|
|
1067
|
+
* Resolve frontmatter patterns against available keys.
|
|
1068
|
+
*
|
|
1069
|
+
* @param patterns - Array of key names or glob patterns. `!`-prefixed patterns exclude.
|
|
1070
|
+
* @param allKeys - All available keys from the rendering context.
|
|
1071
|
+
* @returns Ordered array of resolved keys: explicit names in declaration order,
|
|
1072
|
+
* then glob-matched names sorted alphabetically, minus exclusions.
|
|
1073
|
+
*/
|
|
1074
|
+
function resolveFrontmatterKeys(patterns, allKeys) {
|
|
1075
|
+
const includes = [];
|
|
1076
|
+
const excludePatterns = [];
|
|
1077
|
+
for (const p of patterns) {
|
|
1078
|
+
if (p.startsWith('!')) {
|
|
1079
|
+
excludePatterns.push(p.slice(1));
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
includes.push(p);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const isExcluded = excludePatterns.length
|
|
1086
|
+
? picomatch(excludePatterns)
|
|
1087
|
+
: () => false;
|
|
1088
|
+
// Collect keys: explicit names first (in order), then glob-expanded (sorted).
|
|
1089
|
+
const result = [];
|
|
1090
|
+
const seen = new Set();
|
|
1091
|
+
// Pass 1: explicit (non-glob) names — preserved in declaration order.
|
|
1092
|
+
const explicitNames = [];
|
|
1093
|
+
const globPatterns = [];
|
|
1094
|
+
for (const p of includes) {
|
|
1095
|
+
if (isGlob(p)) {
|
|
1096
|
+
globPatterns.push(p);
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
explicitNames.push(p);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
for (const name of explicitNames) {
|
|
1103
|
+
if (!isExcluded(name) && allKeys.includes(name) && !seen.has(name)) {
|
|
1104
|
+
result.push(name);
|
|
1105
|
+
seen.add(name);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// Pass 2: glob patterns — matched keys sorted alphabetically.
|
|
1109
|
+
if (globPatterns.length) {
|
|
1110
|
+
const isIncluded = picomatch(globPatterns);
|
|
1111
|
+
const matched = allKeys.filter((k) => isIncluded(k)).sort();
|
|
1112
|
+
for (const key of matched) {
|
|
1113
|
+
if (!isExcluded(key) && !seen.has(key)) {
|
|
1114
|
+
result.push(key);
|
|
1115
|
+
seen.add(key);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return result;
|
|
1120
|
+
}
|
|
1121
|
+
/** Check whether a pattern contains glob characters. */
|
|
1122
|
+
function isGlob(pattern) {
|
|
1123
|
+
return /[*?[\]{}]/.test(pattern);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1052
1126
|
/**
|
|
1053
1127
|
* @module templates/renderDoc
|
|
1054
1128
|
* Declarative renderer for YAML frontmatter + structured Markdown body.
|
|
@@ -1119,9 +1193,10 @@ function renderEach(hbs, section, value) {
|
|
|
1119
1193
|
*/
|
|
1120
1194
|
function renderDoc(context, config, hbs) {
|
|
1121
1195
|
const parts = [];
|
|
1122
|
-
// Frontmatter
|
|
1196
|
+
// Frontmatter — resolve patterns (globs, negations) against context keys.
|
|
1197
|
+
const resolvedKeys = resolveFrontmatterKeys(config.frontmatter, Object.keys(context));
|
|
1123
1198
|
const fmObj = {};
|
|
1124
|
-
for (const key of
|
|
1199
|
+
for (const key of resolvedKeys) {
|
|
1125
1200
|
const v = get(context, key);
|
|
1126
1201
|
if (v !== undefined) {
|
|
1127
1202
|
fmObj[key] = v;
|
|
@@ -1915,10 +1990,12 @@ const renderBodySectionSchema = z.object({
|
|
|
1915
1990
|
});
|
|
1916
1991
|
/** Render config: YAML frontmatter + ordered body sections. */
|
|
1917
1992
|
const renderConfigSchema = z.object({
|
|
1918
|
-
/** Keys to extract from context and include as YAML frontmatter. */
|
|
1993
|
+
/** Keys or glob patterns to extract from context and include as YAML frontmatter. */
|
|
1919
1994
|
frontmatter: z
|
|
1920
1995
|
.array(z.string().min(1))
|
|
1921
|
-
.describe('Keys
|
|
1996
|
+
.describe('Keys or glob patterns to include as YAML frontmatter. ' +
|
|
1997
|
+
'Supports picomatch globs (e.g. "*") and "!"-prefixed exclusion patterns (e.g. "!_*"). ' +
|
|
1998
|
+
'Explicit names preserve declaration order; glob-matched keys are sorted alphabetically.'),
|
|
1922
1999
|
/** Ordered markdown body sections. */
|
|
1923
2000
|
body: z
|
|
1924
2001
|
.array(renderBodySectionSchema)
|
|
@@ -2156,6 +2233,13 @@ const jeevesWatcherConfigSchema = z.object({
|
|
|
2156
2233
|
.object({
|
|
2157
2234
|
/** URL to call when reindex completes. */
|
|
2158
2235
|
callbackUrl: z.url().optional(),
|
|
2236
|
+
/** Maximum concurrent file operations during reindex. */
|
|
2237
|
+
concurrency: z
|
|
2238
|
+
.number()
|
|
2239
|
+
.int()
|
|
2240
|
+
.min(1)
|
|
2241
|
+
.default(50)
|
|
2242
|
+
.describe('Maximum concurrent file operations during reindex (default 50).'),
|
|
2159
2243
|
})
|
|
2160
2244
|
.optional()
|
|
2161
2245
|
.describe('Reindex configuration.'),
|
|
@@ -3209,7 +3293,7 @@ function createRebuildMetadataHandler(deps) {
|
|
|
3209
3293
|
*/
|
|
3210
3294
|
function createReindexHandler(deps) {
|
|
3211
3295
|
return wrapHandler(async (_request, reply) => {
|
|
3212
|
-
const count = await processAllFiles(deps.watch.paths, deps.watch.ignored, deps.processor, 'processFile');
|
|
3296
|
+
const count = await processAllFiles(deps.watch.paths, deps.watch.ignored, deps.processor, 'processFile', deps.concurrency);
|
|
3213
3297
|
return await reply.status(200).send({ ok: true, filesIndexed: count });
|
|
3214
3298
|
}, deps.logger, 'Reindex');
|
|
3215
3299
|
}
|
|
@@ -3544,17 +3628,31 @@ class ReindexTracker {
|
|
|
3544
3628
|
_active = false;
|
|
3545
3629
|
_scope;
|
|
3546
3630
|
_startedAt;
|
|
3631
|
+
_filesProcessed = 0;
|
|
3632
|
+
_totalFiles = 0;
|
|
3547
3633
|
/** Mark a reindex as started. */
|
|
3548
3634
|
start(scope) {
|
|
3549
3635
|
this._active = true;
|
|
3550
3636
|
this._scope = scope;
|
|
3551
3637
|
this._startedAt = new Date().toISOString();
|
|
3638
|
+
this._filesProcessed = 0;
|
|
3639
|
+
this._totalFiles = 0;
|
|
3640
|
+
}
|
|
3641
|
+
/** Set the total number of files to process. */
|
|
3642
|
+
setTotal(total) {
|
|
3643
|
+
this._totalFiles = total;
|
|
3644
|
+
}
|
|
3645
|
+
/** Increment the processed file count. */
|
|
3646
|
+
incrementProcessed() {
|
|
3647
|
+
this._filesProcessed++;
|
|
3552
3648
|
}
|
|
3553
3649
|
/** Mark the current reindex as complete. */
|
|
3554
3650
|
complete() {
|
|
3555
3651
|
this._active = false;
|
|
3556
3652
|
this._scope = undefined;
|
|
3557
3653
|
this._startedAt = undefined;
|
|
3654
|
+
this._filesProcessed = 0;
|
|
3655
|
+
this._totalFiles = 0;
|
|
3558
3656
|
}
|
|
3559
3657
|
/** Get current reindex status. */
|
|
3560
3658
|
getStatus() {
|
|
@@ -3564,6 +3662,8 @@ class ReindexTracker {
|
|
|
3564
3662
|
active: true,
|
|
3565
3663
|
scope: this._scope,
|
|
3566
3664
|
startedAt: this._startedAt,
|
|
3665
|
+
filesProcessed: this._filesProcessed,
|
|
3666
|
+
totalFiles: this._totalFiles,
|
|
3567
3667
|
};
|
|
3568
3668
|
}
|
|
3569
3669
|
}
|
|
@@ -3623,7 +3723,12 @@ function createApiServer(options) {
|
|
|
3623
3723
|
logger,
|
|
3624
3724
|
hybridConfig,
|
|
3625
3725
|
}));
|
|
3626
|
-
app.post('/reindex', createReindexHandler({
|
|
3726
|
+
app.post('/reindex', createReindexHandler({
|
|
3727
|
+
watch: config.watch,
|
|
3728
|
+
processor,
|
|
3729
|
+
logger,
|
|
3730
|
+
concurrency: config.reindex?.concurrency ?? 50,
|
|
3731
|
+
}));
|
|
3627
3732
|
app.post('/rebuild-metadata', createRebuildMetadataHandler({
|
|
3628
3733
|
metadataDir: config.metadataDir,
|
|
3629
3734
|
vectorStore,
|
package/dist/index.d.ts
CHANGED
|
@@ -186,6 +186,7 @@ declare const jeevesWatcherConfigSchema: z.ZodObject<{
|
|
|
186
186
|
}, z.core.$strip>>>;
|
|
187
187
|
reindex: z.ZodOptional<z.ZodObject<{
|
|
188
188
|
callbackUrl: z.ZodOptional<z.ZodURL>;
|
|
189
|
+
concurrency: z.ZodDefault<z.ZodNumber>;
|
|
189
190
|
}, z.core.$strip>>;
|
|
190
191
|
slots: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
191
192
|
search: z.ZodOptional<z.ZodObject<{
|
|
@@ -1240,6 +1241,10 @@ interface ReindexStatus {
|
|
|
1240
1241
|
scope?: string;
|
|
1241
1242
|
/** ISO 8601 timestamp when the current reindex started (when {@link active} is true). */
|
|
1242
1243
|
startedAt?: string;
|
|
1244
|
+
/** Number of files processed so far (when {@link active} is true). */
|
|
1245
|
+
filesProcessed?: number;
|
|
1246
|
+
/** Total number of files to process (when {@link active} is true). */
|
|
1247
|
+
totalFiles?: number;
|
|
1243
1248
|
}
|
|
1244
1249
|
/**
|
|
1245
1250
|
* Tracks the state of reindex operations.
|
|
@@ -1248,8 +1253,14 @@ declare class ReindexTracker {
|
|
|
1248
1253
|
private _active;
|
|
1249
1254
|
private _scope?;
|
|
1250
1255
|
private _startedAt?;
|
|
1256
|
+
private _filesProcessed;
|
|
1257
|
+
private _totalFiles;
|
|
1251
1258
|
/** Mark a reindex as started. */
|
|
1252
1259
|
start(scope: 'issues' | 'full'): void;
|
|
1260
|
+
/** Set the total number of files to process. */
|
|
1261
|
+
setTotal(total: number): void;
|
|
1262
|
+
/** Increment the processed file count. */
|
|
1263
|
+
incrementProcessed(): void;
|
|
1253
1264
|
/** Mark the current reindex as complete. */
|
|
1254
1265
|
complete(): void;
|
|
1255
1266
|
/** Get current reindex status. */
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,13 @@ import dayjs from 'dayjs';
|
|
|
8
8
|
import { toMdast } from 'hast-util-to-mdast';
|
|
9
9
|
import { fromADF } from 'mdast-util-from-adf';
|
|
10
10
|
import { toMarkdown } from 'mdast-util-to-markdown';
|
|
11
|
-
import { capitalize, title, camel, snake, dash, isEqual, get, omit } from 'radash';
|
|
11
|
+
import { capitalize, title, camel, snake, dash, isEqual, get, parallel, omit } from 'radash';
|
|
12
12
|
import rehypeParse from 'rehype-parse';
|
|
13
13
|
import { unified } from 'unified';
|
|
14
14
|
import yaml from 'js-yaml';
|
|
15
|
+
import picomatch from 'picomatch';
|
|
15
16
|
import Ajv from 'ajv';
|
|
16
17
|
import addFormats from 'ajv-formats';
|
|
17
|
-
import picomatch from 'picomatch';
|
|
18
18
|
import { readdir, stat, writeFile, rm, readFile, mkdir } from 'node:fs/promises';
|
|
19
19
|
import { z, ZodError } from 'zod';
|
|
20
20
|
import { JSONPath } from 'jsonpath-plus';
|
|
@@ -374,6 +374,72 @@ function rebaseHeadings(markdown, baseHeading) {
|
|
|
374
374
|
return rebased.join('\n');
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
/**
|
|
378
|
+
* @module templates/resolveFrontmatterKeys
|
|
379
|
+
* Resolves frontmatter key patterns (with glob/negation support) against
|
|
380
|
+
* available context keys. Patterns prefixed with `!` are exclusions.
|
|
381
|
+
* Supports picomatch glob syntax (e.g. `*`, `_*`, `chunk_*`).
|
|
382
|
+
*/
|
|
383
|
+
/**
|
|
384
|
+
* Resolve frontmatter patterns against available keys.
|
|
385
|
+
*
|
|
386
|
+
* @param patterns - Array of key names or glob patterns. `!`-prefixed patterns exclude.
|
|
387
|
+
* @param allKeys - All available keys from the rendering context.
|
|
388
|
+
* @returns Ordered array of resolved keys: explicit names in declaration order,
|
|
389
|
+
* then glob-matched names sorted alphabetically, minus exclusions.
|
|
390
|
+
*/
|
|
391
|
+
function resolveFrontmatterKeys(patterns, allKeys) {
|
|
392
|
+
const includes = [];
|
|
393
|
+
const excludePatterns = [];
|
|
394
|
+
for (const p of patterns) {
|
|
395
|
+
if (p.startsWith('!')) {
|
|
396
|
+
excludePatterns.push(p.slice(1));
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
includes.push(p);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const isExcluded = excludePatterns.length
|
|
403
|
+
? picomatch(excludePatterns)
|
|
404
|
+
: () => false;
|
|
405
|
+
// Collect keys: explicit names first (in order), then glob-expanded (sorted).
|
|
406
|
+
const result = [];
|
|
407
|
+
const seen = new Set();
|
|
408
|
+
// Pass 1: explicit (non-glob) names — preserved in declaration order.
|
|
409
|
+
const explicitNames = [];
|
|
410
|
+
const globPatterns = [];
|
|
411
|
+
for (const p of includes) {
|
|
412
|
+
if (isGlob(p)) {
|
|
413
|
+
globPatterns.push(p);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
explicitNames.push(p);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const name of explicitNames) {
|
|
420
|
+
if (!isExcluded(name) && allKeys.includes(name) && !seen.has(name)) {
|
|
421
|
+
result.push(name);
|
|
422
|
+
seen.add(name);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Pass 2: glob patterns — matched keys sorted alphabetically.
|
|
426
|
+
if (globPatterns.length) {
|
|
427
|
+
const isIncluded = picomatch(globPatterns);
|
|
428
|
+
const matched = allKeys.filter((k) => isIncluded(k)).sort();
|
|
429
|
+
for (const key of matched) {
|
|
430
|
+
if (!isExcluded(key) && !seen.has(key)) {
|
|
431
|
+
result.push(key);
|
|
432
|
+
seen.add(key);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
/** Check whether a pattern contains glob characters. */
|
|
439
|
+
function isGlob(pattern) {
|
|
440
|
+
return /[*?[\]{}]/.test(pattern);
|
|
441
|
+
}
|
|
442
|
+
|
|
377
443
|
/**
|
|
378
444
|
* @module templates/renderDoc
|
|
379
445
|
* Declarative renderer for YAML frontmatter + structured Markdown body.
|
|
@@ -444,9 +510,10 @@ function renderEach(hbs, section, value) {
|
|
|
444
510
|
*/
|
|
445
511
|
function renderDoc(context, config, hbs) {
|
|
446
512
|
const parts = [];
|
|
447
|
-
// Frontmatter
|
|
513
|
+
// Frontmatter — resolve patterns (globs, negations) against context keys.
|
|
514
|
+
const resolvedKeys = resolveFrontmatterKeys(config.frontmatter, Object.keys(context));
|
|
448
515
|
const fmObj = {};
|
|
449
|
-
for (const key of
|
|
516
|
+
for (const key of resolvedKeys) {
|
|
450
517
|
const v = get(context, key);
|
|
451
518
|
if (v !== undefined) {
|
|
452
519
|
fmObj[key] = v;
|
|
@@ -1317,6 +1384,8 @@ async function listFilesFromGlobs(patterns, ignored = []) {
|
|
|
1317
1384
|
*
|
|
1318
1385
|
* Shared helper for processing all files matching configured globs.
|
|
1319
1386
|
*/
|
|
1387
|
+
/** Default concurrency limit for reindex operations. */
|
|
1388
|
+
const DEFAULT_REINDEX_CONCURRENCY = 50;
|
|
1320
1389
|
/**
|
|
1321
1390
|
* Process all files from globs using the specified processor method.
|
|
1322
1391
|
*
|
|
@@ -1324,15 +1393,17 @@ async function listFilesFromGlobs(patterns, ignored = []) {
|
|
|
1324
1393
|
* @param ignoredPaths - The glob patterns to ignore.
|
|
1325
1394
|
* @param processor - The document processor instance.
|
|
1326
1395
|
* @param method - The processor method to call ('processFile' or 'processRulesUpdate').
|
|
1396
|
+
* @param concurrency - Maximum concurrent file operations (default 50).
|
|
1397
|
+
* @param callbacks - Optional progress tracking callbacks.
|
|
1327
1398
|
* @returns The number of files processed.
|
|
1328
1399
|
*/
|
|
1329
|
-
async function processAllFiles(watchPaths, ignoredPaths, processor, method) {
|
|
1400
|
+
async function processAllFiles(watchPaths, ignoredPaths, processor, method, concurrency = DEFAULT_REINDEX_CONCURRENCY, callbacks) {
|
|
1330
1401
|
const files = await listFilesFromGlobs(watchPaths, ignoredPaths);
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
// Queue integration can come later.
|
|
1402
|
+
callbacks?.onTotal?.(files.length);
|
|
1403
|
+
await parallel(concurrency, files, async (file) => {
|
|
1334
1404
|
await processor[method](file);
|
|
1335
|
-
|
|
1405
|
+
callbacks?.onFileProcessed?.();
|
|
1406
|
+
});
|
|
1336
1407
|
return files.length;
|
|
1337
1408
|
}
|
|
1338
1409
|
|
|
@@ -1381,8 +1452,8 @@ async function executeReindex(deps, scope) {
|
|
|
1381
1452
|
// Reprocess only files with issues
|
|
1382
1453
|
const issues = deps.issuesManager.getAll();
|
|
1383
1454
|
const issuePaths = Object.keys(issues);
|
|
1384
|
-
|
|
1385
|
-
|
|
1455
|
+
const concurrency = config.reindex?.concurrency ?? 50;
|
|
1456
|
+
await parallel(concurrency, issuePaths, async (filePath) => {
|
|
1386
1457
|
try {
|
|
1387
1458
|
await processor.processFile(filePath);
|
|
1388
1459
|
filesProcessed++;
|
|
@@ -1391,11 +1462,15 @@ async function executeReindex(deps, scope) {
|
|
|
1391
1462
|
errors++;
|
|
1392
1463
|
logger.warn({ filePath, err: normalizeError(error) }, 'Failed to reprocess issue file');
|
|
1393
1464
|
}
|
|
1394
|
-
}
|
|
1465
|
+
});
|
|
1395
1466
|
}
|
|
1396
1467
|
else {
|
|
1397
1468
|
// Full reindex - process all watched files
|
|
1398
|
-
|
|
1469
|
+
const concurrency = config.reindex?.concurrency ?? 50;
|
|
1470
|
+
filesProcessed = await processAllFiles(config.watch.paths, config.watch.ignored, processor, 'processFile', concurrency, {
|
|
1471
|
+
onTotal: (total) => reindexTracker?.setTotal(total),
|
|
1472
|
+
onFileProcessed: () => reindexTracker?.incrementProcessed(),
|
|
1473
|
+
});
|
|
1399
1474
|
}
|
|
1400
1475
|
const durationMs = Date.now() - startTime;
|
|
1401
1476
|
logger.info({ scope, filesProcessed, durationMs }, `Reindex (${scope}) completed`);
|
|
@@ -1601,10 +1676,12 @@ const renderBodySectionSchema = z.object({
|
|
|
1601
1676
|
});
|
|
1602
1677
|
/** Render config: YAML frontmatter + ordered body sections. */
|
|
1603
1678
|
const renderConfigSchema = z.object({
|
|
1604
|
-
/** Keys to extract from context and include as YAML frontmatter. */
|
|
1679
|
+
/** Keys or glob patterns to extract from context and include as YAML frontmatter. */
|
|
1605
1680
|
frontmatter: z
|
|
1606
1681
|
.array(z.string().min(1))
|
|
1607
|
-
.describe('Keys
|
|
1682
|
+
.describe('Keys or glob patterns to include as YAML frontmatter. ' +
|
|
1683
|
+
'Supports picomatch globs (e.g. "*") and "!"-prefixed exclusion patterns (e.g. "!_*"). ' +
|
|
1684
|
+
'Explicit names preserve declaration order; glob-matched keys are sorted alphabetically.'),
|
|
1608
1685
|
/** Ordered markdown body sections. */
|
|
1609
1686
|
body: z
|
|
1610
1687
|
.array(renderBodySectionSchema)
|
|
@@ -1842,6 +1919,13 @@ const jeevesWatcherConfigSchema = z.object({
|
|
|
1842
1919
|
.object({
|
|
1843
1920
|
/** URL to call when reindex completes. */
|
|
1844
1921
|
callbackUrl: z.url().optional(),
|
|
1922
|
+
/** Maximum concurrent file operations during reindex. */
|
|
1923
|
+
concurrency: z
|
|
1924
|
+
.number()
|
|
1925
|
+
.int()
|
|
1926
|
+
.min(1)
|
|
1927
|
+
.default(50)
|
|
1928
|
+
.describe('Maximum concurrent file operations during reindex (default 50).'),
|
|
1845
1929
|
})
|
|
1846
1930
|
.optional()
|
|
1847
1931
|
.describe('Reindex configuration.'),
|
|
@@ -2895,7 +2979,7 @@ function createRebuildMetadataHandler(deps) {
|
|
|
2895
2979
|
*/
|
|
2896
2980
|
function createReindexHandler(deps) {
|
|
2897
2981
|
return wrapHandler(async (_request, reply) => {
|
|
2898
|
-
const count = await processAllFiles(deps.watch.paths, deps.watch.ignored, deps.processor, 'processFile');
|
|
2982
|
+
const count = await processAllFiles(deps.watch.paths, deps.watch.ignored, deps.processor, 'processFile', deps.concurrency);
|
|
2899
2983
|
return await reply.status(200).send({ ok: true, filesIndexed: count });
|
|
2900
2984
|
}, deps.logger, 'Reindex');
|
|
2901
2985
|
}
|
|
@@ -3230,17 +3314,31 @@ class ReindexTracker {
|
|
|
3230
3314
|
_active = false;
|
|
3231
3315
|
_scope;
|
|
3232
3316
|
_startedAt;
|
|
3317
|
+
_filesProcessed = 0;
|
|
3318
|
+
_totalFiles = 0;
|
|
3233
3319
|
/** Mark a reindex as started. */
|
|
3234
3320
|
start(scope) {
|
|
3235
3321
|
this._active = true;
|
|
3236
3322
|
this._scope = scope;
|
|
3237
3323
|
this._startedAt = new Date().toISOString();
|
|
3324
|
+
this._filesProcessed = 0;
|
|
3325
|
+
this._totalFiles = 0;
|
|
3326
|
+
}
|
|
3327
|
+
/** Set the total number of files to process. */
|
|
3328
|
+
setTotal(total) {
|
|
3329
|
+
this._totalFiles = total;
|
|
3330
|
+
}
|
|
3331
|
+
/** Increment the processed file count. */
|
|
3332
|
+
incrementProcessed() {
|
|
3333
|
+
this._filesProcessed++;
|
|
3238
3334
|
}
|
|
3239
3335
|
/** Mark the current reindex as complete. */
|
|
3240
3336
|
complete() {
|
|
3241
3337
|
this._active = false;
|
|
3242
3338
|
this._scope = undefined;
|
|
3243
3339
|
this._startedAt = undefined;
|
|
3340
|
+
this._filesProcessed = 0;
|
|
3341
|
+
this._totalFiles = 0;
|
|
3244
3342
|
}
|
|
3245
3343
|
/** Get current reindex status. */
|
|
3246
3344
|
getStatus() {
|
|
@@ -3250,6 +3348,8 @@ class ReindexTracker {
|
|
|
3250
3348
|
active: true,
|
|
3251
3349
|
scope: this._scope,
|
|
3252
3350
|
startedAt: this._startedAt,
|
|
3351
|
+
filesProcessed: this._filesProcessed,
|
|
3352
|
+
totalFiles: this._totalFiles,
|
|
3253
3353
|
};
|
|
3254
3354
|
}
|
|
3255
3355
|
}
|
|
@@ -3309,7 +3409,12 @@ function createApiServer(options) {
|
|
|
3309
3409
|
logger,
|
|
3310
3410
|
hybridConfig,
|
|
3311
3411
|
}));
|
|
3312
|
-
app.post('/reindex', createReindexHandler({
|
|
3412
|
+
app.post('/reindex', createReindexHandler({
|
|
3413
|
+
watch: config.watch,
|
|
3414
|
+
processor,
|
|
3415
|
+
logger,
|
|
3416
|
+
concurrency: config.reindex?.concurrency ?? 50,
|
|
3417
|
+
}));
|
|
3313
3418
|
app.post('/rebuild-metadata', createRebuildMetadataHandler({
|
|
3314
3419
|
metadataDir: config.metadataDir,
|
|
3315
3420
|
vectorStore,
|
package/package.json
CHANGED