@gmickel/gno 1.25.1 → 1.27.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/README.md +12 -5
- package/assets/skill/SKILL.md +37 -17
- package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.27.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +161 -6
- package/spec/db/schema.sql +1 -1
- package/spec/evals-agentic.md +17 -0
- package/spec/mcp.md +25 -6
- package/spec/output-schemas/ask.schema.json +3 -0
- package/spec/output-schemas/project-profile-apply.schema.json +209 -0
- package/spec/output-schemas/project-profile-command.schema.json +160 -0
- package/spec/output-schemas/query-diagnose.schema.json +68 -5
- package/spec/output-schemas/search-results.schema.json +87 -1
- package/spec/output-schemas/setup-profile-result.schema.json +87 -0
- package/spec/output-schemas/status.schema.json +24 -0
- package/spec/project-profile.schema.json +303 -0
- package/src/app/context-runtime-contract.ts +4 -1
- package/src/app/context-runtime-types.ts +2 -0
- package/src/app/context-runtime.ts +26 -0
- package/src/app/verified-ask.ts +6 -1
- package/src/cli/commands/ask.ts +8 -1
- package/src/cli/commands/collection/add.ts +39 -45
- package/src/cli/commands/collection/remove.ts +28 -28
- package/src/cli/commands/collection/rename.ts +55 -73
- package/src/cli/commands/context/add.ts +37 -26
- package/src/cli/commands/context/rm.ts +47 -20
- package/src/cli/commands/init.ts +55 -125
- package/src/cli/commands/models/use.ts +43 -38
- package/src/cli/commands/profile-apply.ts +334 -0
- package/src/cli/commands/profile.ts +409 -0
- package/src/cli/commands/query.ts +6 -3
- package/src/cli/commands/search.ts +6 -1
- package/src/cli/commands/setup-activation.ts +205 -54
- package/src/cli/commands/setup-profile.ts +223 -0
- package/src/cli/commands/setup.ts +3 -0
- package/src/cli/commands/status.ts +43 -7
- package/src/cli/program.ts +112 -9
- package/src/config/content-types.ts +82 -0
- package/src/config/index.ts +11 -0
- package/src/config/project-profile.ts +374 -0
- package/src/config/saver.ts +16 -7
- package/src/config/types.ts +53 -2
- package/src/core/config-mutation.ts +138 -76
- package/src/core/config-write-lock.ts +89 -0
- package/src/core/context-compiler.ts +38 -1
- package/src/core/context-identity.ts +16 -0
- package/src/core/context-resolver.ts +2 -12
- package/src/core/folder-setup-planning.ts +6 -21
- package/src/core/folder-setup.ts +30 -2
- package/src/core/path-rules.ts +53 -0
- package/src/core/project-affinity-surface.ts +102 -7
- package/src/core/project-profile-apply-state.ts +268 -0
- package/src/core/project-profile-apply-validation.ts +95 -0
- package/src/core/project-profile-apply.ts +408 -0
- package/src/core/project-profile-canonical.ts +71 -0
- package/src/core/project-profile-diff.ts +302 -0
- package/src/core/project-profile-discovery.ts +519 -0
- package/src/core/project-profile-file.ts +37 -0
- package/src/core/project-profile-parser.ts +98 -0
- package/src/core/project-profile.ts +490 -0
- package/src/core/retrieval-replay-candidate.ts +6 -1
- package/src/ingestion/sync-options.ts +6 -2
- package/src/ingestion/sync.ts +21 -29
- package/src/ingestion/types.ts +1 -1
- package/src/ingestion/walker.ts +85 -44
- package/src/llm/cache.ts +21 -0
- package/src/mcp/tools/ask.ts +1 -0
- package/src/mcp/tools/index.ts +4 -0
- package/src/mcp/tools/query.ts +4 -2
- package/src/mcp/tools/search.ts +3 -0
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/content-type-boost.ts +264 -0
- package/src/pipeline/diagnose.ts +46 -19
- package/src/pipeline/explain.ts +15 -2
- package/src/pipeline/hybrid.ts +170 -74
- package/src/pipeline/rerank.ts +45 -15
- package/src/pipeline/search.ts +29 -11
- package/src/pipeline/types.ts +13 -4
- package/src/pipeline/vsearch.ts +30 -10
- package/src/sdk/client.ts +19 -3
- package/src/sdk/index.ts +1 -0
- package/src/sdk/types.ts +21 -5
- package/src/serve/config-sync.ts +2 -2
- package/src/serve/resident-runtime.ts +1 -0
- package/src/serve/routes/api.ts +18 -3
- package/src/serve/status-model.ts +2 -0
- package/src/serve/status.ts +4 -0
- package/src/store/migrations/021-multi-context-identity.ts +37 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +86 -0
- package/src/store/types.ts +3 -2
- package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +0 -1
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// node:fs/promises provides realpath/stat; Bun has no equivalent for symlink-safe path identity and regular-file metadata.
|
|
2
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
|
+
// node:path provides cross-platform path operations; Bun has no path utilities.
|
|
4
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { Config, ModelPreset } from "../config/types";
|
|
7
|
+
|
|
8
|
+
import { normalizeContentTypes } from "../config/content-types";
|
|
9
|
+
import { createDefaultConfig } from "../config/defaults";
|
|
10
|
+
import {
|
|
11
|
+
PROJECT_PROFILE_FORCED_EXCLUDES,
|
|
12
|
+
PROJECT_PROFILE_SCHEMA_VERSION,
|
|
13
|
+
type ProjectProfile,
|
|
14
|
+
} from "../config/project-profile";
|
|
15
|
+
import { parseModelUri } from "../llm/cache";
|
|
16
|
+
import { getPreset } from "../llm/registry";
|
|
17
|
+
import { canonicalOperationalPath } from "./config-write-lock";
|
|
18
|
+
import { normalizePersistedContextText } from "./context-identity";
|
|
19
|
+
import { hasLikelySecretPath } from "./path-rules";
|
|
20
|
+
import {
|
|
21
|
+
canonicalProjectProfileJson,
|
|
22
|
+
compareCodeUnits,
|
|
23
|
+
fingerprintProjectProfileState,
|
|
24
|
+
normalizeLogicalPath,
|
|
25
|
+
projectProfileIncludePattern,
|
|
26
|
+
sha256,
|
|
27
|
+
sortedUnique,
|
|
28
|
+
} from "./project-profile-canonical";
|
|
29
|
+
import { parseProjectProfile } from "./project-profile-parser";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
canonicalProjectProfileJson,
|
|
33
|
+
fingerprintProjectProfileState,
|
|
34
|
+
projectProfileIncludePattern,
|
|
35
|
+
} from "./project-profile-canonical";
|
|
36
|
+
export type ProjectProfileDiagnosticCode =
|
|
37
|
+
| "COLLECTION_ROOT_NOT_DIRECTORY"
|
|
38
|
+
| "CONTEXT_FILE_INVALID"
|
|
39
|
+
| "CONTEXT_FILE_NOT_REGULAR"
|
|
40
|
+
| "CONTEXT_FILE_TOO_LARGE"
|
|
41
|
+
| "CONTEXT_FILE_UNREADABLE"
|
|
42
|
+
| "CONTEXT_TEXT_EMPTY"
|
|
43
|
+
| "INVALID_PROFILE"
|
|
44
|
+
| "MIGRATION_REQUIRED"
|
|
45
|
+
| "MODEL_CACHE_CHECK_FAILED"
|
|
46
|
+
| "MODEL_PATH_OVERLAP"
|
|
47
|
+
| "MODEL_PRESET_NOT_FOUND"
|
|
48
|
+
| "MODEL_PRESET_UNAVAILABLE_OFFLINE"
|
|
49
|
+
| "PATH_NOT_FOUND"
|
|
50
|
+
| "SYMLINK_ESCAPE"
|
|
51
|
+
| "UNSAFE_PATH"
|
|
52
|
+
| "UNSUPPORTED_SCHEMA_MAJOR"
|
|
53
|
+
| "UNSUPPORTED_SCHEMA_MINOR";
|
|
54
|
+
|
|
55
|
+
export interface ProjectProfileDiagnostic {
|
|
56
|
+
code: ProjectProfileDiagnosticCode;
|
|
57
|
+
severity: "error" | "warning";
|
|
58
|
+
path: string;
|
|
59
|
+
message: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ProjectProfileContextState {
|
|
63
|
+
scopeType: "collection";
|
|
64
|
+
scopeKey: string;
|
|
65
|
+
text: string;
|
|
66
|
+
source:
|
|
67
|
+
| { kind: "inline"; sha256: string }
|
|
68
|
+
| { kind: "file"; path: string; sha256: string };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ProjectProfileDesiredState {
|
|
72
|
+
schemaVersion: typeof PROJECT_PROFILE_SCHEMA_VERSION;
|
|
73
|
+
collection: {
|
|
74
|
+
name: string;
|
|
75
|
+
root: string;
|
|
76
|
+
include: string[];
|
|
77
|
+
exclude: string[];
|
|
78
|
+
languageHint?: string;
|
|
79
|
+
modelPreset?: string;
|
|
80
|
+
};
|
|
81
|
+
contexts: ProjectProfileContextState[];
|
|
82
|
+
contentTypes: Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
prefixes: string[];
|
|
85
|
+
preset: string;
|
|
86
|
+
graphHints?: string[];
|
|
87
|
+
searchBoost?: number;
|
|
88
|
+
temporal?: boolean;
|
|
89
|
+
}>;
|
|
90
|
+
affinityDefaults: {
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
contribution: number;
|
|
93
|
+
};
|
|
94
|
+
recommendedCapabilities: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ResolvedProjectProfilePaths {
|
|
98
|
+
profileRoot: string;
|
|
99
|
+
collectionRoot: string;
|
|
100
|
+
contextFiles: Array<{
|
|
101
|
+
logicalPath: string;
|
|
102
|
+
absolutePath: string;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface CompiledProjectProfile {
|
|
107
|
+
profile: ProjectProfile;
|
|
108
|
+
desiredState: ProjectProfileDesiredState;
|
|
109
|
+
canonicalJson: string;
|
|
110
|
+
fingerprint: string;
|
|
111
|
+
resolvedPaths: ResolvedProjectProfilePaths;
|
|
112
|
+
diagnostics: ProjectProfileDiagnostic[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type CompileProjectProfileResult =
|
|
116
|
+
| { ok: true; value: CompiledProjectProfile }
|
|
117
|
+
| { ok: false; diagnostics: ProjectProfileDiagnostic[] };
|
|
118
|
+
|
|
119
|
+
export interface ProjectProfileCompilerOptions {
|
|
120
|
+
profileRoot: string;
|
|
121
|
+
config?: Config;
|
|
122
|
+
isModelAvailableOffline?: (
|
|
123
|
+
modelUri: string,
|
|
124
|
+
modelType: "embed" | "rerank" | "expand" | "gen"
|
|
125
|
+
) => Promise<boolean>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const PROJECT_PROFILE_CONTEXT_FILE_MAX_BYTES = 65_536;
|
|
129
|
+
|
|
130
|
+
const isContained = (parent: string, candidate: string): boolean => {
|
|
131
|
+
const pathFromParent = relative(parent, candidate);
|
|
132
|
+
return (
|
|
133
|
+
pathFromParent === "" ||
|
|
134
|
+
(pathFromParent !== ".." &&
|
|
135
|
+
!pathFromParent.startsWith(`..${sep}`) &&
|
|
136
|
+
!isAbsolute(pathFromParent))
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const resolveContainedPath = async (
|
|
141
|
+
profileRoot: string,
|
|
142
|
+
logicalPath: string,
|
|
143
|
+
diagnosticPath: string
|
|
144
|
+
): Promise<
|
|
145
|
+
| {
|
|
146
|
+
ok: true;
|
|
147
|
+
absolutePath: string;
|
|
148
|
+
metadata: Awaited<ReturnType<typeof stat>>;
|
|
149
|
+
}
|
|
150
|
+
| { ok: false; diagnostic: ProjectProfileDiagnostic }
|
|
151
|
+
> => {
|
|
152
|
+
const candidate = resolve(profileRoot, normalizeLogicalPath(logicalPath));
|
|
153
|
+
let absolutePath: string;
|
|
154
|
+
let metadata: Awaited<ReturnType<typeof stat>>;
|
|
155
|
+
try {
|
|
156
|
+
absolutePath = await realpath(candidate);
|
|
157
|
+
metadata = await stat(absolutePath);
|
|
158
|
+
} catch {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
diagnostic: {
|
|
162
|
+
code: "PATH_NOT_FOUND",
|
|
163
|
+
severity: "error",
|
|
164
|
+
path: diagnosticPath,
|
|
165
|
+
message: "Referenced project path does not exist.",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (!isContained(profileRoot, absolutePath)) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
diagnostic: {
|
|
173
|
+
code: "SYMLINK_ESCAPE",
|
|
174
|
+
severity: "error",
|
|
175
|
+
path: diagnosticPath,
|
|
176
|
+
message:
|
|
177
|
+
"Referenced project path resolves outside the trusted profile root.",
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return { ok: true, absolutePath, metadata };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const modelUris = (
|
|
185
|
+
preset: ModelPreset
|
|
186
|
+
): Array<["embed" | "rerank" | "expand" | "gen", string]> => [
|
|
187
|
+
["embed", preset.embed],
|
|
188
|
+
["rerank", preset.rerank],
|
|
189
|
+
["expand", preset.expand ?? preset.gen],
|
|
190
|
+
["gen", preset.gen],
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const diagnoseModelPreset = async (
|
|
194
|
+
profile: ProjectProfile,
|
|
195
|
+
options: ProjectProfileCompilerOptions,
|
|
196
|
+
profileRoot: string
|
|
197
|
+
): Promise<ProjectProfileDiagnostic[]> => {
|
|
198
|
+
const presetId = profile.collection.modelPreset;
|
|
199
|
+
if (!presetId) return [];
|
|
200
|
+
const preset = getPreset(options.config ?? createDefaultConfig(), presetId);
|
|
201
|
+
if (!preset) {
|
|
202
|
+
return [
|
|
203
|
+
{
|
|
204
|
+
code: "MODEL_PRESET_NOT_FOUND",
|
|
205
|
+
severity: "error",
|
|
206
|
+
path: "collection.modelPreset",
|
|
207
|
+
message: `Model preset alias "${presetId}" is not configured.`,
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
for (const [, uri] of modelUris(preset)) {
|
|
212
|
+
const parsed = parseModelUri(uri);
|
|
213
|
+
if (!parsed.ok || parsed.value.scheme !== "file") continue;
|
|
214
|
+
const modelPath = await canonicalOperationalPath(parsed.value.file);
|
|
215
|
+
if (isContained(profileRoot, modelPath)) {
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
code: "MODEL_PATH_OVERLAP",
|
|
219
|
+
severity: "error",
|
|
220
|
+
path: "collection.modelPreset",
|
|
221
|
+
message:
|
|
222
|
+
"The selected model preset resolves runtime model state inside the project profile root.",
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!options.isModelAvailableOffline) return [];
|
|
228
|
+
|
|
229
|
+
const unavailable: string[] = [];
|
|
230
|
+
try {
|
|
231
|
+
for (const [modelType, uri] of modelUris(preset)) {
|
|
232
|
+
if (!(await options.isModelAvailableOffline(uri, modelType))) {
|
|
233
|
+
unavailable.push(modelType);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
code: "MODEL_CACHE_CHECK_FAILED",
|
|
240
|
+
severity: "error",
|
|
241
|
+
path: "collection.modelPreset",
|
|
242
|
+
message: `Offline cache availability could not be verified for model preset alias "${presetId}".`,
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
if (unavailable.length === 0) return [];
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
code: "MODEL_PRESET_UNAVAILABLE_OFFLINE",
|
|
250
|
+
severity: "error",
|
|
251
|
+
path: "collection.modelPreset",
|
|
252
|
+
message: `Model preset alias "${presetId}" is unavailable offline for: ${unavailable.join(", ")}.`,
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export async function compileProjectProfileYaml(
|
|
258
|
+
yaml: string,
|
|
259
|
+
options: ProjectProfileCompilerOptions
|
|
260
|
+
): Promise<CompileProjectProfileResult> {
|
|
261
|
+
const parsed = parseProjectProfile(yaml);
|
|
262
|
+
if ("ok" in parsed) return parsed;
|
|
263
|
+
const profile = parsed;
|
|
264
|
+
const diagnostics: ProjectProfileDiagnostic[] = [];
|
|
265
|
+
|
|
266
|
+
let profileRoot: string;
|
|
267
|
+
try {
|
|
268
|
+
profileRoot = await realpath(options.profileRoot);
|
|
269
|
+
} catch {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
diagnostics: [
|
|
273
|
+
{
|
|
274
|
+
code: "PATH_NOT_FOUND",
|
|
275
|
+
severity: "error",
|
|
276
|
+
path: "profileRoot",
|
|
277
|
+
message: "Trusted project profile root does not exist.",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const collectionRoot = await resolveContainedPath(
|
|
284
|
+
profileRoot,
|
|
285
|
+
profile.collection.root,
|
|
286
|
+
"collection.root"
|
|
287
|
+
);
|
|
288
|
+
if (!collectionRoot.ok) diagnostics.push(collectionRoot.diagnostic);
|
|
289
|
+
else if (!collectionRoot.metadata.isDirectory()) {
|
|
290
|
+
diagnostics.push({
|
|
291
|
+
code: "COLLECTION_ROOT_NOT_DIRECTORY",
|
|
292
|
+
severity: "error",
|
|
293
|
+
path: "collection.root",
|
|
294
|
+
message: "The collection root must resolve to a directory.",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const contexts: ProjectProfileContextState[] = [];
|
|
299
|
+
const contextFiles: ResolvedProjectProfilePaths["contextFiles"] = [];
|
|
300
|
+
for (const [index, context] of profile.contexts.entries()) {
|
|
301
|
+
if ("text" in context) {
|
|
302
|
+
const normalizedText = normalizePersistedContextText(context.text);
|
|
303
|
+
if (!normalizedText) {
|
|
304
|
+
diagnostics.push({
|
|
305
|
+
code: "CONTEXT_TEXT_EMPTY",
|
|
306
|
+
severity: "error",
|
|
307
|
+
path: `contexts[${index}].text`,
|
|
308
|
+
message: "Context text must not be empty after normalization.",
|
|
309
|
+
});
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
contexts.push({
|
|
313
|
+
scopeType: "collection",
|
|
314
|
+
scopeKey: `${profile.collection.name}:`,
|
|
315
|
+
text: normalizedText,
|
|
316
|
+
source: { kind: "inline", sha256: sha256(context.text) },
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const resolved = await resolveContainedPath(
|
|
322
|
+
profileRoot,
|
|
323
|
+
context.file,
|
|
324
|
+
`contexts[${index}].file`
|
|
325
|
+
);
|
|
326
|
+
if (!resolved.ok) {
|
|
327
|
+
diagnostics.push(resolved.diagnostic);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const resolvedLogicalPath = normalizeLogicalPath(
|
|
331
|
+
relative(profileRoot, resolved.absolutePath)
|
|
332
|
+
);
|
|
333
|
+
if (hasLikelySecretPath(resolvedLogicalPath)) {
|
|
334
|
+
diagnostics.push({
|
|
335
|
+
code: "UNSAFE_PATH",
|
|
336
|
+
severity: "error",
|
|
337
|
+
path: `contexts[${index}].file`,
|
|
338
|
+
message:
|
|
339
|
+
"Context input resolves to a likely credential or secret file.",
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!resolved.metadata.isFile()) {
|
|
344
|
+
diagnostics.push({
|
|
345
|
+
code: "CONTEXT_FILE_NOT_REGULAR",
|
|
346
|
+
severity: "error",
|
|
347
|
+
path: `contexts[${index}].file`,
|
|
348
|
+
message: "Context input must resolve to a regular file.",
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (resolved.metadata.size > PROJECT_PROFILE_CONTEXT_FILE_MAX_BYTES) {
|
|
353
|
+
diagnostics.push({
|
|
354
|
+
code: "CONTEXT_FILE_TOO_LARGE",
|
|
355
|
+
severity: "error",
|
|
356
|
+
path: `contexts[${index}].file`,
|
|
357
|
+
message: `Context file exceeds ${PROJECT_PROFILE_CONTEXT_FILE_MAX_BYTES} bytes.`,
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
let bytes: Uint8Array;
|
|
362
|
+
try {
|
|
363
|
+
bytes = new Uint8Array(
|
|
364
|
+
await Bun.file(resolved.absolutePath)
|
|
365
|
+
.slice(0, PROJECT_PROFILE_CONTEXT_FILE_MAX_BYTES + 1)
|
|
366
|
+
.arrayBuffer()
|
|
367
|
+
);
|
|
368
|
+
} catch {
|
|
369
|
+
diagnostics.push({
|
|
370
|
+
code: "CONTEXT_FILE_UNREADABLE",
|
|
371
|
+
severity: "error",
|
|
372
|
+
path: `contexts[${index}].file`,
|
|
373
|
+
message: "Context file could not be read.",
|
|
374
|
+
});
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (bytes.byteLength > PROJECT_PROFILE_CONTEXT_FILE_MAX_BYTES) {
|
|
378
|
+
diagnostics.push({
|
|
379
|
+
code: "CONTEXT_FILE_TOO_LARGE",
|
|
380
|
+
severity: "error",
|
|
381
|
+
path: `contexts[${index}].file`,
|
|
382
|
+
message: `Context file exceeds ${PROJECT_PROFILE_CONTEXT_FILE_MAX_BYTES} bytes.`,
|
|
383
|
+
});
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
let text: string;
|
|
387
|
+
try {
|
|
388
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
389
|
+
} catch {
|
|
390
|
+
diagnostics.push({
|
|
391
|
+
code: "CONTEXT_FILE_INVALID",
|
|
392
|
+
severity: "error",
|
|
393
|
+
path: `contexts[${index}].file`,
|
|
394
|
+
message: "Context file is not valid UTF-8.",
|
|
395
|
+
});
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const logicalPath = normalizeLogicalPath(context.file);
|
|
399
|
+
const normalizedText = normalizePersistedContextText(text);
|
|
400
|
+
if (!normalizedText) {
|
|
401
|
+
diagnostics.push({
|
|
402
|
+
code: "CONTEXT_TEXT_EMPTY",
|
|
403
|
+
severity: "error",
|
|
404
|
+
path: `contexts[${index}].file`,
|
|
405
|
+
message: "Context file must not be empty after normalization.",
|
|
406
|
+
});
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
contexts.push({
|
|
410
|
+
scopeType: "collection",
|
|
411
|
+
scopeKey: `${profile.collection.name}:`,
|
|
412
|
+
text: normalizedText,
|
|
413
|
+
source: { kind: "file", path: logicalPath, sha256: sha256(bytes) },
|
|
414
|
+
});
|
|
415
|
+
contextFiles.push({
|
|
416
|
+
logicalPath,
|
|
417
|
+
absolutePath: resolved.absolutePath,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
diagnostics.push(
|
|
422
|
+
...(await diagnoseModelPreset(profile, options, profileRoot))
|
|
423
|
+
);
|
|
424
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
425
|
+
return { ok: false, diagnostics };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const normalizedContentTypes = normalizeContentTypes(
|
|
429
|
+
Object.entries(profile.contentTypes).map(([id, rule]) => ({ id, ...rule }))
|
|
430
|
+
).rules.map((rule) => ({
|
|
431
|
+
id: rule.id,
|
|
432
|
+
prefixes: sortedUnique(rule.prefixes.map(normalizeLogicalPath)),
|
|
433
|
+
preset: rule.preset,
|
|
434
|
+
...(rule.graphHints ? { graphHints: sortedUnique(rule.graphHints) } : {}),
|
|
435
|
+
...(rule.searchBoost === undefined
|
|
436
|
+
? {}
|
|
437
|
+
: { searchBoost: rule.searchBoost }),
|
|
438
|
+
...(rule.temporal === undefined ? {} : { temporal: rule.temporal }),
|
|
439
|
+
}));
|
|
440
|
+
|
|
441
|
+
const desiredState: ProjectProfileDesiredState = {
|
|
442
|
+
schemaVersion: PROJECT_PROFILE_SCHEMA_VERSION,
|
|
443
|
+
collection: {
|
|
444
|
+
name: profile.collection.name,
|
|
445
|
+
root: normalizeLogicalPath(profile.collection.root),
|
|
446
|
+
include: sortedUnique(
|
|
447
|
+
profile.collection.include.map(normalizeLogicalPath)
|
|
448
|
+
),
|
|
449
|
+
exclude: sortedUnique([
|
|
450
|
+
...profile.collection.exclude.map(normalizeLogicalPath),
|
|
451
|
+
...PROJECT_PROFILE_FORCED_EXCLUDES,
|
|
452
|
+
]),
|
|
453
|
+
...(profile.collection.languageHint
|
|
454
|
+
? { languageHint: profile.collection.languageHint }
|
|
455
|
+
: {}),
|
|
456
|
+
...(profile.collection.modelPreset
|
|
457
|
+
? { modelPreset: profile.collection.modelPreset }
|
|
458
|
+
: {}),
|
|
459
|
+
},
|
|
460
|
+
contexts: contexts.sort((left, right) =>
|
|
461
|
+
compareCodeUnits(
|
|
462
|
+
canonicalProjectProfileJson(left),
|
|
463
|
+
canonicalProjectProfileJson(right)
|
|
464
|
+
)
|
|
465
|
+
),
|
|
466
|
+
contentTypes: normalizedContentTypes,
|
|
467
|
+
affinityDefaults: profile.affinityDefaults,
|
|
468
|
+
recommendedCapabilities: sortedUnique(profile.recommendedCapabilities),
|
|
469
|
+
};
|
|
470
|
+
const canonicalJson = canonicalProjectProfileJson(desiredState);
|
|
471
|
+
return {
|
|
472
|
+
ok: true,
|
|
473
|
+
value: {
|
|
474
|
+
profile,
|
|
475
|
+
desiredState,
|
|
476
|
+
canonicalJson,
|
|
477
|
+
fingerprint: fingerprintProjectProfileState(desiredState),
|
|
478
|
+
resolvedPaths: {
|
|
479
|
+
profileRoot,
|
|
480
|
+
collectionRoot: collectionRoot.ok
|
|
481
|
+
? collectionRoot.absolutePath
|
|
482
|
+
: profileRoot,
|
|
483
|
+
contextFiles: contextFiles.sort((left, right) =>
|
|
484
|
+
compareCodeUnits(left.logicalPath, right.logicalPath)
|
|
485
|
+
),
|
|
486
|
+
},
|
|
487
|
+
diagnostics,
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
@@ -12,6 +12,7 @@ import type { RetrievalQrelsCase } from "./retrieval-qrels";
|
|
|
12
12
|
import type { RetrievalReplayCandidate } from "./retrieval-replay-types";
|
|
13
13
|
|
|
14
14
|
import { parseUri } from "../app/constants";
|
|
15
|
+
import { normalizeContentTypes } from "../config";
|
|
15
16
|
import { searchHybrid } from "../pipeline/hybrid";
|
|
16
17
|
import { searchBm25 } from "../pipeline/search";
|
|
17
18
|
import { SEARCH_RESULTS_TRACE_METADATA } from "../pipeline/types";
|
|
@@ -145,7 +146,11 @@ const runCandidateOnce = async (
|
|
|
145
146
|
options: HybridSearchOptions
|
|
146
147
|
): Promise<StoreResult<SearchResults>> => {
|
|
147
148
|
if (candidate.type === "bm25") {
|
|
148
|
-
return searchBm25(deps.store, source.query.text,
|
|
149
|
+
return searchBm25(deps.store, source.query.text, {
|
|
150
|
+
...options,
|
|
151
|
+
contentTypeRules: normalizeContentTypes(deps.config.contentTypes ?? [])
|
|
152
|
+
.rules,
|
|
153
|
+
});
|
|
149
154
|
}
|
|
150
155
|
if (candidate.type === "vector") {
|
|
151
156
|
if (!(deps.vectorIndex && deps.embedPort)) {
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
import type { Config, NormalizedContentTypeRule } from "../config";
|
|
8
8
|
import type { SyncOptions } from "./types";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
fingerprintContentTypeMetadataRules,
|
|
12
|
+
normalizeContentTypes,
|
|
13
|
+
} from "../config";
|
|
11
14
|
|
|
12
15
|
export function resolveContentTypeRules(
|
|
13
16
|
config?: Pick<Config, "contentTypes">
|
|
@@ -24,6 +27,7 @@ export function withContentTypeRules(
|
|
|
24
27
|
...options,
|
|
25
28
|
contentTypeRules: rules,
|
|
26
29
|
contentTypeRulesFingerprint:
|
|
27
|
-
options.contentTypeRulesFingerprint ??
|
|
30
|
+
options.contentTypeRulesFingerprint ??
|
|
31
|
+
fingerprintContentTypeMetadataRules(rules),
|
|
28
32
|
};
|
|
29
33
|
}
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -33,7 +33,10 @@ import type {
|
|
|
33
33
|
WalkerPort,
|
|
34
34
|
} from "./types";
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
fingerprintContentTypeMetadataRules,
|
|
38
|
+
resolveContentTypeRule,
|
|
39
|
+
} from "../config";
|
|
37
40
|
import { getDefaultMimeDetector, type MimeDetector } from "../converters/mime";
|
|
38
41
|
import {
|
|
39
42
|
type ConversionPipeline,
|
|
@@ -80,7 +83,8 @@ const MAX_CONCURRENCY = 16;
|
|
|
80
83
|
* Documents with ingestVersion < INGEST_VERSION will be re-processed.
|
|
81
84
|
*/
|
|
82
85
|
export const INGEST_VERSION = 6;
|
|
83
|
-
const EMPTY_CONTENT_TYPE_RULES_FINGERPRINT =
|
|
86
|
+
const EMPTY_CONTENT_TYPE_RULES_FINGERPRINT =
|
|
87
|
+
fingerprintContentTypeMetadataRules([]);
|
|
84
88
|
const RELATION_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
85
89
|
const PROJECTION_YIELD_INTERVAL = 25;
|
|
86
90
|
const NON_RETRYABLE_CONVERSION_ERROR_CODES = new Set([
|
|
@@ -401,18 +405,6 @@ function parseCategories(input: unknown): string[] {
|
|
|
401
405
|
return [];
|
|
402
406
|
}
|
|
403
407
|
|
|
404
|
-
function matchPrefixContentType(
|
|
405
|
-
relPath: string,
|
|
406
|
-
rules: NormalizedContentTypeRule[]
|
|
407
|
-
): string | undefined {
|
|
408
|
-
for (const rule of rules) {
|
|
409
|
-
if (rule.prefixes.some((prefix) => relPath.startsWith(prefix))) {
|
|
410
|
-
return rule.id;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return undefined;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
408
|
export function extractDocumentMetadata(
|
|
417
409
|
markdown: string,
|
|
418
410
|
relPath: string,
|
|
@@ -421,23 +413,23 @@ export function extractDocumentMetadata(
|
|
|
421
413
|
): DocumentMetadata {
|
|
422
414
|
const parsed = parseFrontmatter(markdown);
|
|
423
415
|
const metadata = parsed.metadata;
|
|
424
|
-
const typedRules = new Map(contentTypeRules.map((rule) => [rule.id, rule]));
|
|
425
416
|
const rawFrontmatterType =
|
|
426
417
|
typeof metadata.type === "string"
|
|
427
418
|
? normalizeFrontmatterScalar(metadata.type)
|
|
428
419
|
: "";
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
420
|
+
const configuredRule = resolveContentTypeRule(
|
|
421
|
+
rawFrontmatterType,
|
|
422
|
+
relPath,
|
|
423
|
+
contentTypeRules
|
|
424
|
+
);
|
|
434
425
|
const inferred = inferPathContentType(relPath, ext);
|
|
435
|
-
const contentType =
|
|
436
|
-
const contentTypeSource: ContentTypeSource =
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
426
|
+
const contentType = configuredRule?.rule.id ?? inferred.contentType;
|
|
427
|
+
const contentTypeSource: ContentTypeSource =
|
|
428
|
+
configuredRule?.source === "configured-id"
|
|
429
|
+
? "frontmatter-type"
|
|
430
|
+
: configuredRule?.source === "prefix"
|
|
431
|
+
? "prefix"
|
|
432
|
+
: inferred.source;
|
|
441
433
|
const categories = new Set<string>([contentType]);
|
|
442
434
|
|
|
443
435
|
const fmCategories = parseCategories(
|
|
@@ -679,7 +671,7 @@ export class SyncService {
|
|
|
679
671
|
const contentTypeRules = options.contentTypeRules ?? [];
|
|
680
672
|
const contentTypeRulesFingerprint =
|
|
681
673
|
options.contentTypeRulesFingerprint ??
|
|
682
|
-
|
|
674
|
+
fingerprintContentTypeMetadataRules(contentTypeRules);
|
|
683
675
|
|
|
684
676
|
// 4. Check existing doc for skip/repair decision
|
|
685
677
|
const existingResult = await store.getDocument(
|
|
@@ -1066,7 +1058,7 @@ export class SyncService {
|
|
|
1066
1058
|
contentTypeRules: options.contentTypeRules ?? [],
|
|
1067
1059
|
contentTypeRulesFingerprint:
|
|
1068
1060
|
options.contentTypeRulesFingerprint ??
|
|
1069
|
-
|
|
1061
|
+
fingerprintContentTypeMetadataRules(options.contentTypeRules ?? []),
|
|
1070
1062
|
};
|
|
1071
1063
|
const results: FileSyncResult[] = [];
|
|
1072
1064
|
const projectionSourceIds = new Set<number>();
|
|
@@ -1411,7 +1403,7 @@ export class SyncService {
|
|
|
1411
1403
|
contentTypeRules: options.contentTypeRules ?? [],
|
|
1412
1404
|
contentTypeRulesFingerprint:
|
|
1413
1405
|
options.contentTypeRulesFingerprint ??
|
|
1414
|
-
|
|
1406
|
+
fingerprintContentTypeMetadataRules(options.contentTypeRules ?? []),
|
|
1415
1407
|
};
|
|
1416
1408
|
const errors: Array<{ relPath: string; code: string; message: string }> =
|
|
1417
1409
|
[];
|
package/src/ingestion/types.ts
CHANGED
|
@@ -135,7 +135,7 @@ export interface SyncOptions {
|
|
|
135
135
|
concurrency?: number;
|
|
136
136
|
/** Normalized content type rules from config.contentTypes. */
|
|
137
137
|
contentTypeRules?: NormalizedContentTypeRule[];
|
|
138
|
-
/** Stable hash of
|
|
138
|
+
/** Stable hash of metadata-affecting rules, used for re-derivation. */
|
|
139
139
|
contentTypeRulesFingerprint?: string;
|
|
140
140
|
/** Internal orchestration flag: defer graph projection to an outer sync. */
|
|
141
141
|
projectTypedEdges?: boolean;
|