@karmaniverous/jeeves-watcher 0.7.1 → 0.8.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.
- package/config.schema.json +40 -25
- package/dist/cli/jeeves-watcher/index.js +227 -3
- package/dist/index.d.ts +34 -0
- package/dist/index.js +227 -3
- package/package.json +3 -3
package/config.schema.json
CHANGED
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
"description": "Reusable named JsonMap transformations (inline definition or .json file path resolved relative to config directory).",
|
|
150
150
|
"allOf": [
|
|
151
151
|
{
|
|
152
|
-
"$ref": "#/definitions/
|
|
152
|
+
"$ref": "#/definitions/__schema77"
|
|
153
153
|
}
|
|
154
154
|
]
|
|
155
155
|
},
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"description": "Named reusable Handlebars templates (inline strings or .hbs/.handlebars file paths).",
|
|
158
158
|
"allOf": [
|
|
159
159
|
{
|
|
160
|
-
"$ref": "#/definitions/
|
|
160
|
+
"$ref": "#/definitions/__schema78"
|
|
161
161
|
}
|
|
162
162
|
]
|
|
163
163
|
},
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"description": "Custom Handlebars helper registration.",
|
|
166
166
|
"allOf": [
|
|
167
167
|
{
|
|
168
|
-
"$ref": "#/definitions/
|
|
168
|
+
"$ref": "#/definitions/__schema79"
|
|
169
169
|
}
|
|
170
170
|
]
|
|
171
171
|
},
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
"description": "Custom JsonMap lib function registration.",
|
|
174
174
|
"allOf": [
|
|
175
175
|
{
|
|
176
|
-
"$ref": "#/definitions/
|
|
176
|
+
"$ref": "#/definitions/__schema80"
|
|
177
177
|
}
|
|
178
178
|
]
|
|
179
179
|
},
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"description": "Reindex configuration.",
|
|
182
182
|
"allOf": [
|
|
183
183
|
{
|
|
184
|
-
"$ref": "#/definitions/
|
|
184
|
+
"$ref": "#/definitions/__schema81"
|
|
185
185
|
}
|
|
186
186
|
]
|
|
187
187
|
},
|
|
@@ -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/__schema82"
|
|
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/__schema83"
|
|
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/__schema84"
|
|
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/__schema87"
|
|
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/__schema88"
|
|
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/__schema89"
|
|
233
233
|
}
|
|
234
234
|
]
|
|
235
235
|
}
|
|
@@ -614,6 +614,9 @@
|
|
|
614
614
|
},
|
|
615
615
|
"render": {
|
|
616
616
|
"$ref": "#/definitions/__schema64"
|
|
617
|
+
},
|
|
618
|
+
"renderAs": {
|
|
619
|
+
"$ref": "#/definitions/__schema75"
|
|
617
620
|
}
|
|
618
621
|
},
|
|
619
622
|
"required": [
|
|
@@ -922,6 +925,18 @@
|
|
|
922
925
|
"type": "string"
|
|
923
926
|
},
|
|
924
927
|
"__schema75": {
|
|
928
|
+
"description": "Output file extension override (without dot). Requires template or render.",
|
|
929
|
+
"allOf": [
|
|
930
|
+
{
|
|
931
|
+
"$ref": "#/definitions/__schema76"
|
|
932
|
+
}
|
|
933
|
+
]
|
|
934
|
+
},
|
|
935
|
+
"__schema76": {
|
|
936
|
+
"type": "string",
|
|
937
|
+
"pattern": "^[a-z0-9]{1,10}$"
|
|
938
|
+
},
|
|
939
|
+
"__schema77": {
|
|
925
940
|
"type": "object",
|
|
926
941
|
"propertyNames": {
|
|
927
942
|
"type": "string"
|
|
@@ -958,7 +973,7 @@
|
|
|
958
973
|
]
|
|
959
974
|
}
|
|
960
975
|
},
|
|
961
|
-
"
|
|
976
|
+
"__schema78": {
|
|
962
977
|
"type": "object",
|
|
963
978
|
"propertyNames": {
|
|
964
979
|
"type": "string"
|
|
@@ -985,7 +1000,7 @@
|
|
|
985
1000
|
]
|
|
986
1001
|
}
|
|
987
1002
|
},
|
|
988
|
-
"
|
|
1003
|
+
"__schema79": {
|
|
989
1004
|
"type": "object",
|
|
990
1005
|
"propertyNames": {
|
|
991
1006
|
"type": "string"
|
|
@@ -1005,7 +1020,7 @@
|
|
|
1005
1020
|
]
|
|
1006
1021
|
}
|
|
1007
1022
|
},
|
|
1008
|
-
"
|
|
1023
|
+
"__schema80": {
|
|
1009
1024
|
"type": "object",
|
|
1010
1025
|
"propertyNames": {
|
|
1011
1026
|
"type": "string"
|
|
@@ -1025,7 +1040,7 @@
|
|
|
1025
1040
|
]
|
|
1026
1041
|
}
|
|
1027
1042
|
},
|
|
1028
|
-
"
|
|
1043
|
+
"__schema81": {
|
|
1029
1044
|
"type": "object",
|
|
1030
1045
|
"properties": {
|
|
1031
1046
|
"callbackUrl": {
|
|
@@ -1034,14 +1049,14 @@
|
|
|
1034
1049
|
}
|
|
1035
1050
|
}
|
|
1036
1051
|
},
|
|
1037
|
-
"
|
|
1052
|
+
"__schema82": {
|
|
1038
1053
|
"type": "object",
|
|
1039
1054
|
"propertyNames": {
|
|
1040
1055
|
"type": "string"
|
|
1041
1056
|
},
|
|
1042
1057
|
"additionalProperties": {}
|
|
1043
1058
|
},
|
|
1044
|
-
"
|
|
1059
|
+
"__schema83": {
|
|
1045
1060
|
"type": "object",
|
|
1046
1061
|
"properties": {
|
|
1047
1062
|
"scoreThresholds": {
|
|
@@ -1086,14 +1101,14 @@
|
|
|
1086
1101
|
}
|
|
1087
1102
|
}
|
|
1088
1103
|
},
|
|
1089
|
-
"
|
|
1104
|
+
"__schema84": {
|
|
1090
1105
|
"type": "object",
|
|
1091
1106
|
"properties": {
|
|
1092
1107
|
"level": {
|
|
1093
1108
|
"description": "Logging level (trace, debug, info, warn, error, fatal).",
|
|
1094
1109
|
"allOf": [
|
|
1095
1110
|
{
|
|
1096
|
-
"$ref": "#/definitions/
|
|
1111
|
+
"$ref": "#/definitions/__schema85"
|
|
1097
1112
|
}
|
|
1098
1113
|
]
|
|
1099
1114
|
},
|
|
@@ -1101,25 +1116,25 @@
|
|
|
1101
1116
|
"description": "Path to log file (logs to stdout if omitted).",
|
|
1102
1117
|
"allOf": [
|
|
1103
1118
|
{
|
|
1104
|
-
"$ref": "#/definitions/
|
|
1119
|
+
"$ref": "#/definitions/__schema86"
|
|
1105
1120
|
}
|
|
1106
1121
|
]
|
|
1107
1122
|
}
|
|
1108
1123
|
}
|
|
1109
1124
|
},
|
|
1110
|
-
"
|
|
1125
|
+
"__schema85": {
|
|
1111
1126
|
"type": "string"
|
|
1112
1127
|
},
|
|
1113
|
-
"
|
|
1128
|
+
"__schema86": {
|
|
1114
1129
|
"type": "string"
|
|
1115
1130
|
},
|
|
1116
|
-
"
|
|
1131
|
+
"__schema87": {
|
|
1117
1132
|
"type": "number"
|
|
1118
1133
|
},
|
|
1119
|
-
"
|
|
1134
|
+
"__schema88": {
|
|
1120
1135
|
"type": "number"
|
|
1121
1136
|
},
|
|
1122
|
-
"
|
|
1137
|
+
"__schema89": {
|
|
1123
1138
|
"type": "number"
|
|
1124
1139
|
}
|
|
1125
1140
|
}
|
|
@@ -1537,11 +1537,16 @@ async function applyRules(compiledRules, attributes, options = {}) {
|
|
|
1537
1537
|
const lib = createJsonMapLib(configDir, customMapLib);
|
|
1538
1538
|
let merged = {};
|
|
1539
1539
|
let renderedContent = null;
|
|
1540
|
+
let renderAs = null;
|
|
1540
1541
|
const matchedRules = [];
|
|
1541
1542
|
const log = logger ?? console;
|
|
1542
1543
|
for (const [, { rule, validate }] of compiledRules.entries()) {
|
|
1543
1544
|
if (validate(attributes)) {
|
|
1544
1545
|
matchedRules.push(rule.name);
|
|
1546
|
+
// Resolve renderAs (last-match-wins)
|
|
1547
|
+
if (rule.renderAs) {
|
|
1548
|
+
renderAs = rule.renderAs;
|
|
1549
|
+
}
|
|
1545
1550
|
// Apply schema-based metadata extraction
|
|
1546
1551
|
if (rule.schema && rule.schema.length > 0) {
|
|
1547
1552
|
try {
|
|
@@ -1645,7 +1650,7 @@ async function applyRules(compiledRules, attributes, options = {}) {
|
|
|
1645
1650
|
}
|
|
1646
1651
|
}
|
|
1647
1652
|
}
|
|
1648
|
-
return { metadata: merged, renderedContent, matchedRules };
|
|
1653
|
+
return { metadata: merged, renderedContent, matchedRules, renderAs };
|
|
1649
1654
|
}
|
|
1650
1655
|
|
|
1651
1656
|
/**
|
|
@@ -1926,6 +1931,12 @@ const inferenceRuleSchema = z
|
|
|
1926
1931
|
render: renderConfigSchema
|
|
1927
1932
|
.optional()
|
|
1928
1933
|
.describe('Declarative render configuration for frontmatter + structured Markdown output (mutually exclusive with template).'),
|
|
1934
|
+
/** Output file extension override (e.g. "md", "html", "txt"). Requires template or render. */
|
|
1935
|
+
renderAs: z
|
|
1936
|
+
.string()
|
|
1937
|
+
.regex(/^[a-z0-9]{1,10}$/, 'renderAs must be 1-10 lowercase alphanumeric characters')
|
|
1938
|
+
.optional()
|
|
1939
|
+
.describe('Output file extension override (without dot). Requires template or render.'),
|
|
1929
1940
|
})
|
|
1930
1941
|
.superRefine((val, ctx) => {
|
|
1931
1942
|
if (val.render && val.template) {
|
|
@@ -1935,6 +1946,13 @@ const inferenceRuleSchema = z
|
|
|
1935
1946
|
message: 'render is mutually exclusive with template',
|
|
1936
1947
|
});
|
|
1937
1948
|
}
|
|
1949
|
+
if (val.renderAs && !val.template && !val.render) {
|
|
1950
|
+
ctx.addIssue({
|
|
1951
|
+
code: 'custom',
|
|
1952
|
+
path: ['renderAs'],
|
|
1953
|
+
message: 'renderAs requires template or render',
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1938
1956
|
});
|
|
1939
1957
|
|
|
1940
1958
|
/**
|
|
@@ -2711,6 +2729,107 @@ function createConfigValidateHandler(deps) {
|
|
|
2711
2729
|
}, deps.logger, 'Config validate');
|
|
2712
2730
|
}
|
|
2713
2731
|
|
|
2732
|
+
/**
|
|
2733
|
+
* @module api/handlers/facets
|
|
2734
|
+
* GET /search/facets route handler. Returns schema-derived facet definitions with live values.
|
|
2735
|
+
*/
|
|
2736
|
+
/** Compute a simple hash of rule names + schema refs for cache invalidation. */
|
|
2737
|
+
function computeRulesHash(rules) {
|
|
2738
|
+
if (!rules)
|
|
2739
|
+
return '';
|
|
2740
|
+
return rules
|
|
2741
|
+
.map((r) => `${r.name}:${JSON.stringify(r.schema ?? [])}`)
|
|
2742
|
+
.join('|');
|
|
2743
|
+
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Check whether a resolved property should be exposed as a facet.
|
|
2746
|
+
* A property is facetable if it declares `uiHint` or `enum`.
|
|
2747
|
+
*/
|
|
2748
|
+
function isFacetable(prop) {
|
|
2749
|
+
return prop.uiHint !== undefined || prop.enum !== undefined;
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Build the schema-derived facet structure from inference rules.
|
|
2753
|
+
*
|
|
2754
|
+
* Iterates all rules, resolves their schemas via `mergeSchemas`, and extracts
|
|
2755
|
+
* properties that have `uiHint` or `enum` defined. Deduplicates across rules.
|
|
2756
|
+
*/
|
|
2757
|
+
function buildFacetSchema(rules, mergeOptions) {
|
|
2758
|
+
const fields = new Map();
|
|
2759
|
+
for (const rule of rules ?? []) {
|
|
2760
|
+
if (!rule.schema?.length)
|
|
2761
|
+
continue;
|
|
2762
|
+
const resolved = mergeSchemas(rule.schema, mergeOptions);
|
|
2763
|
+
for (const [propName, propDef] of Object.entries(resolved.properties)) {
|
|
2764
|
+
if (!isFacetable(propDef))
|
|
2765
|
+
continue;
|
|
2766
|
+
const existing = fields.get(propName);
|
|
2767
|
+
if (existing) {
|
|
2768
|
+
existing.rules.push(rule.name);
|
|
2769
|
+
if (propDef.enum)
|
|
2770
|
+
existing.enumValues = propDef.enum;
|
|
2771
|
+
if (propDef.uiHint)
|
|
2772
|
+
existing.uiHint = propDef.uiHint;
|
|
2773
|
+
}
|
|
2774
|
+
else {
|
|
2775
|
+
fields.set(propName, {
|
|
2776
|
+
type: propDef.type ?? 'string',
|
|
2777
|
+
uiHint: propDef.uiHint ?? 'dropdown',
|
|
2778
|
+
enumValues: propDef.enum,
|
|
2779
|
+
rules: [rule.name],
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return { fields, rulesHash: computeRulesHash(rules) };
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Create the GET /search/facets route handler.
|
|
2788
|
+
*
|
|
2789
|
+
* Returns facet definitions derived from inference rule schemas, enriched
|
|
2790
|
+
* with live values from the ValuesManager.
|
|
2791
|
+
*
|
|
2792
|
+
* @param deps - Handler dependencies.
|
|
2793
|
+
* @returns Fastify route handler (plain return, compatible with `withCache`).
|
|
2794
|
+
*/
|
|
2795
|
+
function createFacetsHandler(deps) {
|
|
2796
|
+
const { config, valuesManager, configDir } = deps;
|
|
2797
|
+
let cached;
|
|
2798
|
+
const mergeOptions = {
|
|
2799
|
+
globalSchemas: config.schemas,
|
|
2800
|
+
configDir,
|
|
2801
|
+
};
|
|
2802
|
+
return () => {
|
|
2803
|
+
// Rebuild schema cache if rules changed
|
|
2804
|
+
const currentHash = computeRulesHash(config.inferenceRules);
|
|
2805
|
+
if (!cached || cached.rulesHash !== currentHash) {
|
|
2806
|
+
cached = buildFacetSchema(config.inferenceRules, mergeOptions);
|
|
2807
|
+
}
|
|
2808
|
+
// Merge with live values
|
|
2809
|
+
const allValues = valuesManager.getAll();
|
|
2810
|
+
const facets = [];
|
|
2811
|
+
for (const [field, schema] of cached.fields) {
|
|
2812
|
+
// Collect live values from all rules that define this field
|
|
2813
|
+
const liveValues = new Set();
|
|
2814
|
+
for (const ruleName of schema.rules) {
|
|
2815
|
+
const fieldValues = allValues[ruleName]?.[field];
|
|
2816
|
+
if (fieldValues) {
|
|
2817
|
+
for (const v of fieldValues)
|
|
2818
|
+
liveValues.add(v);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
facets.push({
|
|
2822
|
+
field,
|
|
2823
|
+
type: schema.type,
|
|
2824
|
+
uiHint: schema.uiHint,
|
|
2825
|
+
values: schema.enumValues ?? [...liveValues].sort(),
|
|
2826
|
+
rules: schema.rules,
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
return { facets };
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2714
2833
|
/**
|
|
2715
2834
|
* @module api/handlers/issues
|
|
2716
2835
|
* Fastify route handler for GET /issues. Returns current processing issues.
|
|
@@ -3037,6 +3156,79 @@ function createReindexHandler(deps) {
|
|
|
3037
3156
|
}, deps.logger, 'Reindex');
|
|
3038
3157
|
}
|
|
3039
3158
|
|
|
3159
|
+
/**
|
|
3160
|
+
* @module util/isPathWatched
|
|
3161
|
+
* Checks whether a file path falls within the watched scope defined by watch config globs.
|
|
3162
|
+
*/
|
|
3163
|
+
/**
|
|
3164
|
+
* Check whether a file path matches the watched scope.
|
|
3165
|
+
* A path is watched if it matches at least one `paths` glob
|
|
3166
|
+
* AND does not match any `ignored` glob. Mirrors chokidar's
|
|
3167
|
+
* inclusion/exclusion logic.
|
|
3168
|
+
*
|
|
3169
|
+
* @param filePath - The file path to check.
|
|
3170
|
+
* @param watchPaths - Glob patterns that define watched paths.
|
|
3171
|
+
* @param ignoredPaths - Glob patterns that exclude paths (optional).
|
|
3172
|
+
* @returns `true` if the file is within watched scope.
|
|
3173
|
+
*/
|
|
3174
|
+
function isPathWatched(filePath, watchPaths, ignoredPaths) {
|
|
3175
|
+
const normalised = normalizeSlashes(filePath);
|
|
3176
|
+
const isIncluded = picomatch(watchPaths, { dot: true });
|
|
3177
|
+
if (!isIncluded(normalised))
|
|
3178
|
+
return false;
|
|
3179
|
+
if (ignoredPaths && ignoredPaths.length > 0) {
|
|
3180
|
+
const isExcluded = picomatch(ignoredPaths, { dot: true });
|
|
3181
|
+
if (isExcluded(normalised))
|
|
3182
|
+
return false;
|
|
3183
|
+
}
|
|
3184
|
+
return true;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
/**
|
|
3188
|
+
* @module api/handlers/render
|
|
3189
|
+
* POST /render route handler. Runs a file through the inference rule engine and returns rendered content.
|
|
3190
|
+
*/
|
|
3191
|
+
/**
|
|
3192
|
+
* Create the POST /render route handler.
|
|
3193
|
+
*
|
|
3194
|
+
* @param deps - Handler dependencies.
|
|
3195
|
+
* @returns Fastify route handler.
|
|
3196
|
+
*/
|
|
3197
|
+
function createRenderHandler(deps) {
|
|
3198
|
+
const { processor, watch, logger } = deps;
|
|
3199
|
+
return async (request, reply) => {
|
|
3200
|
+
const { path: filePath } = request.body;
|
|
3201
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
3202
|
+
return reply.status(400).send({ error: 'Missing required field: path' });
|
|
3203
|
+
}
|
|
3204
|
+
// Validate path is within watched scope
|
|
3205
|
+
if (!isPathWatched(filePath, watch.paths, watch.ignored)) {
|
|
3206
|
+
return reply.status(403).send({ error: 'Path is outside watched scope' });
|
|
3207
|
+
}
|
|
3208
|
+
try {
|
|
3209
|
+
const result = await processor.renderFile(filePath);
|
|
3210
|
+
// Passthrough responses should not be cached
|
|
3211
|
+
if (!result.transformed) {
|
|
3212
|
+
void reply.header('Cache-Control', 'no-cache');
|
|
3213
|
+
}
|
|
3214
|
+
return await reply.send({
|
|
3215
|
+
renderAs: result.renderAs,
|
|
3216
|
+
content: result.content,
|
|
3217
|
+
rules: result.rules,
|
|
3218
|
+
metadata: result.metadata,
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
catch (error) {
|
|
3222
|
+
const msg = error instanceof Error ? error.message : 'Unknown render error';
|
|
3223
|
+
if (msg.includes('ENOENT') || msg.includes('no such file')) {
|
|
3224
|
+
return reply.status(404).send({ error: 'File not found' });
|
|
3225
|
+
}
|
|
3226
|
+
logger.error({ filePath, error: msg }, 'Render failed');
|
|
3227
|
+
return reply.status(422).send({ error: msg });
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3040
3232
|
/**
|
|
3041
3233
|
* @module api/handlers/rulesReapply
|
|
3042
3234
|
* Fastify route handler for POST /rules/reapply.
|
|
@@ -3231,6 +3423,11 @@ function withCache(ttlMs, handler) {
|
|
|
3231
3423
|
if (fReply.statusCode >= 400) {
|
|
3232
3424
|
return result;
|
|
3233
3425
|
}
|
|
3426
|
+
// Skip cache for responses with Cache-Control: no-cache
|
|
3427
|
+
const cacheControl = fReply.getHeader('Cache-Control');
|
|
3428
|
+
if (cacheControl === 'no-cache') {
|
|
3429
|
+
return result;
|
|
3430
|
+
}
|
|
3234
3431
|
// Store in cache
|
|
3235
3432
|
cache.set(key, {
|
|
3236
3433
|
value: result,
|
|
@@ -3308,6 +3505,12 @@ function createApiServer(options) {
|
|
|
3308
3505
|
reindexTracker,
|
|
3309
3506
|
})));
|
|
3310
3507
|
app.post('/metadata', createMetadataHandler({ processor, config, logger }));
|
|
3508
|
+
app.post('/render', withCache(cacheTtlMs, createRenderHandler({ processor, watch: config.watch, logger })));
|
|
3509
|
+
app.get('/search/facets', createFacetsHandler({
|
|
3510
|
+
config,
|
|
3511
|
+
valuesManager,
|
|
3512
|
+
configDir: dirname(configPath),
|
|
3513
|
+
}));
|
|
3311
3514
|
const hybridConfig = config.search?.hybrid
|
|
3312
3515
|
? {
|
|
3313
3516
|
enabled: config.search.hybrid.enabled,
|
|
@@ -3925,7 +4128,7 @@ async function buildMergedMetadata(options) {
|
|
|
3925
4128
|
const extracted = await extractText(filePath, ext);
|
|
3926
4129
|
// 2. Build attributes + apply rules
|
|
3927
4130
|
const attributes = buildAttributes(filePath, stats, extracted.frontmatter, extracted.json);
|
|
3928
|
-
const { metadata: inferred, renderedContent, matchedRules, } = await applyRules(compiledRules, attributes, {
|
|
4131
|
+
const { metadata: inferred, renderedContent, matchedRules, renderAs, } = await applyRules(compiledRules, attributes, {
|
|
3929
4132
|
namedMaps: maps,
|
|
3930
4133
|
logger,
|
|
3931
4134
|
templateEngine,
|
|
@@ -3947,6 +4150,7 @@ async function buildMergedMetadata(options) {
|
|
|
3947
4150
|
extracted,
|
|
3948
4151
|
renderedContent,
|
|
3949
4152
|
matchedRules,
|
|
4153
|
+
renderAs,
|
|
3950
4154
|
};
|
|
3951
4155
|
}
|
|
3952
4156
|
|
|
@@ -4162,7 +4366,7 @@ class DocumentProcessor {
|
|
|
4162
4366
|
...result.metadata,
|
|
4163
4367
|
matched_rules: result.matchedRules,
|
|
4164
4368
|
};
|
|
4165
|
-
return { ...result, metadataWithRules };
|
|
4369
|
+
return { ...result, metadataWithRules, renderAs: result.renderAs };
|
|
4166
4370
|
}
|
|
4167
4371
|
/**
|
|
4168
4372
|
* Execute an async operation with standardized file error handling.
|
|
@@ -4283,6 +4487,26 @@ class DocumentProcessor {
|
|
|
4283
4487
|
return metadataWithRules;
|
|
4284
4488
|
}, null);
|
|
4285
4489
|
}
|
|
4490
|
+
/**
|
|
4491
|
+
* Render a file through the rule engine without embedding.
|
|
4492
|
+
* Returns rendered content, renderAs, matched rules, and metadata.
|
|
4493
|
+
*
|
|
4494
|
+
* @param filePath - The file to render.
|
|
4495
|
+
* @returns The render result.
|
|
4496
|
+
*/
|
|
4497
|
+
async renderFile(filePath) {
|
|
4498
|
+
const ext = extname(filePath);
|
|
4499
|
+
const { renderedContent, extracted, matchedRules, metadataWithRules, renderAs, } = await this.buildMetadataWithRules(filePath);
|
|
4500
|
+
const content = renderedContent ?? extracted.text;
|
|
4501
|
+
const resolved = renderAs ?? (ext.slice(1) || 'txt');
|
|
4502
|
+
return {
|
|
4503
|
+
renderAs: resolved,
|
|
4504
|
+
content,
|
|
4505
|
+
rules: matchedRules,
|
|
4506
|
+
metadata: metadataWithRules,
|
|
4507
|
+
transformed: renderedContent !== null,
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4286
4510
|
/**
|
|
4287
4511
|
* Update compiled inference rules, template engine, and custom map lib.
|
|
4288
4512
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ declare const inferenceRuleSchema: z.ZodObject<{
|
|
|
91
91
|
sort: z.ZodOptional<z.ZodString>;
|
|
92
92
|
}, z.core.$strip>>;
|
|
93
93
|
}, z.core.$strip>>;
|
|
94
|
+
renderAs: z.ZodOptional<z.ZodString>;
|
|
94
95
|
}, z.core.$strip>;
|
|
95
96
|
/** An inference rule: JSON Schema match condition, schema array, and optional JsonMap transformation. */
|
|
96
97
|
type InferenceRule = z.infer<typeof inferenceRuleSchema>;
|
|
@@ -165,6 +166,7 @@ declare const jeevesWatcherConfigSchema: z.ZodObject<{
|
|
|
165
166
|
sort: z.ZodOptional<z.ZodString>;
|
|
166
167
|
}, z.core.$strip>>;
|
|
167
168
|
}, z.core.$strip>>;
|
|
169
|
+
renderAs: z.ZodOptional<z.ZodString>;
|
|
168
170
|
}, z.core.$strip>, z.ZodString]>>>;
|
|
169
171
|
maps: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodType<_karmaniverous_jsonmap.JsonMapMap, unknown, z.core.$ZodTypeInternals<_karmaniverous_jsonmap.JsonMapMap, unknown>>, z.ZodString, z.ZodObject<{
|
|
170
172
|
map: z.ZodUnion<[z.ZodType<_karmaniverous_jsonmap.JsonMapMap, unknown, z.core.$ZodTypeInternals<_karmaniverous_jsonmap.JsonMapMap, unknown>>, z.ZodString]>;
|
|
@@ -580,6 +582,8 @@ interface ApplyRulesResult {
|
|
|
580
582
|
renderedContent: string | null;
|
|
581
583
|
/** Names of rules that matched. */
|
|
582
584
|
matchedRules: string[];
|
|
585
|
+
/** The renderAs value from the last matching rule that declares it, or null. */
|
|
586
|
+
renderAs: string | null;
|
|
583
587
|
}
|
|
584
588
|
/**
|
|
585
589
|
* Optional parameters for applyRules beyond the required compiledRules and attributes.
|
|
@@ -908,6 +912,26 @@ interface ProcessorConfig {
|
|
|
908
912
|
globalSchemas?: Record<string, SchemaEntry>;
|
|
909
913
|
}
|
|
910
914
|
|
|
915
|
+
/**
|
|
916
|
+
* @module processor/renderResult
|
|
917
|
+
* Type definition for the render endpoint result.
|
|
918
|
+
*/
|
|
919
|
+
/**
|
|
920
|
+
* Result returned by {@link DocumentProcessorInterface.renderFile}.
|
|
921
|
+
*/
|
|
922
|
+
interface RenderResult {
|
|
923
|
+
/** Output content type (file extension without dot). Always present. */
|
|
924
|
+
renderAs: string;
|
|
925
|
+
/** Rendered content (if a transform ran) or extracted text (passthrough). */
|
|
926
|
+
content: string;
|
|
927
|
+
/** Names of matched inference rules (diagnostic). */
|
|
928
|
+
rules: string[];
|
|
929
|
+
/** Composed embedding properties from matched rules. */
|
|
930
|
+
metadata: Record<string, unknown>;
|
|
931
|
+
/** Whether a template or render transform produced the content. */
|
|
932
|
+
transformed: boolean;
|
|
933
|
+
}
|
|
934
|
+
|
|
911
935
|
/**
|
|
912
936
|
* @module processor/types
|
|
913
937
|
* Document processor interface definitions.
|
|
@@ -927,6 +951,8 @@ interface DocumentProcessorInterface {
|
|
|
927
951
|
processMetadataUpdate(filePath: string, metadata: Record<string, unknown>): Promise<Record<string, unknown> | null>;
|
|
928
952
|
/** Process a rules update for a file (rebuild merged metadata, payload update only). */
|
|
929
953
|
processRulesUpdate(filePath: string): Promise<Record<string, unknown> | null>;
|
|
954
|
+
/** Render a file through the rule engine without embedding. */
|
|
955
|
+
renderFile(filePath: string): Promise<RenderResult>;
|
|
930
956
|
/** Update compiled inference rules and associated engines. */
|
|
931
957
|
updateRules(compiledRules: CompiledRule[], templateEngine?: TemplateEngine, customMapLib?: Record<string, (...args: unknown[]) => unknown>): void;
|
|
932
958
|
}
|
|
@@ -1017,6 +1043,14 @@ declare class DocumentProcessor implements DocumentProcessorInterface {
|
|
|
1017
1043
|
* @returns The merged metadata, or `null` if the file is not indexed.
|
|
1018
1044
|
*/
|
|
1019
1045
|
processRulesUpdate(filePath: string): Promise<Record<string, unknown> | null>;
|
|
1046
|
+
/**
|
|
1047
|
+
* Render a file through the rule engine without embedding.
|
|
1048
|
+
* Returns rendered content, renderAs, matched rules, and metadata.
|
|
1049
|
+
*
|
|
1050
|
+
* @param filePath - The file to render.
|
|
1051
|
+
* @returns The render result.
|
|
1052
|
+
*/
|
|
1053
|
+
renderFile(filePath: string): Promise<RenderResult>;
|
|
1020
1054
|
/**
|
|
1021
1055
|
* Update compiled inference rules, template engine, and custom map lib.
|
|
1022
1056
|
*
|
package/dist/index.js
CHANGED
|
@@ -892,11 +892,16 @@ async function applyRules(compiledRules, attributes, options = {}) {
|
|
|
892
892
|
const lib = createJsonMapLib(configDir, customMapLib);
|
|
893
893
|
let merged = {};
|
|
894
894
|
let renderedContent = null;
|
|
895
|
+
let renderAs = null;
|
|
895
896
|
const matchedRules = [];
|
|
896
897
|
const log = logger ?? console;
|
|
897
898
|
for (const [, { rule, validate }] of compiledRules.entries()) {
|
|
898
899
|
if (validate(attributes)) {
|
|
899
900
|
matchedRules.push(rule.name);
|
|
901
|
+
// Resolve renderAs (last-match-wins)
|
|
902
|
+
if (rule.renderAs) {
|
|
903
|
+
renderAs = rule.renderAs;
|
|
904
|
+
}
|
|
900
905
|
// Apply schema-based metadata extraction
|
|
901
906
|
if (rule.schema && rule.schema.length > 0) {
|
|
902
907
|
try {
|
|
@@ -1000,7 +1005,7 @@ async function applyRules(compiledRules, attributes, options = {}) {
|
|
|
1000
1005
|
}
|
|
1001
1006
|
}
|
|
1002
1007
|
}
|
|
1003
|
-
return { metadata: merged, renderedContent, matchedRules };
|
|
1008
|
+
return { metadata: merged, renderedContent, matchedRules, renderAs };
|
|
1004
1009
|
}
|
|
1005
1010
|
|
|
1006
1011
|
/**
|
|
@@ -1617,6 +1622,12 @@ const inferenceRuleSchema = z
|
|
|
1617
1622
|
render: renderConfigSchema
|
|
1618
1623
|
.optional()
|
|
1619
1624
|
.describe('Declarative render configuration for frontmatter + structured Markdown output (mutually exclusive with template).'),
|
|
1625
|
+
/** Output file extension override (e.g. "md", "html", "txt"). Requires template or render. */
|
|
1626
|
+
renderAs: z
|
|
1627
|
+
.string()
|
|
1628
|
+
.regex(/^[a-z0-9]{1,10}$/, 'renderAs must be 1-10 lowercase alphanumeric characters')
|
|
1629
|
+
.optional()
|
|
1630
|
+
.describe('Output file extension override (without dot). Requires template or render.'),
|
|
1620
1631
|
})
|
|
1621
1632
|
.superRefine((val, ctx) => {
|
|
1622
1633
|
if (val.render && val.template) {
|
|
@@ -1626,6 +1637,13 @@ const inferenceRuleSchema = z
|
|
|
1626
1637
|
message: 'render is mutually exclusive with template',
|
|
1627
1638
|
});
|
|
1628
1639
|
}
|
|
1640
|
+
if (val.renderAs && !val.template && !val.render) {
|
|
1641
|
+
ctx.addIssue({
|
|
1642
|
+
code: 'custom',
|
|
1643
|
+
path: ['renderAs'],
|
|
1644
|
+
message: 'renderAs requires template or render',
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1629
1647
|
});
|
|
1630
1648
|
|
|
1631
1649
|
/**
|
|
@@ -2402,6 +2420,107 @@ function createConfigValidateHandler(deps) {
|
|
|
2402
2420
|
}, deps.logger, 'Config validate');
|
|
2403
2421
|
}
|
|
2404
2422
|
|
|
2423
|
+
/**
|
|
2424
|
+
* @module api/handlers/facets
|
|
2425
|
+
* GET /search/facets route handler. Returns schema-derived facet definitions with live values.
|
|
2426
|
+
*/
|
|
2427
|
+
/** Compute a simple hash of rule names + schema refs for cache invalidation. */
|
|
2428
|
+
function computeRulesHash(rules) {
|
|
2429
|
+
if (!rules)
|
|
2430
|
+
return '';
|
|
2431
|
+
return rules
|
|
2432
|
+
.map((r) => `${r.name}:${JSON.stringify(r.schema ?? [])}`)
|
|
2433
|
+
.join('|');
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Check whether a resolved property should be exposed as a facet.
|
|
2437
|
+
* A property is facetable if it declares `uiHint` or `enum`.
|
|
2438
|
+
*/
|
|
2439
|
+
function isFacetable(prop) {
|
|
2440
|
+
return prop.uiHint !== undefined || prop.enum !== undefined;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Build the schema-derived facet structure from inference rules.
|
|
2444
|
+
*
|
|
2445
|
+
* Iterates all rules, resolves their schemas via `mergeSchemas`, and extracts
|
|
2446
|
+
* properties that have `uiHint` or `enum` defined. Deduplicates across rules.
|
|
2447
|
+
*/
|
|
2448
|
+
function buildFacetSchema(rules, mergeOptions) {
|
|
2449
|
+
const fields = new Map();
|
|
2450
|
+
for (const rule of rules ?? []) {
|
|
2451
|
+
if (!rule.schema?.length)
|
|
2452
|
+
continue;
|
|
2453
|
+
const resolved = mergeSchemas(rule.schema, mergeOptions);
|
|
2454
|
+
for (const [propName, propDef] of Object.entries(resolved.properties)) {
|
|
2455
|
+
if (!isFacetable(propDef))
|
|
2456
|
+
continue;
|
|
2457
|
+
const existing = fields.get(propName);
|
|
2458
|
+
if (existing) {
|
|
2459
|
+
existing.rules.push(rule.name);
|
|
2460
|
+
if (propDef.enum)
|
|
2461
|
+
existing.enumValues = propDef.enum;
|
|
2462
|
+
if (propDef.uiHint)
|
|
2463
|
+
existing.uiHint = propDef.uiHint;
|
|
2464
|
+
}
|
|
2465
|
+
else {
|
|
2466
|
+
fields.set(propName, {
|
|
2467
|
+
type: propDef.type ?? 'string',
|
|
2468
|
+
uiHint: propDef.uiHint ?? 'dropdown',
|
|
2469
|
+
enumValues: propDef.enum,
|
|
2470
|
+
rules: [rule.name],
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return { fields, rulesHash: computeRulesHash(rules) };
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Create the GET /search/facets route handler.
|
|
2479
|
+
*
|
|
2480
|
+
* Returns facet definitions derived from inference rule schemas, enriched
|
|
2481
|
+
* with live values from the ValuesManager.
|
|
2482
|
+
*
|
|
2483
|
+
* @param deps - Handler dependencies.
|
|
2484
|
+
* @returns Fastify route handler (plain return, compatible with `withCache`).
|
|
2485
|
+
*/
|
|
2486
|
+
function createFacetsHandler(deps) {
|
|
2487
|
+
const { config, valuesManager, configDir } = deps;
|
|
2488
|
+
let cached;
|
|
2489
|
+
const mergeOptions = {
|
|
2490
|
+
globalSchemas: config.schemas,
|
|
2491
|
+
configDir,
|
|
2492
|
+
};
|
|
2493
|
+
return () => {
|
|
2494
|
+
// Rebuild schema cache if rules changed
|
|
2495
|
+
const currentHash = computeRulesHash(config.inferenceRules);
|
|
2496
|
+
if (!cached || cached.rulesHash !== currentHash) {
|
|
2497
|
+
cached = buildFacetSchema(config.inferenceRules, mergeOptions);
|
|
2498
|
+
}
|
|
2499
|
+
// Merge with live values
|
|
2500
|
+
const allValues = valuesManager.getAll();
|
|
2501
|
+
const facets = [];
|
|
2502
|
+
for (const [field, schema] of cached.fields) {
|
|
2503
|
+
// Collect live values from all rules that define this field
|
|
2504
|
+
const liveValues = new Set();
|
|
2505
|
+
for (const ruleName of schema.rules) {
|
|
2506
|
+
const fieldValues = allValues[ruleName]?.[field];
|
|
2507
|
+
if (fieldValues) {
|
|
2508
|
+
for (const v of fieldValues)
|
|
2509
|
+
liveValues.add(v);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
facets.push({
|
|
2513
|
+
field,
|
|
2514
|
+
type: schema.type,
|
|
2515
|
+
uiHint: schema.uiHint,
|
|
2516
|
+
values: schema.enumValues ?? [...liveValues].sort(),
|
|
2517
|
+
rules: schema.rules,
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
return { facets };
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2405
2524
|
/**
|
|
2406
2525
|
* @module api/handlers/issues
|
|
2407
2526
|
* Fastify route handler for GET /issues. Returns current processing issues.
|
|
@@ -2728,6 +2847,79 @@ function createReindexHandler(deps) {
|
|
|
2728
2847
|
}, deps.logger, 'Reindex');
|
|
2729
2848
|
}
|
|
2730
2849
|
|
|
2850
|
+
/**
|
|
2851
|
+
* @module util/isPathWatched
|
|
2852
|
+
* Checks whether a file path falls within the watched scope defined by watch config globs.
|
|
2853
|
+
*/
|
|
2854
|
+
/**
|
|
2855
|
+
* Check whether a file path matches the watched scope.
|
|
2856
|
+
* A path is watched if it matches at least one `paths` glob
|
|
2857
|
+
* AND does not match any `ignored` glob. Mirrors chokidar's
|
|
2858
|
+
* inclusion/exclusion logic.
|
|
2859
|
+
*
|
|
2860
|
+
* @param filePath - The file path to check.
|
|
2861
|
+
* @param watchPaths - Glob patterns that define watched paths.
|
|
2862
|
+
* @param ignoredPaths - Glob patterns that exclude paths (optional).
|
|
2863
|
+
* @returns `true` if the file is within watched scope.
|
|
2864
|
+
*/
|
|
2865
|
+
function isPathWatched(filePath, watchPaths, ignoredPaths) {
|
|
2866
|
+
const normalised = normalizeSlashes(filePath);
|
|
2867
|
+
const isIncluded = picomatch(watchPaths, { dot: true });
|
|
2868
|
+
if (!isIncluded(normalised))
|
|
2869
|
+
return false;
|
|
2870
|
+
if (ignoredPaths && ignoredPaths.length > 0) {
|
|
2871
|
+
const isExcluded = picomatch(ignoredPaths, { dot: true });
|
|
2872
|
+
if (isExcluded(normalised))
|
|
2873
|
+
return false;
|
|
2874
|
+
}
|
|
2875
|
+
return true;
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
/**
|
|
2879
|
+
* @module api/handlers/render
|
|
2880
|
+
* POST /render route handler. Runs a file through the inference rule engine and returns rendered content.
|
|
2881
|
+
*/
|
|
2882
|
+
/**
|
|
2883
|
+
* Create the POST /render route handler.
|
|
2884
|
+
*
|
|
2885
|
+
* @param deps - Handler dependencies.
|
|
2886
|
+
* @returns Fastify route handler.
|
|
2887
|
+
*/
|
|
2888
|
+
function createRenderHandler(deps) {
|
|
2889
|
+
const { processor, watch, logger } = deps;
|
|
2890
|
+
return async (request, reply) => {
|
|
2891
|
+
const { path: filePath } = request.body;
|
|
2892
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
2893
|
+
return reply.status(400).send({ error: 'Missing required field: path' });
|
|
2894
|
+
}
|
|
2895
|
+
// Validate path is within watched scope
|
|
2896
|
+
if (!isPathWatched(filePath, watch.paths, watch.ignored)) {
|
|
2897
|
+
return reply.status(403).send({ error: 'Path is outside watched scope' });
|
|
2898
|
+
}
|
|
2899
|
+
try {
|
|
2900
|
+
const result = await processor.renderFile(filePath);
|
|
2901
|
+
// Passthrough responses should not be cached
|
|
2902
|
+
if (!result.transformed) {
|
|
2903
|
+
void reply.header('Cache-Control', 'no-cache');
|
|
2904
|
+
}
|
|
2905
|
+
return await reply.send({
|
|
2906
|
+
renderAs: result.renderAs,
|
|
2907
|
+
content: result.content,
|
|
2908
|
+
rules: result.rules,
|
|
2909
|
+
metadata: result.metadata,
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
catch (error) {
|
|
2913
|
+
const msg = error instanceof Error ? error.message : 'Unknown render error';
|
|
2914
|
+
if (msg.includes('ENOENT') || msg.includes('no such file')) {
|
|
2915
|
+
return reply.status(404).send({ error: 'File not found' });
|
|
2916
|
+
}
|
|
2917
|
+
logger.error({ filePath, error: msg }, 'Render failed');
|
|
2918
|
+
return reply.status(422).send({ error: msg });
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2731
2923
|
/**
|
|
2732
2924
|
* @module api/handlers/rulesReapply
|
|
2733
2925
|
* Fastify route handler for POST /rules/reapply.
|
|
@@ -2922,6 +3114,11 @@ function withCache(ttlMs, handler) {
|
|
|
2922
3114
|
if (fReply.statusCode >= 400) {
|
|
2923
3115
|
return result;
|
|
2924
3116
|
}
|
|
3117
|
+
// Skip cache for responses with Cache-Control: no-cache
|
|
3118
|
+
const cacheControl = fReply.getHeader('Cache-Control');
|
|
3119
|
+
if (cacheControl === 'no-cache') {
|
|
3120
|
+
return result;
|
|
3121
|
+
}
|
|
2925
3122
|
// Store in cache
|
|
2926
3123
|
cache.set(key, {
|
|
2927
3124
|
value: result,
|
|
@@ -2999,6 +3196,12 @@ function createApiServer(options) {
|
|
|
2999
3196
|
reindexTracker,
|
|
3000
3197
|
})));
|
|
3001
3198
|
app.post('/metadata', createMetadataHandler({ processor, config, logger }));
|
|
3199
|
+
app.post('/render', withCache(cacheTtlMs, createRenderHandler({ processor, watch: config.watch, logger })));
|
|
3200
|
+
app.get('/search/facets', createFacetsHandler({
|
|
3201
|
+
config,
|
|
3202
|
+
valuesManager,
|
|
3203
|
+
configDir: dirname(configPath),
|
|
3204
|
+
}));
|
|
3002
3205
|
const hybridConfig = config.search?.hybrid
|
|
3003
3206
|
? {
|
|
3004
3207
|
enabled: config.search.hybrid.enabled,
|
|
@@ -3903,7 +4106,7 @@ async function buildMergedMetadata(options) {
|
|
|
3903
4106
|
const extracted = await extractText(filePath, ext);
|
|
3904
4107
|
// 2. Build attributes + apply rules
|
|
3905
4108
|
const attributes = buildAttributes(filePath, stats, extracted.frontmatter, extracted.json);
|
|
3906
|
-
const { metadata: inferred, renderedContent, matchedRules, } = await applyRules(compiledRules, attributes, {
|
|
4109
|
+
const { metadata: inferred, renderedContent, matchedRules, renderAs, } = await applyRules(compiledRules, attributes, {
|
|
3907
4110
|
namedMaps: maps,
|
|
3908
4111
|
logger,
|
|
3909
4112
|
templateEngine,
|
|
@@ -3925,6 +4128,7 @@ async function buildMergedMetadata(options) {
|
|
|
3925
4128
|
extracted,
|
|
3926
4129
|
renderedContent,
|
|
3927
4130
|
matchedRules,
|
|
4131
|
+
renderAs,
|
|
3928
4132
|
};
|
|
3929
4133
|
}
|
|
3930
4134
|
|
|
@@ -4140,7 +4344,7 @@ class DocumentProcessor {
|
|
|
4140
4344
|
...result.metadata,
|
|
4141
4345
|
matched_rules: result.matchedRules,
|
|
4142
4346
|
};
|
|
4143
|
-
return { ...result, metadataWithRules };
|
|
4347
|
+
return { ...result, metadataWithRules, renderAs: result.renderAs };
|
|
4144
4348
|
}
|
|
4145
4349
|
/**
|
|
4146
4350
|
* Execute an async operation with standardized file error handling.
|
|
@@ -4261,6 +4465,26 @@ class DocumentProcessor {
|
|
|
4261
4465
|
return metadataWithRules;
|
|
4262
4466
|
}, null);
|
|
4263
4467
|
}
|
|
4468
|
+
/**
|
|
4469
|
+
* Render a file through the rule engine without embedding.
|
|
4470
|
+
* Returns rendered content, renderAs, matched rules, and metadata.
|
|
4471
|
+
*
|
|
4472
|
+
* @param filePath - The file to render.
|
|
4473
|
+
* @returns The render result.
|
|
4474
|
+
*/
|
|
4475
|
+
async renderFile(filePath) {
|
|
4476
|
+
const ext = extname(filePath);
|
|
4477
|
+
const { renderedContent, extracted, matchedRules, metadataWithRules, renderAs, } = await this.buildMetadataWithRules(filePath);
|
|
4478
|
+
const content = renderedContent ?? extracted.text;
|
|
4479
|
+
const resolved = renderAs ?? (ext.slice(1) || 'txt');
|
|
4480
|
+
return {
|
|
4481
|
+
renderAs: resolved,
|
|
4482
|
+
content,
|
|
4483
|
+
rules: matchedRules,
|
|
4484
|
+
metadata: metadataWithRules,
|
|
4485
|
+
transformed: renderedContent !== null,
|
|
4486
|
+
};
|
|
4487
|
+
}
|
|
4264
4488
|
/**
|
|
4265
4489
|
* Update compiled inference rules, template engine, and custom map lib.
|
|
4266
4490
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karmaniverous/jeeves-watcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"author": "Jason Williscroft",
|
|
5
5
|
"description": "Filesystem watcher that keeps a Qdrant vector store in sync with document changes",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"mammoth": "^1.11.0",
|
|
74
74
|
"mdast-util-from-adf": "^2.2.0",
|
|
75
75
|
"mdast-util-to-markdown": "^2.1.2",
|
|
76
|
-
"picomatch": "
|
|
76
|
+
"picomatch": "^4.0.3",
|
|
77
77
|
"pino": "*",
|
|
78
78
|
"radash": "^12.1.1",
|
|
79
79
|
"rehype-parse": "^9.0.1",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@types/fs-extra": "^11.0.4",
|
|
94
94
|
"@types/js-yaml": "*",
|
|
95
95
|
"@types/node": "^25.3.0",
|
|
96
|
-
"@types/picomatch": "
|
|
96
|
+
"@types/picomatch": "^4.0.2",
|
|
97
97
|
"@types/uuid": "*",
|
|
98
98
|
"@vitest/coverage-v8": "^4.0.18",
|
|
99
99
|
"auto-changelog": "^2.5.0",
|