@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,374 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { NOTE_PRESETS } from "../core/note-presets";
|
|
4
|
+
import { hasLikelySecretPath } from "../core/path-rules";
|
|
5
|
+
import {
|
|
6
|
+
CONTENT_TYPE_SEARCH_BOOST_MAX,
|
|
7
|
+
CONTENT_TYPE_SEARCH_BOOST_MIN,
|
|
8
|
+
isValidLanguageHint,
|
|
9
|
+
PROJECT_AFFINITY_MAX_CONTRIBUTION,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export const PROJECT_PROFILE_SCHEMA_VERSION = "1.0" as const;
|
|
13
|
+
export const PROJECT_PROFILE_FINGERPRINT_DOMAIN = "gno-project-profile\0v1.0\0";
|
|
14
|
+
export const PROJECT_PROFILE_FORCED_EXCLUDES = [".gno"] as const;
|
|
15
|
+
|
|
16
|
+
const COLLECTION_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
17
|
+
const REFERENCE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
18
|
+
const CAPABILITY_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
19
|
+
const WINDOWS_DRIVE_PATH_PATTERN = /^[a-z]:/i;
|
|
20
|
+
const WINDOWS_UNC_PATH_PATTERN = /^(?:\\\\|\/\/)/;
|
|
21
|
+
const ENV_EXPANSION_PATTERN =
|
|
22
|
+
/(?:\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*|%[A-Za-z_][A-Za-z0-9_]*%)/;
|
|
23
|
+
const GLOB_META_PATTERN = /[*?[\]{}]/;
|
|
24
|
+
const WINDOWS_INVALID_COMPONENT_PATTERN = /[<>:"|]/;
|
|
25
|
+
const WINDOWS_RESERVED_COMPONENT_PATTERN =
|
|
26
|
+
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
27
|
+
const RUNTIME_PATH_PATTERN =
|
|
28
|
+
/(?:^|\/)\.gno(?:\/|$)|\.(?:db|sqlite|sqlite3|gguf|lock)$/i;
|
|
29
|
+
const NOTE_PRESET_IDS = new Set<string>(
|
|
30
|
+
NOTE_PRESETS.map((preset) => preset.id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const globStructureIssue = (value: string): string | null => {
|
|
34
|
+
if (value.startsWith("!")) {
|
|
35
|
+
return "must not use whole-pattern negation";
|
|
36
|
+
}
|
|
37
|
+
let braceDepth = 0;
|
|
38
|
+
let bracketDepth = 0;
|
|
39
|
+
for (const character of value) {
|
|
40
|
+
if (character === "{") braceDepth += 1;
|
|
41
|
+
else if (character === "}") {
|
|
42
|
+
braceDepth -= 1;
|
|
43
|
+
if (braceDepth < 0) return "contains an unmatched closing brace";
|
|
44
|
+
} else if (character === "[") bracketDepth += 1;
|
|
45
|
+
else if (character === "]") {
|
|
46
|
+
bracketDepth -= 1;
|
|
47
|
+
if (bracketDepth < 0) return "contains an unmatched closing bracket";
|
|
48
|
+
}
|
|
49
|
+
if (braceDepth > 16 || bracketDepth > 16) {
|
|
50
|
+
return "contains excessively nested glob groups";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (braceDepth !== 0) return "contains an unmatched opening brace";
|
|
54
|
+
if (bracketDepth !== 0) return "contains an unmatched opening bracket";
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const windowsComponentIssue = (
|
|
59
|
+
normalized: string,
|
|
60
|
+
allowGlob: boolean
|
|
61
|
+
): string | null => {
|
|
62
|
+
const components: string[] = [];
|
|
63
|
+
let component = "";
|
|
64
|
+
let braceDepth = 0;
|
|
65
|
+
const pushComponent = (): void => {
|
|
66
|
+
if (component !== "") components.push(component);
|
|
67
|
+
component = "";
|
|
68
|
+
};
|
|
69
|
+
for (const character of normalized) {
|
|
70
|
+
const isBraceSeparator =
|
|
71
|
+
character === "{" ||
|
|
72
|
+
character === "}" ||
|
|
73
|
+
(character === "," && braceDepth > 0);
|
|
74
|
+
if (character === "/" || isBraceSeparator) {
|
|
75
|
+
pushComponent();
|
|
76
|
+
if (character === "{") braceDepth += 1;
|
|
77
|
+
else if (character === "}") braceDepth -= 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
component += character;
|
|
81
|
+
}
|
|
82
|
+
pushComponent();
|
|
83
|
+
|
|
84
|
+
for (const component of components) {
|
|
85
|
+
if (component === "." || component === "") continue;
|
|
86
|
+
let hasControlCharacter = false;
|
|
87
|
+
for (const character of component) {
|
|
88
|
+
if ((character.codePointAt(0) ?? 0) < 32) {
|
|
89
|
+
hasControlCharacter = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
hasControlCharacter ||
|
|
95
|
+
WINDOWS_INVALID_COMPONENT_PATTERN.test(component)
|
|
96
|
+
) {
|
|
97
|
+
return "contains characters invalid in Windows path components";
|
|
98
|
+
}
|
|
99
|
+
if (component.endsWith(".") || component.endsWith(" ")) {
|
|
100
|
+
return "contains a component ending in a dot or space";
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
(!allowGlob || !GLOB_META_PATTERN.test(component)) &&
|
|
104
|
+
WINDOWS_RESERVED_COMPONENT_PATTERN.test(component)
|
|
105
|
+
) {
|
|
106
|
+
return "contains a Windows-reserved path component";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const portablePathIssue = (
|
|
113
|
+
value: string,
|
|
114
|
+
options: { allowGlob: boolean; allowRuntimeExclusion: boolean }
|
|
115
|
+
): string | null => {
|
|
116
|
+
if (value.includes("\0")) return "contains a NUL byte";
|
|
117
|
+
if (
|
|
118
|
+
value.startsWith("/") ||
|
|
119
|
+
value.startsWith("\\") ||
|
|
120
|
+
WINDOWS_UNC_PATH_PATTERN.test(value) ||
|
|
121
|
+
WINDOWS_DRIVE_PATH_PATTERN.test(value) ||
|
|
122
|
+
value.startsWith("~")
|
|
123
|
+
) {
|
|
124
|
+
return "must be repository-relative on every supported platform";
|
|
125
|
+
}
|
|
126
|
+
if (ENV_EXPANSION_PATTERN.test(value)) {
|
|
127
|
+
return "must not contain environment expansion";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const normalized = value.replaceAll("\\", "/");
|
|
131
|
+
if (normalized.split("/").includes("..")) {
|
|
132
|
+
return "must not contain traversal segments";
|
|
133
|
+
}
|
|
134
|
+
const windowsIssue = windowsComponentIssue(normalized, options.allowGlob);
|
|
135
|
+
if (windowsIssue) return windowsIssue;
|
|
136
|
+
if (!options.allowGlob && GLOB_META_PATTERN.test(normalized)) {
|
|
137
|
+
return "must not contain glob metacharacters";
|
|
138
|
+
}
|
|
139
|
+
if (options.allowGlob) {
|
|
140
|
+
const globIssue = globStructureIssue(normalized);
|
|
141
|
+
if (globIssue) return globIssue;
|
|
142
|
+
if (normalized.includes("{") || normalized.includes("}")) {
|
|
143
|
+
return "must not use brace alternatives; use separate include or exclude entries";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!options.allowRuntimeExclusion && RUNTIME_PATH_PATTERN.test(normalized)) {
|
|
147
|
+
return "must not reference GNO runtime state";
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const portablePathSchema = (
|
|
153
|
+
options: { allowGlob?: boolean; allowRuntimeExclusion?: boolean } = {}
|
|
154
|
+
) =>
|
|
155
|
+
z
|
|
156
|
+
.string()
|
|
157
|
+
.min(1)
|
|
158
|
+
.max(512)
|
|
159
|
+
.superRefine((value, context) => {
|
|
160
|
+
const issue = portablePathIssue(value, {
|
|
161
|
+
allowGlob: options.allowGlob ?? false,
|
|
162
|
+
allowRuntimeExclusion: options.allowRuntimeExclusion ?? false,
|
|
163
|
+
});
|
|
164
|
+
if (issue) {
|
|
165
|
+
context.addIssue({
|
|
166
|
+
code: "custom",
|
|
167
|
+
message: `UNSAFE_PATH: ${issue}`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const ProjectProfileCollectionSchema = z
|
|
173
|
+
.object({
|
|
174
|
+
name: z.string().regex(COLLECTION_NAME_PATTERN),
|
|
175
|
+
root: portablePathSchema().default("."),
|
|
176
|
+
include: z
|
|
177
|
+
.array(portablePathSchema({ allowGlob: true }))
|
|
178
|
+
.min(1)
|
|
179
|
+
.max(128)
|
|
180
|
+
.superRefine((values, context) => {
|
|
181
|
+
const seen = new Set<string>();
|
|
182
|
+
for (const [index, value] of values.entries()) {
|
|
183
|
+
if (seen.has(value)) {
|
|
184
|
+
context.addIssue({
|
|
185
|
+
code: "custom",
|
|
186
|
+
path: [index],
|
|
187
|
+
message: "Duplicate include rule",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
seen.add(value);
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.default(["**/*"]),
|
|
194
|
+
exclude: z
|
|
195
|
+
.array(
|
|
196
|
+
portablePathSchema({
|
|
197
|
+
allowGlob: true,
|
|
198
|
+
allowRuntimeExclusion: true,
|
|
199
|
+
})
|
|
200
|
+
)
|
|
201
|
+
.max(128)
|
|
202
|
+
.superRefine((values, context) => {
|
|
203
|
+
const seen = new Set<string>();
|
|
204
|
+
for (const [index, value] of values.entries()) {
|
|
205
|
+
if (seen.has(value)) {
|
|
206
|
+
context.addIssue({
|
|
207
|
+
code: "custom",
|
|
208
|
+
path: [index],
|
|
209
|
+
message: "Duplicate exclude rule",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
seen.add(value);
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
.default([]),
|
|
216
|
+
languageHint: z
|
|
217
|
+
.string()
|
|
218
|
+
.refine(isValidLanguageHint, "Invalid BCP-47 language hint")
|
|
219
|
+
.optional(),
|
|
220
|
+
modelPreset: z.string().regex(REFERENCE_PATTERN).optional(),
|
|
221
|
+
})
|
|
222
|
+
.strict();
|
|
223
|
+
|
|
224
|
+
const InlineProjectProfileContextSchema = z
|
|
225
|
+
.object({
|
|
226
|
+
text: z.string().min(1).max(65_536),
|
|
227
|
+
})
|
|
228
|
+
.strict();
|
|
229
|
+
|
|
230
|
+
const FileProjectProfileContextSchema = z
|
|
231
|
+
.object({
|
|
232
|
+
file: portablePathSchema().superRefine((value, context) => {
|
|
233
|
+
if (hasLikelySecretPath(value)) {
|
|
234
|
+
context.addIssue({
|
|
235
|
+
code: "custom",
|
|
236
|
+
message:
|
|
237
|
+
"UNSAFE_PATH: likely credential and secret files cannot be profile contexts",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}),
|
|
241
|
+
})
|
|
242
|
+
.strict();
|
|
243
|
+
|
|
244
|
+
export const ProjectProfileContextSchema = z.union([
|
|
245
|
+
InlineProjectProfileContextSchema,
|
|
246
|
+
FileProjectProfileContextSchema,
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
export const ProjectProfileContentTypeSchema = z
|
|
250
|
+
.object({
|
|
251
|
+
prefixes: z
|
|
252
|
+
.array(portablePathSchema())
|
|
253
|
+
.min(1)
|
|
254
|
+
.max(64)
|
|
255
|
+
.superRefine((values, context) => {
|
|
256
|
+
const seen = new Set<string>();
|
|
257
|
+
for (const [index, value] of values.entries()) {
|
|
258
|
+
if (seen.has(value)) {
|
|
259
|
+
context.addIssue({
|
|
260
|
+
code: "custom",
|
|
261
|
+
path: [index],
|
|
262
|
+
message: "Duplicate content type prefix",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
seen.add(value);
|
|
266
|
+
}
|
|
267
|
+
}),
|
|
268
|
+
preset: z
|
|
269
|
+
.string()
|
|
270
|
+
.min(1)
|
|
271
|
+
.refine((value) => NOTE_PRESET_IDS.has(value), "Unknown note preset"),
|
|
272
|
+
graphHints: z
|
|
273
|
+
.array(z.string().min(1).max(64))
|
|
274
|
+
.max(16)
|
|
275
|
+
.superRefine((values, context) => {
|
|
276
|
+
const seen = new Set<string>();
|
|
277
|
+
for (const [index, value] of values.entries()) {
|
|
278
|
+
if (seen.has(value)) {
|
|
279
|
+
context.addIssue({
|
|
280
|
+
code: "custom",
|
|
281
|
+
path: [index],
|
|
282
|
+
message: "Duplicate graph hint",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
seen.add(value);
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
.optional(),
|
|
289
|
+
searchBoost: z
|
|
290
|
+
.number()
|
|
291
|
+
.finite()
|
|
292
|
+
.min(CONTENT_TYPE_SEARCH_BOOST_MIN)
|
|
293
|
+
.max(CONTENT_TYPE_SEARCH_BOOST_MAX)
|
|
294
|
+
.optional(),
|
|
295
|
+
temporal: z.boolean().optional(),
|
|
296
|
+
})
|
|
297
|
+
.strict();
|
|
298
|
+
|
|
299
|
+
export const ProjectProfileAffinityDefaultsSchema = z
|
|
300
|
+
.object({
|
|
301
|
+
enabled: z.boolean().default(true),
|
|
302
|
+
contribution: z
|
|
303
|
+
.number()
|
|
304
|
+
.finite()
|
|
305
|
+
.min(0)
|
|
306
|
+
.max(PROJECT_AFFINITY_MAX_CONTRIBUTION)
|
|
307
|
+
.default(PROJECT_AFFINITY_MAX_CONTRIBUTION),
|
|
308
|
+
})
|
|
309
|
+
.strict();
|
|
310
|
+
|
|
311
|
+
export const ProjectProfileSchema = z
|
|
312
|
+
.object({
|
|
313
|
+
schemaVersion: z.literal(PROJECT_PROFILE_SCHEMA_VERSION),
|
|
314
|
+
collection: ProjectProfileCollectionSchema,
|
|
315
|
+
contexts: z
|
|
316
|
+
.array(ProjectProfileContextSchema)
|
|
317
|
+
.max(64)
|
|
318
|
+
.superRefine((values, context) => {
|
|
319
|
+
const seen = new Set<string>();
|
|
320
|
+
for (const [index, value] of values.entries()) {
|
|
321
|
+
const key = JSON.stringify(value);
|
|
322
|
+
if (seen.has(key)) {
|
|
323
|
+
context.addIssue({
|
|
324
|
+
code: "custom",
|
|
325
|
+
path: [index],
|
|
326
|
+
message: "Duplicate context declaration",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
seen.add(key);
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
.default([]),
|
|
333
|
+
contentTypes: z
|
|
334
|
+
.record(
|
|
335
|
+
z.string().regex(REFERENCE_PATTERN),
|
|
336
|
+
ProjectProfileContentTypeSchema
|
|
337
|
+
)
|
|
338
|
+
.superRefine((rules, context) => {
|
|
339
|
+
if (Object.keys(rules).length > 64) {
|
|
340
|
+
context.addIssue({
|
|
341
|
+
code: "custom",
|
|
342
|
+
message: "Too many content type rules; maximum is 64",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
.default({}),
|
|
347
|
+
affinityDefaults: ProjectProfileAffinityDefaultsSchema.default({
|
|
348
|
+
enabled: true,
|
|
349
|
+
contribution: PROJECT_AFFINITY_MAX_CONTRIBUTION,
|
|
350
|
+
}),
|
|
351
|
+
recommendedCapabilities: z
|
|
352
|
+
.array(z.string().regex(CAPABILITY_PATTERN))
|
|
353
|
+
.max(32)
|
|
354
|
+
.superRefine((values, context) => {
|
|
355
|
+
const seen = new Set<string>();
|
|
356
|
+
for (const [index, value] of values.entries()) {
|
|
357
|
+
if (seen.has(value)) {
|
|
358
|
+
context.addIssue({
|
|
359
|
+
code: "custom",
|
|
360
|
+
path: [index],
|
|
361
|
+
message: "Duplicate recommended capability",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
seen.add(value);
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
.default([]),
|
|
368
|
+
})
|
|
369
|
+
.strict();
|
|
370
|
+
|
|
371
|
+
export type ProjectProfile = z.infer<typeof ProjectProfileSchema>;
|
|
372
|
+
export type ProjectProfileContentType = z.infer<
|
|
373
|
+
typeof ProjectProfileContentTypeSchema
|
|
374
|
+
>;
|
package/src/config/saver.ts
CHANGED
|
@@ -62,9 +62,18 @@ export async function saveConfigToPath(
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
return saveTextToPath(Bun.YAML.stringify(config), filePath);
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Atomically save deterministic text outside the config schema.
|
|
70
|
+
* Used for runtime receipts that need the same crash-safe write discipline as
|
|
71
|
+
* the user config.
|
|
72
|
+
*/
|
|
73
|
+
export async function saveTextToPath(
|
|
74
|
+
content: string,
|
|
75
|
+
filePath: string
|
|
76
|
+
): Promise<SaveResult> {
|
|
68
77
|
// Ensure parent directory exists
|
|
69
78
|
const dir = dirname(filePath);
|
|
70
79
|
try {
|
|
@@ -88,7 +97,7 @@ export async function saveConfigToPath(
|
|
|
88
97
|
);
|
|
89
98
|
|
|
90
99
|
try {
|
|
91
|
-
await Bun.write(tempPath,
|
|
100
|
+
await Bun.write(tempPath, content);
|
|
92
101
|
} catch (cause) {
|
|
93
102
|
return {
|
|
94
103
|
ok: false,
|
|
@@ -102,10 +111,10 @@ export async function saveConfigToPath(
|
|
|
102
111
|
|
|
103
112
|
// Rename temp to target (atomic on POSIX, needs unlink on Windows)
|
|
104
113
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
114
|
+
if (process.platform === "win32") {
|
|
115
|
+
// Windows rename fails if dest exists, so unlink first (ignore ENOENT).
|
|
116
|
+
await unlink(filePath).catch(() => undefined);
|
|
117
|
+
}
|
|
109
118
|
await rename(tempPath, filePath);
|
|
110
119
|
} catch (cause) {
|
|
111
120
|
// Clean up temp file on rename failure
|
package/src/config/types.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* @module src/config/types
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
// node:path provides platform-correct absolute-path validation; Bun has no path utilities.
|
|
9
|
+
import { isAbsolute } from "node:path";
|
|
8
10
|
import { z } from "zod";
|
|
9
11
|
|
|
10
12
|
import { URI_PREFIX } from "../app/constants";
|
|
@@ -123,6 +125,7 @@ export const TrustedProjectRootSourceSchema = z.enum([
|
|
|
123
125
|
"cli_cwd",
|
|
124
126
|
"cli_explicit",
|
|
125
127
|
"cli_worktree",
|
|
128
|
+
"project_profile",
|
|
126
129
|
]);
|
|
127
130
|
export type TrustedProjectRootSource = z.infer<
|
|
128
131
|
typeof TrustedProjectRootSourceSchema
|
|
@@ -183,6 +186,26 @@ export const ProjectAffinityConfigSchema = z.object({
|
|
|
183
186
|
});
|
|
184
187
|
export type ProjectAffinityConfig = z.infer<typeof ProjectAffinityConfigSchema>;
|
|
185
188
|
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// Local Project Profile Bindings
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Machine-local provenance for a project profile projected into this config.
|
|
197
|
+
* Apply canonicalizes `path`; the schema rejects relative paths and malformed
|
|
198
|
+
* fingerprints without making the tracked profile machine-specific.
|
|
199
|
+
*/
|
|
200
|
+
export const ProjectProfileBindingSchema = z.object({
|
|
201
|
+
path: z.string().min(1).refine(isAbsolute, {
|
|
202
|
+
message: "Project profile binding path must be absolute",
|
|
203
|
+
}),
|
|
204
|
+
fingerprint: z.string().regex(SHA256_HEX_REGEX),
|
|
205
|
+
collection: z.string().regex(COLLECTION_NAME_REGEX),
|
|
206
|
+
});
|
|
207
|
+
export type ProjectProfileBinding = z.infer<typeof ProjectProfileBindingSchema>;
|
|
208
|
+
|
|
186
209
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
187
210
|
// Context Schema
|
|
188
211
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -362,6 +385,10 @@ export const CONTENT_TYPE_GRAPH_HINTS = [
|
|
|
362
385
|
|
|
363
386
|
export type ContentTypeGraphHint = (typeof CONTENT_TYPE_GRAPH_HINTS)[number];
|
|
364
387
|
|
|
388
|
+
export const CONTENT_TYPE_SEARCH_BOOST_MIN = 0.5;
|
|
389
|
+
export const CONTENT_TYPE_SEARCH_BOOST_NEUTRAL = 1;
|
|
390
|
+
export const CONTENT_TYPE_SEARCH_BOOST_MAX = 2;
|
|
391
|
+
|
|
365
392
|
export const ContentTypeSchema = z.object({
|
|
366
393
|
/** Stable content type identifier */
|
|
367
394
|
id: z.string().min(1),
|
|
@@ -371,8 +398,13 @@ export const ContentTypeSchema = z.object({
|
|
|
371
398
|
preset: z.string().min(1),
|
|
372
399
|
/** Reserved for fn-84 typed graph hints; accepted but no-op in fn-83 */
|
|
373
400
|
graphHints: z.array(z.string().min(1)).optional(),
|
|
374
|
-
/**
|
|
375
|
-
searchBoost: z
|
|
401
|
+
/** Bounded soft ranking factor; normalized to 1.0 when omitted */
|
|
402
|
+
searchBoost: z
|
|
403
|
+
.number()
|
|
404
|
+
.finite()
|
|
405
|
+
.min(CONTENT_TYPE_SEARCH_BOOST_MIN)
|
|
406
|
+
.max(CONTENT_TYPE_SEARCH_BOOST_MAX)
|
|
407
|
+
.optional(),
|
|
376
408
|
/** Marks time-oriented content types; accepted but no-op in fn-83 */
|
|
377
409
|
temporal: z.boolean().optional(),
|
|
378
410
|
});
|
|
@@ -413,6 +445,25 @@ export const ConfigSchema = z.object({
|
|
|
413
445
|
|
|
414
446
|
/** Bounded project-aware retrieval affinity. */
|
|
415
447
|
projectAffinity: ProjectAffinityConfigSchema.optional(),
|
|
448
|
+
|
|
449
|
+
/** Machine-local, timestamp-free project profile provenance. */
|
|
450
|
+
projectProfileBindings: z
|
|
451
|
+
.array(ProjectProfileBindingSchema)
|
|
452
|
+
.max(256)
|
|
453
|
+
.superRefine((bindings, ctx) => {
|
|
454
|
+
const paths = new Set<string>();
|
|
455
|
+
for (const [index, binding] of bindings.entries()) {
|
|
456
|
+
if (paths.has(binding.path)) {
|
|
457
|
+
ctx.addIssue({
|
|
458
|
+
code: "custom",
|
|
459
|
+
message: "Project profile binding paths must be unique",
|
|
460
|
+
path: [index, "path"],
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
paths.add(binding.path);
|
|
464
|
+
}
|
|
465
|
+
})
|
|
466
|
+
.optional(),
|
|
416
467
|
});
|
|
417
468
|
|
|
418
469
|
export type Config = Omit<z.infer<typeof ConfigSchema>, "contentTypes"> & {
|