@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.
Files changed (94) hide show
  1. package/README.md +12 -5
  2. package/assets/skill/SKILL.md +37 -17
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.27.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.27.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +1 -1
  7. package/spec/cli.md +161 -6
  8. package/spec/db/schema.sql +1 -1
  9. package/spec/evals-agentic.md +17 -0
  10. package/spec/mcp.md +25 -6
  11. package/spec/output-schemas/ask.schema.json +3 -0
  12. package/spec/output-schemas/project-profile-apply.schema.json +209 -0
  13. package/spec/output-schemas/project-profile-command.schema.json +160 -0
  14. package/spec/output-schemas/query-diagnose.schema.json +68 -5
  15. package/spec/output-schemas/search-results.schema.json +87 -1
  16. package/spec/output-schemas/setup-profile-result.schema.json +87 -0
  17. package/spec/output-schemas/status.schema.json +24 -0
  18. package/spec/project-profile.schema.json +303 -0
  19. package/src/app/context-runtime-contract.ts +4 -1
  20. package/src/app/context-runtime-types.ts +2 -0
  21. package/src/app/context-runtime.ts +26 -0
  22. package/src/app/verified-ask.ts +6 -1
  23. package/src/cli/commands/ask.ts +8 -1
  24. package/src/cli/commands/collection/add.ts +39 -45
  25. package/src/cli/commands/collection/remove.ts +28 -28
  26. package/src/cli/commands/collection/rename.ts +55 -73
  27. package/src/cli/commands/context/add.ts +37 -26
  28. package/src/cli/commands/context/rm.ts +47 -20
  29. package/src/cli/commands/init.ts +55 -125
  30. package/src/cli/commands/models/use.ts +43 -38
  31. package/src/cli/commands/profile-apply.ts +334 -0
  32. package/src/cli/commands/profile.ts +409 -0
  33. package/src/cli/commands/query.ts +6 -3
  34. package/src/cli/commands/search.ts +6 -1
  35. package/src/cli/commands/setup-activation.ts +205 -54
  36. package/src/cli/commands/setup-profile.ts +223 -0
  37. package/src/cli/commands/setup.ts +3 -0
  38. package/src/cli/commands/status.ts +43 -7
  39. package/src/cli/program.ts +112 -9
  40. package/src/config/content-types.ts +82 -0
  41. package/src/config/index.ts +11 -0
  42. package/src/config/project-profile.ts +374 -0
  43. package/src/config/saver.ts +16 -7
  44. package/src/config/types.ts +53 -2
  45. package/src/core/config-mutation.ts +138 -76
  46. package/src/core/config-write-lock.ts +89 -0
  47. package/src/core/context-compiler.ts +38 -1
  48. package/src/core/context-identity.ts +16 -0
  49. package/src/core/context-resolver.ts +2 -12
  50. package/src/core/folder-setup-planning.ts +6 -21
  51. package/src/core/folder-setup.ts +30 -2
  52. package/src/core/path-rules.ts +53 -0
  53. package/src/core/project-affinity-surface.ts +102 -7
  54. package/src/core/project-profile-apply-state.ts +268 -0
  55. package/src/core/project-profile-apply-validation.ts +95 -0
  56. package/src/core/project-profile-apply.ts +408 -0
  57. package/src/core/project-profile-canonical.ts +71 -0
  58. package/src/core/project-profile-diff.ts +302 -0
  59. package/src/core/project-profile-discovery.ts +519 -0
  60. package/src/core/project-profile-file.ts +37 -0
  61. package/src/core/project-profile-parser.ts +98 -0
  62. package/src/core/project-profile.ts +490 -0
  63. package/src/core/retrieval-replay-candidate.ts +6 -1
  64. package/src/ingestion/sync-options.ts +6 -2
  65. package/src/ingestion/sync.ts +21 -29
  66. package/src/ingestion/types.ts +1 -1
  67. package/src/ingestion/walker.ts +85 -44
  68. package/src/llm/cache.ts +21 -0
  69. package/src/mcp/tools/ask.ts +1 -0
  70. package/src/mcp/tools/index.ts +4 -0
  71. package/src/mcp/tools/query.ts +4 -2
  72. package/src/mcp/tools/search.ts +3 -0
  73. package/src/mcp/tools/status.ts +4 -0
  74. package/src/pipeline/content-type-boost.ts +264 -0
  75. package/src/pipeline/diagnose.ts +46 -19
  76. package/src/pipeline/explain.ts +15 -2
  77. package/src/pipeline/hybrid.ts +170 -74
  78. package/src/pipeline/rerank.ts +45 -15
  79. package/src/pipeline/search.ts +29 -11
  80. package/src/pipeline/types.ts +13 -4
  81. package/src/pipeline/vsearch.ts +30 -10
  82. package/src/sdk/client.ts +19 -3
  83. package/src/sdk/index.ts +1 -0
  84. package/src/sdk/types.ts +21 -5
  85. package/src/serve/config-sync.ts +2 -2
  86. package/src/serve/resident-runtime.ts +1 -0
  87. package/src/serve/routes/api.ts +18 -3
  88. package/src/serve/status-model.ts +2 -0
  89. package/src/serve/status.ts +4 -0
  90. package/src/store/migrations/021-multi-context-identity.ts +37 -0
  91. package/src/store/migrations/index.ts +2 -0
  92. package/src/store/sqlite/adapter.ts +86 -0
  93. package/src/store/types.ts +3 -2
  94. 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
+ >;
@@ -62,9 +62,18 @@ export async function saveConfigToPath(
62
62
  };
63
63
  }
64
64
 
65
- // Convert to YAML
66
- const yamlContent = Bun.YAML.stringify(config);
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, yamlContent);
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
- // Windows: rename fails if dest exists, so unlink first (ignore if not exists)
106
- await unlink(filePath).catch(() => {
107
- /* ENOENT ok */
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
@@ -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
- /** Reserved for future ranking; accepted but no-op in fn-83 */
375
- searchBoost: z.number().finite().optional(),
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"> & {