@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
@@ -5,11 +5,17 @@
5
5
  * @module src/cli/commands/status
6
6
  */
7
7
 
8
+ import type { ContentTypeBoostStatus } from "../../config/content-types";
8
9
  import type { ActivationStatus } from "../../core/activation-status";
9
10
  import type { IndexStatus } from "../../store/types";
10
11
 
11
12
  import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
12
- import { getConfigPaths, isInitialized, loadConfig } from "../../config";
13
+ import {
14
+ buildContentTypeBoostStatus,
15
+ getConfigPaths,
16
+ isInitialized,
17
+ loadConfig,
18
+ } from "../../config";
13
19
  import { isConnectorActivationComplete } from "../../core/activation-connector-health";
14
20
  import { buildActivationStatus } from "../../core/activation-status";
15
21
  import { ModelCache } from "../../llm/cache";
@@ -36,7 +42,12 @@ export interface StatusOptions {
36
42
  * Result of status command.
37
43
  */
38
44
  export type StatusResult =
39
- | { success: true; status: IndexStatus; activation: ActivationStatus }
45
+ | {
46
+ success: true;
47
+ status: IndexStatus;
48
+ activation: ActivationStatus;
49
+ contentTypeBoost: ContentTypeBoostStatus;
50
+ }
40
51
  | { success: false; error: string };
41
52
 
42
53
  function connectorProjectionLine(activation: ActivationStatus): string | null {
@@ -63,7 +74,8 @@ function isStatusHealthy(
63
74
  */
64
75
  function formatTerminal(
65
76
  indexStatus: IndexStatus,
66
- activation: ActivationStatus
77
+ activation: ActivationStatus,
78
+ contentTypeBoost: ContentTypeBoostStatus
67
79
  ): string {
68
80
  const lines: string[] = [];
69
81
 
@@ -101,6 +113,11 @@ function formatTerminal(
101
113
  lines.push(`Last updated: ${indexStatus.lastUpdatedAt}`);
102
114
  }
103
115
 
116
+ lines.push(
117
+ `Content-type boosts: ${contentTypeBoost.rules.length ? contentTypeBoost.rules.map((rule) => `${rule.id}=${rule.searchBoost}`).join(", ") : "none"}`
118
+ );
119
+ lines.push(`Ranking fingerprint: ${contentTypeBoost.rulesFingerprint}`);
120
+
104
121
  lines.push(
105
122
  `Health: ${isStatusHealthy(indexStatus, activation) ? "OK" : "DEGRADED"}`
106
123
  );
@@ -138,7 +155,8 @@ function formatTerminal(
138
155
  */
139
156
  function formatMarkdown(
140
157
  indexStatus: IndexStatus,
141
- activation: ActivationStatus
158
+ activation: ActivationStatus,
159
+ contentTypeBoost: ContentTypeBoostStatus
142
160
  ): string {
143
161
  const lines: string[] = [];
144
162
 
@@ -174,6 +192,10 @@ function formatMarkdown(
174
192
  if (indexStatus.lastUpdatedAt) {
175
193
  lines.push(`- **Last updated**: ${indexStatus.lastUpdatedAt}`);
176
194
  }
195
+ lines.push(
196
+ `- **Content-type boosts**: ${contentTypeBoost.rules.length ? contentTypeBoost.rules.map((rule) => `${rule.id}=${rule.searchBoost}`).join(", ") : "none"}`
197
+ );
198
+ lines.push(`- **Ranking fingerprint**: ${contentTypeBoost.rulesFingerprint}`);
177
199
 
178
200
  lines.push("");
179
201
  lines.push("## Lexical activation");
@@ -254,7 +276,12 @@ export async function status(
254
276
  }
255
277
  );
256
278
 
257
- return { success: true, status: statusResult.value, activation };
279
+ return {
280
+ success: true,
281
+ status: statusResult.value,
282
+ activation,
283
+ contentTypeBoost: buildContentTypeBoostStatus(config.contentTypes ?? []),
284
+ };
258
285
  } finally {
259
286
  await store.close();
260
287
  }
@@ -294,6 +321,7 @@ export function formatStatus(
294
321
  embeddingBacklog: s.embeddingBacklog,
295
322
  lastUpdated: s.lastUpdatedAt,
296
323
  healthy: isStatusHealthy(s, result.activation),
324
+ contentTypeBoost: result.contentTypeBoost,
297
325
  activation: result.activation,
298
326
  },
299
327
  null,
@@ -302,8 +330,16 @@ export function formatStatus(
302
330
  }
303
331
 
304
332
  if (options.md) {
305
- return formatMarkdown(result.status, result.activation);
333
+ return formatMarkdown(
334
+ result.status,
335
+ result.activation,
336
+ result.contentTypeBoost
337
+ );
306
338
  }
307
339
 
308
- return formatTerminal(result.status, result.activation);
340
+ return formatTerminal(
341
+ result.status,
342
+ result.activation,
343
+ result.contentTypeBoost
344
+ );
309
345
  }
@@ -1151,6 +1151,7 @@ function wireSearchCommands(program: Command): void {
1151
1151
  .option("--context-budget-bytes <num>", "verified Context byte budget")
1152
1152
  .option("--min-score <score>", "minimum retrieval score (0-1)")
1153
1153
  .option("--graph", "include bounded graph expansion")
1154
+ .option("--explain", "include retrieval scoring explanation")
1154
1155
  .option(
1155
1156
  "--project-root <path>",
1156
1157
  "trusted project root (repeatable; replaces cwd affinity)",
@@ -1259,6 +1260,7 @@ function wireSearchCommands(program: Command): void {
1259
1260
  noExpand: depthPolicy.noExpand,
1260
1261
  noRerank: depthPolicy.noRerank,
1261
1262
  candidateLimit: depthPolicy.candidateLimit,
1263
+ explain: Boolean(cmdOpts.explain),
1262
1264
  // Per spec: --answer defaults to false, --no-answer forces retrieval-only
1263
1265
  // Commander creates separate cmdOpts.noAnswer for --no-answer flag
1264
1266
  answer: Boolean(cmdOpts.answer),
@@ -1291,6 +1293,77 @@ function wireSearchCommands(program: Command): void {
1291
1293
  // ─────────────────────────────────────────────────────────────────────────────
1292
1294
 
1293
1295
  function wireOnboardingCommands(program: Command): void {
1296
+ const profileCmd = program
1297
+ .command("profile")
1298
+ .description("Inspect a project-local retrieval profile");
1299
+
1300
+ const wireProfileRead = (
1301
+ command: "check" | "show" | "diff",
1302
+ description: string
1303
+ ): void => {
1304
+ profileCmd
1305
+ .command(`${command} [path]`)
1306
+ .description(description)
1307
+ .option("--json", "JSON output")
1308
+ .action(
1309
+ async (path: string | undefined, cmdOpts: Record<string, unknown>) => {
1310
+ const globals = getGlobals();
1311
+ const json = Boolean(cmdOpts.json) || globals.json;
1312
+ const { formatProjectProfileResult, runProjectProfileCommand } =
1313
+ await import("./commands/profile");
1314
+ const outcome = await runProjectProfileCommand({
1315
+ command,
1316
+ path,
1317
+ configPath: globals.config,
1318
+ offline: globals.offline,
1319
+ });
1320
+ process.stdout.write(
1321
+ `${formatProjectProfileResult(outcome.result, { json })}\n`
1322
+ );
1323
+ if (outcome.exitCode !== 0) {
1324
+ throw new CliError(
1325
+ outcome.exitCode === 1 ? "VALIDATION" : "RUNTIME",
1326
+ "Project profile inspection failed",
1327
+ { silent: true }
1328
+ );
1329
+ }
1330
+ }
1331
+ );
1332
+ };
1333
+
1334
+ wireProfileRead("check", "Validate a project-local retrieval profile");
1335
+ wireProfileRead("show", "Show normalized project-local retrieval state");
1336
+ wireProfileRead("diff", "Compare a project profile with local config");
1337
+ profileCmd
1338
+ .command("apply [path]")
1339
+ .description("Apply a project profile without implicit deletion")
1340
+ .option("--json", "JSON output")
1341
+ .action(
1342
+ async (path: string | undefined, cmdOpts: Record<string, unknown>) => {
1343
+ const globals = getGlobals();
1344
+ const json = Boolean(cmdOpts.json) || globals.json;
1345
+ const {
1346
+ formatProjectProfileApplyResult,
1347
+ runProjectProfileApplyCommand,
1348
+ } = await import("./commands/profile-apply");
1349
+ const outcome = await runProjectProfileApplyCommand({
1350
+ path,
1351
+ configPath: globals.config,
1352
+ indexName: globals.index,
1353
+ });
1354
+ process.stdout.write(
1355
+ `${formatProjectProfileApplyResult(outcome.result, { json })}\n`
1356
+ );
1357
+ if (outcome.exitCode !== 0) {
1358
+ throw new CliError(
1359
+ outcome.exitCode === 1 ? "VALIDATION" : "RUNTIME",
1360
+ "Project profile apply failed",
1361
+ { silent: true }
1362
+ );
1363
+ }
1364
+ }
1365
+ );
1366
+
1294
1367
  // init - Initialize GNO
1295
1368
  program
1296
1369
  .command("init [path]")
@@ -1320,6 +1393,7 @@ function wireOnboardingCommands(program: Command): void {
1320
1393
  | undefined,
1321
1394
  language: cmdOpts.language as string | undefined,
1322
1395
  yes: globals.yes,
1396
+ configPath: globals.config,
1323
1397
  });
1324
1398
 
1325
1399
  if (!result.success) {
@@ -1367,13 +1441,20 @@ function wireOnboardingCommands(program: Command): void {
1367
1441
  collectRepeatableValue,
1368
1442
  []
1369
1443
  )
1444
+ .option(
1445
+ "--apply-profile",
1446
+ "apply a valid project profile before lexical setup"
1447
+ )
1370
1448
  .option("--no-semantic", "skip background semantic indexing")
1371
1449
  .option("--json", "JSON output")
1372
1450
  .action(async (folder: string, cmdOpts: Record<string, unknown>) => {
1373
1451
  const globals = getGlobals();
1374
1452
  const json = Boolean(cmdOpts.json) || globals.json;
1375
- const { formatSetupOutputResult, setupWithActivation } =
1376
- await import("./commands/setup-activation");
1453
+ const {
1454
+ formatSetupOutputResult,
1455
+ formatSetupProfileAdvisory,
1456
+ setupWithActivation,
1457
+ } = await import("./commands/setup-activation");
1377
1458
  const exclusions = cmdOpts.exclude as string[];
1378
1459
  const outcome = await setupWithActivation({
1379
1460
  folder,
@@ -1381,6 +1462,7 @@ function wireOnboardingCommands(program: Command): void {
1381
1462
  exclude: exclusions.length > 0 ? exclusions : undefined,
1382
1463
  authorizeSecretRisk: Boolean(cmdOpts.authorizeSecretRisk),
1383
1464
  connectorIds: cmdOpts.connector as string[],
1465
+ applyProfile: Boolean(cmdOpts.applyProfile),
1384
1466
  semantic: cmdOpts.semantic !== false,
1385
1467
  indexName: globals.index,
1386
1468
  configPath: globals.config,
@@ -1391,6 +1473,17 @@ function wireOnboardingCommands(program: Command): void {
1391
1473
  progress: (stage) => {
1392
1474
  process.stderr.write(`setup: ${stage}\n`);
1393
1475
  },
1476
+ onProfileAdvisory: (profile) => {
1477
+ if (json || globals.quiet) return;
1478
+ const advisory = formatSetupProfileAdvisory(profile, {
1479
+ applyRequested: Boolean(cmdOpts.applyProfile),
1480
+ });
1481
+ if (advisory) process.stderr.write(`${advisory}\n`);
1482
+ },
1483
+ onProfileApply: (profile) => {
1484
+ if (json || globals.quiet) return;
1485
+ process.stderr.write(`setup: project profile ${profile.status}\n`);
1486
+ },
1394
1487
  });
1395
1488
  const output = formatSetupOutputResult(outcome.result, { json });
1396
1489
  if (json || outcome.exitCode === 0) {
@@ -1958,6 +2051,7 @@ function wireManagementCommands(program: Command): void {
1958
2051
  include: cmdOpts.include as string | undefined,
1959
2052
  exclude: cmdOpts.exclude as string | undefined,
1960
2053
  update: cmdOpts.update as string | undefined,
2054
+ configPath: getGlobals().config,
1961
2055
  });
1962
2056
  });
1963
2057
 
@@ -1982,7 +2076,7 @@ function wireManagementCommands(program: Command): void {
1982
2076
  .description("Remove a collection")
1983
2077
  .action(async (name: string) => {
1984
2078
  const { collectionRemove } = await import("./commands/collection");
1985
- await collectionRemove(name);
2079
+ await collectionRemove(name, { configPath: getGlobals().config });
1986
2080
  });
1987
2081
 
1988
2082
  collectionCmd
@@ -1990,7 +2084,9 @@ function wireManagementCommands(program: Command): void {
1990
2084
  .description("Rename a collection")
1991
2085
  .action(async (oldName: string, newName: string) => {
1992
2086
  const { collectionRename } = await import("./commands/collection");
1993
- await collectionRename(oldName, newName);
2087
+ await collectionRename(oldName, newName, {
2088
+ configPath: getGlobals().config,
2089
+ });
1994
2090
  });
1995
2091
 
1996
2092
  collectionCmd
@@ -2017,9 +2113,11 @@ function wireManagementCommands(program: Command): void {
2017
2113
  .description("Add context metadata for a scope")
2018
2114
  .action(async (scope: string, text: string) => {
2019
2115
  const { contextAdd } = await import("./commands/context");
2020
- const exitCode = await contextAdd(scope, text);
2116
+ const exitCode = await contextAdd(scope, text, {
2117
+ configPath: getGlobals().config,
2118
+ });
2021
2119
  if (exitCode !== 0) {
2022
- throw new CliError("RUNTIME", "Failed to add context");
2120
+ throw new CliError("VALIDATION", "Failed to add context");
2023
2121
  }
2024
2122
  });
2025
2123
 
@@ -2301,11 +2399,16 @@ function wireManagementCommands(program: Command): void {
2301
2399
  });
2302
2400
 
2303
2401
  contextCmd
2304
- .command("rm <uri>")
2402
+ .command("rm <scope> [text]")
2305
2403
  .description("Remove context item")
2306
- .action(async (uri: string) => {
2404
+ .action(async (scope: string, text?: string) => {
2307
2405
  const { contextRm } = await import("./commands/context");
2308
- await contextRm(uri);
2406
+ const exitCode = await contextRm(scope, text, {
2407
+ configPath: getGlobals().config,
2408
+ });
2409
+ if (exitCode !== 0) {
2410
+ throw new CliError("VALIDATION", "Failed to remove context");
2411
+ }
2309
2412
  });
2310
2413
 
2311
2414
  // models subcommands
@@ -8,6 +8,7 @@ import type { NotePresetId } from "../core/note-presets";
8
8
  import type { Config, ContentTypeConfig } from "./types";
9
9
 
10
10
  import { NOTE_PRESETS } from "../core/note-presets";
11
+ import { CONTENT_TYPE_SEARCH_BOOST_NEUTRAL } from "./types";
11
12
 
12
13
  export type ConfigWarningCode =
13
14
  | "UNKNOWN_CONTENT_TYPE_PRESET"
@@ -21,6 +22,7 @@ export interface ConfigWarning {
21
22
 
22
23
  export interface NormalizedContentTypeRule extends ContentTypeConfig {
23
24
  preset: NotePresetId;
25
+ searchBoost: number;
24
26
  }
25
27
 
26
28
  export interface ContentTypeNormalizationResult {
@@ -33,6 +35,14 @@ export interface ConfigNormalizationResult {
33
35
  warnings: ConfigWarning[];
34
36
  }
35
37
 
38
+ export interface ContentTypeBoostStatus {
39
+ rulesFingerprint: string;
40
+ rules: Array<{
41
+ id: string;
42
+ searchBoost: number;
43
+ }>;
44
+ }
45
+
36
46
  const NOTE_PRESET_IDS = new Set<NotePresetId>(
37
47
  NOTE_PRESETS.map((preset) => preset.id)
38
48
  );
@@ -48,6 +58,10 @@ function isNotePresetId(value: string): value is NotePresetId {
48
58
  return NOTE_PRESET_IDS.has(value as NotePresetId);
49
59
  }
50
60
 
61
+ function normalizeSearchBoost(value: number | undefined): number {
62
+ return value ?? CONTENT_TYPE_SEARCH_BOOST_NEUTRAL;
63
+ }
64
+
51
65
  export function normalizeContentTypes(
52
66
  contentTypes: ContentTypeConfig[]
53
67
  ): ContentTypeNormalizationResult {
@@ -89,6 +103,7 @@ export function normalizeContentTypes(
89
103
  graphHints: contentType.graphHints
90
104
  ? contentType.graphHints.map(normalizeGraphHint).filter(Boolean)
91
105
  : undefined,
106
+ searchBoost: normalizeSearchBoost(contentType.searchBoost),
92
107
  });
93
108
  continue;
94
109
  }
@@ -100,6 +115,7 @@ export function normalizeContentTypes(
100
115
  graphHints: contentType.graphHints
101
116
  ? contentType.graphHints.map(normalizeGraphHint).filter(Boolean)
102
117
  : undefined,
118
+ searchBoost: normalizeSearchBoost(contentType.searchBoost),
103
119
  });
104
120
  }
105
121
 
@@ -112,6 +128,34 @@ export function normalizeContentTypes(
112
128
  return { rules, warnings };
113
129
  }
114
130
 
131
+ export interface ContentTypeRuleResolution {
132
+ rule: NormalizedContentTypeRule;
133
+ source: "configured-id" | "prefix";
134
+ }
135
+
136
+ /**
137
+ * Resolve one canonical configured rule. A configured frontmatter type wins;
138
+ * otherwise normalized rule order provides longest-prefix matching. Arbitrary
139
+ * category text is intentionally not an input and boosts never stack.
140
+ */
141
+ export function resolveContentTypeRule(
142
+ configuredId: string | undefined,
143
+ relativePath: string,
144
+ rules: NormalizedContentTypeRule[]
145
+ ): ContentTypeRuleResolution | undefined {
146
+ if (configuredId) {
147
+ const configuredRule = rules.find((rule) => rule.id === configuredId);
148
+ if (configuredRule) {
149
+ return { rule: configuredRule, source: "configured-id" };
150
+ }
151
+ }
152
+
153
+ const prefixRule = rules.find((rule) =>
154
+ rule.prefixes.some((prefix) => relativePath.startsWith(prefix))
155
+ );
156
+ return prefixRule ? { rule: prefixRule, source: "prefix" } : undefined;
157
+ }
158
+
115
159
  export function normalizeConfigContentTypes(
116
160
  config: Config
117
161
  ): ConfigNormalizationResult {
@@ -127,6 +171,44 @@ export function normalizeConfigContentTypes(
127
171
 
128
172
  export function fingerprintContentTypeRules(
129
173
  rules: NormalizedContentTypeRule[]
174
+ ): string {
175
+ const canonical = rules.map((rule) => {
176
+ const base = {
177
+ id: rule.id,
178
+ preset: rule.preset,
179
+ prefixes: rule.prefixes,
180
+ graphHints: rule.graphHints ?? [],
181
+ };
182
+ return rule.searchBoost === CONTENT_TYPE_SEARCH_BOOST_NEUTRAL
183
+ ? base
184
+ : { ...base, searchBoost: rule.searchBoost };
185
+ });
186
+ const hasher = new Bun.CryptoHasher("sha256");
187
+ hasher.update(JSON.stringify(canonical));
188
+ return hasher.digest("hex");
189
+ }
190
+
191
+ /**
192
+ * Redacted live-ranking projection for status surfaces. Prefixes remain local
193
+ * configuration details; rule IDs and factors are enough to audit behavior.
194
+ */
195
+ export function buildContentTypeBoostStatus(
196
+ contentTypes: ContentTypeConfig[]
197
+ ): ContentTypeBoostStatus {
198
+ const { rules } = normalizeContentTypes(contentTypes);
199
+ return {
200
+ rulesFingerprint: fingerprintContentTypeRules(rules),
201
+ rules: rules.map(({ id, searchBoost }) => ({ id, searchBoost })),
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Fingerprint only fields that derive persisted document metadata.
207
+ * Search boosts are evaluated from live config at query time, so changing one
208
+ * must not force content conversion or vector rebuilds.
209
+ */
210
+ export function fingerprintContentTypeMetadataRules(
211
+ rules: NormalizedContentTypeRule[]
130
212
  ): string {
131
213
  const canonical = rules.map((rule) => ({
132
214
  id: rule.id,
@@ -6,13 +6,18 @@
6
6
 
7
7
  export { createDefaultConfig } from "./defaults";
8
8
  export {
9
+ type ContentTypeRuleResolution,
10
+ type ContentTypeBoostStatus,
9
11
  type ConfigWarning,
12
+ buildContentTypeBoostStatus,
13
+ fingerprintContentTypeMetadataRules,
10
14
  fingerprintContentTypeRules,
11
15
  formatConfigWarning,
12
16
  formatConfigWarnings,
13
17
  normalizeConfigContentTypes,
14
18
  normalizeContentTypes,
15
19
  type NormalizedContentTypeRule,
20
+ resolveContentTypeRule,
16
21
  writeConfigWarningsToStderr,
17
22
  } from "./content-types";
18
23
  // Loading
@@ -42,10 +47,14 @@ export {
42
47
  type SaveResult,
43
48
  saveConfig,
44
49
  saveConfigToPath,
50
+ saveTextToPath,
45
51
  } from "./saver";
46
52
  // Types and schemas
47
53
  export {
48
54
  CONFIG_VERSION,
55
+ CONTENT_TYPE_SEARCH_BOOST_MAX,
56
+ CONTENT_TYPE_SEARCH_BOOST_MIN,
57
+ CONTENT_TYPE_SEARCH_BOOST_NEUTRAL,
49
58
  type Collection,
50
59
  CollectionSchema,
51
60
  type Config,
@@ -66,6 +75,8 @@ export {
66
75
  getCollectionFromScope,
67
76
  isValidLanguageHint,
68
77
  parseScope,
78
+ type ProjectProfileBinding,
79
+ ProjectProfileBindingSchema,
69
80
  type ScopeType,
70
81
  ScopeTypeSchema,
71
82
  } from "./types";