@gmickel/gno 2.2.1 → 2.3.1

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.
@@ -23,6 +23,7 @@ import {
23
23
  } from "../core/change-diff";
24
24
  import { createEgressLineage } from "../core/egress-provenance";
25
25
  import { normalizeTag, validateTag } from "../core/tags";
26
+ import { persistChunkLayout } from "./chunking";
26
27
  import { runRecordAdapter } from "./record-adapter";
27
28
  import { recordVirtualPath } from "./record-path";
28
29
  import { reconcileRecordSnapshot, type RecordSyncPlan } from "./record-sync";
@@ -397,7 +398,7 @@ const persistRecord = async (
397
398
  const chunks: ChunkInput[] = input.chunker
398
399
  .chunk(
399
400
  record.markdown,
400
- DEFAULT_CHUNK_PARAMS,
401
+ input.options.chunkingToken?.params ?? DEFAULT_CHUNK_PARAMS,
401
402
  record.languageHint ?? input.collection.languageHint,
402
403
  virtualPath
403
404
  )
@@ -410,13 +411,13 @@ const persistRecord = async (
410
411
  language: chunk.language ?? undefined,
411
412
  tokenCount: chunk.tokenCount ?? undefined,
412
413
  }));
413
- mustOk(
414
- await input.store.upsertChunks(record.mirrorHash, chunks),
415
- "upsertChunks"
416
- );
417
- mustOk(
418
- await input.store.rebuildFtsForHash(record.mirrorHash),
419
- "rebuildFtsForHash"
414
+ await persistChunkLayout(
415
+ input.store,
416
+ record.mirrorHash,
417
+ chunks,
418
+ input.options.chunkingToken,
419
+ virtualPath,
420
+ record.languageHint ?? input.collection.languageHint
420
421
  );
421
422
  mustOk(
422
423
  await input.store.setDocTags(document.id, categories, "frontmatter"),
@@ -20,11 +20,13 @@ export function resolveContentTypeRules(
20
20
 
21
21
  export function withContentTypeRules(
22
22
  options: SyncOptions = {},
23
- config?: Pick<Config, "contentTypes">
23
+ config?: Pick<Config, "contentTypes" | "chunking">
24
24
  ): SyncOptions {
25
25
  const rules = options.contentTypeRules ?? resolveContentTypeRules(config);
26
+ const chunking = options.chunking ?? config?.chunking;
26
27
  return {
27
28
  ...options,
29
+ ...(chunking ? { chunking } : {}),
28
30
  contentTypeRules: rules,
29
31
  contentTypeRulesFingerprint:
30
32
  options.contentTypeRulesFingerprint ??
@@ -58,6 +58,7 @@ import {
58
58
  import { extractMemoryScopes } from "../core/memory-record";
59
59
  import { normalizeTag, validateTag } from "../core/tags";
60
60
  import { defaultChunker } from "./chunker";
61
+ import { persistChunkLayout, prepareChunking } from "./chunking";
61
62
  import {
62
63
  extractHashtags,
63
64
  parseFrontmatter,
@@ -937,7 +938,7 @@ export class SyncService {
937
938
  // 9. Chunk content
938
939
  const chunks = this.chunker.chunk(
939
940
  artifact.markdown,
940
- DEFAULT_CHUNK_PARAMS,
941
+ options.chunkingToken?.params ?? DEFAULT_CHUNK_PARAMS,
941
942
  artifact.languageHint ?? collection.languageHint,
942
943
  entry.relPath
943
944
  );
@@ -953,21 +954,15 @@ export class SyncService {
953
954
  tokenCount: c.tokenCount ?? undefined,
954
955
  }));
955
956
 
956
- // 11. Upsert chunks - CHECKED
957
- const chunksResult = await store.upsertChunks(
957
+ // 11-12. Apply chunks and lexical projection under the policy token.
958
+ await persistChunkLayout(
959
+ store,
958
960
  artifact.mirrorHash,
959
- chunkInputs
961
+ chunkInputs,
962
+ options.chunkingToken,
963
+ entry.relPath,
964
+ artifact.languageHint ?? collection.languageHint
960
965
  );
961
- mustOk(chunksResult, "upsertChunks", {
962
- mirrorHash: artifact.mirrorHash,
963
- chunkCount: chunkInputs.length,
964
- });
965
-
966
- // 12. Rebuild FTS for this hash - CHECKED
967
- const ftsResult = await store.rebuildFtsForHash(artifact.mirrorHash);
968
- mustOk(ftsResult, "rebuildFtsForHash", {
969
- mirrorHash: artifact.mirrorHash,
970
- });
971
966
 
972
967
  // 13. Extract and store tags from frontmatter and body hashtags
973
968
  // Always call setDocTags to clear removed tags on re-sync
@@ -1249,8 +1244,9 @@ export class SyncService {
1249
1244
  options: SyncOptions = {}
1250
1245
  ): Promise<CollectionSyncResult> {
1251
1246
  const startedAt = Date.now();
1247
+ const prepared = await prepareChunking(store, this.chunker, options);
1252
1248
  const syncOptions: SyncOptions = {
1253
- ...options,
1249
+ ...prepared.options,
1254
1250
  contentTypeRules: options.contentTypeRules ?? [],
1255
1251
  contentTypeRulesFingerprint:
1256
1252
  options.contentTypeRulesFingerprint ??
@@ -1511,13 +1507,18 @@ export class SyncService {
1511
1507
  syncOptions.projectTypedEdges === false
1512
1508
  ? []
1513
1509
  : await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
1514
- return summarizePathResults(
1515
- collection.name,
1516
- results,
1517
- markedInactive,
1518
- startedAt,
1519
- errors
1520
- );
1510
+ return {
1511
+ ...summarizePathResults(
1512
+ collection.name,
1513
+ results,
1514
+ markedInactive,
1515
+ startedAt,
1516
+ errors
1517
+ ),
1518
+ ...(prepared.rechunkedMirrors
1519
+ ? { rechunkedMirrors: prepared.rechunkedMirrors }
1520
+ : {}),
1521
+ };
1521
1522
  }
1522
1523
 
1523
1524
  /**
@@ -1717,8 +1718,9 @@ export class SyncService {
1717
1718
  options: SyncOptions = {}
1718
1719
  ): Promise<CollectionSyncResult> {
1719
1720
  const startTime = Date.now();
1721
+ const prepared = await prepareChunking(store, this.chunker, options);
1720
1722
  const syncOptions: SyncOptions = {
1721
- ...options,
1723
+ ...prepared.options,
1722
1724
  contentTypeRules: options.contentTypeRules ?? [],
1723
1725
  contentTypeRulesFingerprint:
1724
1726
  options.contentTypeRulesFingerprint ??
@@ -2038,6 +2040,9 @@ export class SyncService {
2038
2040
 
2039
2041
  return {
2040
2042
  collection: collection.name,
2043
+ ...(prepared.rechunkedMirrors
2044
+ ? { rechunkedMirrors: prepared.rechunkedMirrors }
2045
+ : {}),
2041
2046
  filesProcessed: entries.length + inventoryErrored,
2042
2047
  filesAdded: added,
2043
2048
  filesUpdated: updated,
@@ -2060,9 +2065,10 @@ export class SyncService {
2060
2065
  options: SyncOptions = {}
2061
2066
  ): Promise<SyncResult> {
2062
2067
  const startTime = Date.now();
2068
+ const prepared = await prepareChunking(store, this.chunker, options);
2063
2069
  const results: CollectionSyncResult[] = [];
2064
2070
  const deferredProjectionOptions: SyncOptions = {
2065
- ...options,
2071
+ ...prepared.options,
2066
2072
  projectTypedEdges: false,
2067
2073
  };
2068
2074
 
@@ -2094,6 +2100,9 @@ export class SyncService {
2094
2100
 
2095
2101
  return {
2096
2102
  collections: results,
2103
+ ...(prepared.rechunkedMirrors
2104
+ ? { rechunkedMirrors: prepared.rechunkedMirrors }
2105
+ : {}),
2097
2106
  totalDurationMs: Date.now() - startTime,
2098
2107
  totalFilesProcessed: totals.processed,
2099
2108
  totalFilesAdded: totals.added,
@@ -12,8 +12,14 @@ import type {
12
12
  RecordAttachmentInventoryItem,
13
13
  } from "../converters/types";
14
14
  import type { EgressLineage } from "../core/egress-provenance";
15
+ import type { ChunkingPolicyToken } from "../store/chunking";
15
16
  import type { DirectoryAvailabilityPort } from "./source-availability/types";
16
17
 
18
+ import {
19
+ DEFAULT_CHUNKING_PARAMS,
20
+ type ChunkingParams,
21
+ } from "../config/chunking";
22
+
17
23
  // ─────────────────────────────────────────────────────────────────────────────
18
24
  // Walker Types
19
25
  // ─────────────────────────────────────────────────────────────────────────────
@@ -115,8 +121,7 @@ export interface ChunkParams {
115
121
 
116
122
  /** Default chunk params */
117
123
  export const DEFAULT_CHUNK_PARAMS: ChunkParams = {
118
- maxTokens: 800,
119
- overlapPercent: 0.15,
124
+ ...DEFAULT_CHUNKING_PARAMS,
120
125
  };
121
126
 
122
127
  /** Chunked output */
@@ -157,6 +162,10 @@ export interface ChunkerPort {
157
162
 
158
163
  /** Sync options */
159
164
  export interface SyncOptions {
165
+ /** Index-wide configured policy; omitted means the existing defaults. */
166
+ chunking?: Partial<ChunkingParams>;
167
+ /** Internal token passed from an outer sync; never a public CLI override. */
168
+ chunkingToken?: ChunkingPolicyToken;
160
169
  /** Run git pull before scanning */
161
170
  gitPull?: boolean;
162
171
  /** Run collection updateCmd before scanning */
@@ -266,6 +275,8 @@ export interface FileSyncResult {
266
275
 
267
276
  /** Collection sync summary */
268
277
  export interface CollectionSyncResult {
278
+ /** Cached layouts updated independently of source-refresh file counters. */
279
+ rechunkedMirrors?: number;
269
280
  collection: string;
270
281
  filesProcessed: number;
271
282
  filesAdded: number;
@@ -285,6 +296,8 @@ export interface CollectionSyncResult {
285
296
 
286
297
  /** Full sync summary */
287
298
  export interface SyncResult {
299
+ /** Cached layouts updated independently of source-refresh file counters. */
300
+ rechunkedMirrors?: number;
288
301
  collections: CollectionSyncResult[];
289
302
  totalDurationMs: number;
290
303
  totalFilesProcessed: number;
@@ -8,6 +8,7 @@ import type { IndexStatus } from "../../store/types";
8
8
  import type { ToolContext } from "../server";
9
9
 
10
10
  import { buildContentTypeBoostStatus } from "../../config/content-types";
11
+ import { formatChunkingStatus } from "../../core/chunking-status";
11
12
  import { resolveModelUri } from "../../llm/registry";
12
13
  import { createStandaloneResidentStatus } from "../../serve/resident-status";
13
14
  import { runTool, type ToolResult } from "./index";
@@ -55,6 +56,9 @@ function formatStatus(status: IndexStatus): string {
55
56
  lines.push(`Embedding backlog: ${status.embeddingBacklog} chunks`);
56
57
  }
57
58
 
59
+ const chunking = formatChunkingStatus(status.chunking);
60
+ if (chunking) lines.push(chunking);
61
+
58
62
  if (status.recentErrors > 0) {
59
63
  lines.push(`Recent errors: ${status.recentErrors} (last 24h)`);
60
64
  }
@@ -79,6 +83,7 @@ export function handleStatus(
79
83
  async () => {
80
84
  const result = await ctx.store.getStatus({
81
85
  embedModel: resolveModelUri(ctx.config, "embed"),
86
+ chunking: ctx.config.chunking ?? {},
82
87
  });
83
88
  if (!result.ok) {
84
89
  throw new Error(result.error.message);
package/src/sdk/client.ts CHANGED
@@ -1352,6 +1352,7 @@ class GnoClientImpl implements GnoClient {
1352
1352
  const status = unwrapStore(
1353
1353
  await this.store.getStatus({
1354
1354
  embedModel: resolveModelUri(this.config, "embed"),
1355
+ chunking: this.config.chunking ?? {},
1355
1356
  })
1356
1357
  );
1357
1358
  return {
@@ -1,5 +1,6 @@
1
1
  import type { ContentTypeBoostStatus } from "../config/content-types";
2
2
  import type { ActivationStatus } from "../core/activation-status";
3
+ import type { ChunkingStatus } from "../store/chunking";
3
4
 
4
5
  export type HealthCheckStatus = "ok" | "warn" | "error";
5
6
 
@@ -176,6 +177,7 @@ export interface BootstrapState {
176
177
  }
177
178
 
178
179
  export interface AppStatusResponse {
180
+ chunking?: ChunkingStatus;
179
181
  resident: ResidentStatus;
180
182
  indexName: string;
181
183
  configPath: string;
@@ -654,6 +654,7 @@ export async function buildAppStatus(
654
654
  ): Promise<AppStatusResponse> {
655
655
  const result = await ctx.store.getStatus({
656
656
  embedModel: resolveModelUri(ctx.config, "embed"),
657
+ chunking: ctx.config.chunking ?? {},
657
658
  });
658
659
  if (!result.ok) {
659
660
  throw result.error;
@@ -733,6 +734,7 @@ export async function buildAppStatus(
733
734
  totalDocuments: status.activeDocuments,
734
735
  totalChunks: status.totalChunks,
735
736
  embeddingBacklog: status.embeddingBacklog,
737
+ chunking: status.chunking,
736
738
  lastUpdated: status.lastUpdatedAt,
737
739
  recentErrors: status.recentErrors,
738
740
  healthy: checks.every((check) => check.status === "ok"),
@@ -0,0 +1,31 @@
1
+ import type { ChunkingParams } from "../config/chunking";
2
+
3
+ export interface ChunkingPolicyToken {
4
+ params: ChunkingParams;
5
+ generation: number;
6
+ }
7
+
8
+ export interface PendingChunkingMirror {
9
+ mirrorHash: string;
10
+ sourcePath: string;
11
+ languageHint?: string;
12
+ }
13
+
14
+ export interface ChunkingStatus {
15
+ configured: ChunkingParams;
16
+ applied: ChunkingParams | null;
17
+ state: "empty" | "legacy-default" | "current" | "pending" | "mixed";
18
+ pendingDocuments: number;
19
+ pendingMirrors: number;
20
+ }
21
+
22
+ export class ChunkingPolicyConflictError extends Error {
23
+ readonly code = "CHUNKING_POLICY_CONFLICT";
24
+
25
+ constructor() {
26
+ super(
27
+ "CHUNKING_POLICY_CONFLICT: The index chunking policy changed. Reopen the client or restart the resident runtime with the intended configuration."
28
+ );
29
+ this.name = "ChunkingPolicyConflictError";
30
+ }
31
+ }
@@ -113,6 +113,11 @@ import type {
113
113
  import type { SqliteDbProvider } from "./types";
114
114
 
115
115
  import { buildUri, deriveDocid, stripUriIndex } from "../../app/constants";
116
+ import {
117
+ DEFAULT_CHUNKING_PARAMS,
118
+ resolveChunkingParams,
119
+ type ChunkingParams,
120
+ } from "../../config/chunking";
116
121
  import {
117
122
  type Collection,
118
123
  type Context,
@@ -140,6 +145,11 @@ import {
140
145
  parseActivationReceipt,
141
146
  serializeActivationReceipt,
142
147
  } from "../activation-receipts";
148
+ import {
149
+ ChunkingPolicyConflictError,
150
+ type ChunkingPolicyToken,
151
+ type PendingChunkingMirror,
152
+ } from "../chunking";
143
153
  import { getSchemaVersion, migrations, runMigrations } from "../migrations";
144
154
  import { err, ok } from "../types";
145
155
  import { getStoredEmbeddingFingerprint } from "../vector/freshness";
@@ -164,6 +174,15 @@ import {
164
174
  purgeDocumentChanges as purgeStoredDocumentChanges,
165
175
  snapshotDocumentChange,
166
176
  } from "./change-journal-store";
177
+ import {
178
+ assertChunkingTarget,
179
+ claimChunkingTarget,
180
+ getChunkingStatus,
181
+ markChunkingApplied,
182
+ pendingChunkingMirrors,
183
+ pruneChunkingMetadata,
184
+ readChunkingTarget,
185
+ } from "./chunking-policy";
167
186
  import {
168
187
  appendEgressAuditReceipt as appendStoredEgressAuditReceipt,
169
188
  appendEgressAuditReceiptWithRetention as appendStoredEgressAuditReceiptWithRetention,
@@ -535,6 +554,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
535
554
  private shutdownFenced = false;
536
555
  private shutdownDeadline?: number;
537
556
  private contextGeneration = 0;
557
+ private chunkingGeneration = 0;
538
558
 
539
559
  // ─────────────────────────────────────────────────────────────────────────
540
560
  // Lifecycle
@@ -603,6 +623,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
603
623
  return result;
604
624
  }
605
625
 
626
+ this.chunkingGeneration = readChunkingTarget(this.db).generation;
606
627
  this.contextGeneration += 1;
607
628
  return result;
608
629
  } catch (cause) {
@@ -2631,12 +2652,20 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2631
2652
 
2632
2653
  async upsertChunks(
2633
2654
  mirrorHash: string,
2634
- chunks: ChunkInput[]
2655
+ chunks: ChunkInput[],
2656
+ policy?: ChunkingPolicyToken
2635
2657
  ): Promise<StoreResult<void>> {
2636
2658
  try {
2637
2659
  const db = this.ensureOpen();
2638
2660
 
2639
2661
  const transaction = db.transaction(() => {
2662
+ assertChunkingTarget(
2663
+ db,
2664
+ policy ?? {
2665
+ params: DEFAULT_CHUNKING_PARAMS,
2666
+ generation: this.chunkingGeneration,
2667
+ }
2668
+ );
2640
2669
  // Retain stable rows: DELETE cascades erase valid legacy vectors even
2641
2670
  // when duplicate ingestion produces exactly the same embedding input.
2642
2671
  const nextBySequence = new Map(
@@ -2693,13 +2722,83 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2693
2722
  return ok(undefined);
2694
2723
  } catch (cause) {
2695
2724
  return err(
2696
- "QUERY_FAILED",
2725
+ cause instanceof ChunkingPolicyConflictError
2726
+ ? cause.code
2727
+ : "QUERY_FAILED",
2697
2728
  cause instanceof Error ? cause.message : "Failed to upsert chunks",
2698
2729
  cause
2699
2730
  );
2700
2731
  }
2701
2732
  }
2702
2733
 
2734
+ async claimChunkingPolicy(
2735
+ input: ChunkingParams
2736
+ ): Promise<StoreResult<ChunkingPolicyToken>> {
2737
+ const params = resolveChunkingParams(input);
2738
+ const result = await this.withTransaction(async () =>
2739
+ claimChunkingTarget(this.ensureOpen(), this.chunkingGeneration, params)
2740
+ );
2741
+ if (result.ok) this.chunkingGeneration = result.value.generation;
2742
+ else if (result.error.cause instanceof ChunkingPolicyConflictError) {
2743
+ return err(
2744
+ "CHUNKING_POLICY_CONFLICT",
2745
+ result.error.message,
2746
+ result.error.cause
2747
+ );
2748
+ }
2749
+ return result;
2750
+ }
2751
+
2752
+ async listPendingChunkingMirrors(
2753
+ policy: ChunkingPolicyToken,
2754
+ afterHash = ""
2755
+ ): Promise<StoreResult<PendingChunkingMirror[]>> {
2756
+ try {
2757
+ return ok(pendingChunkingMirrors(this.ensureOpen(), policy, afterHash));
2758
+ } catch (cause) {
2759
+ return err(
2760
+ cause instanceof ChunkingPolicyConflictError
2761
+ ? cause.code
2762
+ : "QUERY_FAILED",
2763
+ cause instanceof Error
2764
+ ? cause.message
2765
+ : "Failed to list pending chunk layouts",
2766
+ cause
2767
+ );
2768
+ }
2769
+ }
2770
+
2771
+ async applyChunkLayout(
2772
+ mirrorHash: string,
2773
+ chunks: ChunkInput[],
2774
+ policy: ChunkingPolicyToken,
2775
+ sourcePath: string,
2776
+ languageHint?: string
2777
+ ): Promise<StoreResult<void>> {
2778
+ const result = await this.withTransaction(async () => {
2779
+ const db = this.ensureOpen();
2780
+ assertChunkingTarget(db, policy);
2781
+ const applied = await this.upsertChunks(mirrorHash, chunks, policy);
2782
+ if (!applied.ok)
2783
+ throw applied.error.cause ?? new Error(applied.error.message);
2784
+ const indexed = await this.rebuildFtsForHash(mirrorHash);
2785
+ if (!indexed.ok)
2786
+ throw indexed.error.cause ?? new Error(indexed.error.message);
2787
+ markChunkingApplied(db, mirrorHash, policy, sourcePath, languageHint);
2788
+ });
2789
+ if (
2790
+ !result.ok &&
2791
+ result.error.cause instanceof ChunkingPolicyConflictError
2792
+ ) {
2793
+ return err(
2794
+ "CHUNKING_POLICY_CONFLICT",
2795
+ result.error.message,
2796
+ result.error.cause
2797
+ );
2798
+ }
2799
+ return result;
2800
+ }
2801
+
2703
2802
  async getChunks(mirrorHash: string): Promise<StoreResult<ChunkRow[]>> {
2704
2803
  try {
2705
2804
  const db = this.ensureOpen();
@@ -5690,6 +5789,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5690
5789
  async getStatus(options?: {
5691
5790
  embedModel?: string;
5692
5791
  embedFingerprint?: string;
5792
+ chunking?: Partial<ChunkingParams>;
5693
5793
  }): Promise<StoreResult<IndexStatus>> {
5694
5794
  try {
5695
5795
  const db = this.ensureOpen();
@@ -5884,6 +5984,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5884
5984
  activeDocuments: totalsRow?.active ?? 0,
5885
5985
  totalChunks: chunkCount,
5886
5986
  embeddingBacklog: variantStatus?.backlog ?? backlogRow?.count ?? 0,
5987
+ chunking: getChunkingStatus(db, options?.chunking),
5887
5988
  recentErrors,
5888
5989
  lastUpdatedAt: lastUpdatedRow?.last_updated ?? null,
5889
5990
  healthy,
@@ -5968,6 +6069,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5968
6069
  )
5969
6070
  `);
5970
6071
  orphanedContent = contentResult.changes;
6072
+ pruneChunkingMetadata(db);
5971
6073
 
5972
6074
  // Delete chunks for deleted content
5973
6075
  const chunksResult = db.run(`